+```
+
+#### 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.
+
+### 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)
+
+### 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 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.
+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`).
+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`.
+
+## Upgrading to `v1.250.0`
+
+Components PR [#723](https://github.com/cloudposse/terraform-aws-components/pull/723)
+
+### 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 43cf6b9f2..b4c723e2d 100644
--- a/modules/eks/cluster/README.md
+++ b/modules/eks/cluster/README.md
@@ -1,7 +1,23 @@
-# Component: `eks`
+---
+tags:
+ - component/eks/cluster
+ - layer/eks
+ - provider/aws
+---
-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.
+# Component: `eks/cluster`
+
+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.
## Usage
@@ -9,27 +25,159 @@ 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/) 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/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
+
+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:
+ eks/cluster:
vars:
enabled: true
- cluster_kubernetes_version: "1.21"
- availability_zones: ["us-west-2a", "us-west-2b", "us-west-2c"]
+ name: eks
+ 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"]
+ 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 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: # 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
+ # 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: 6
+ 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
@@ -38,83 +186,381 @@ components:
attributes: []
create_before_destroy: true
- disk_size: 100
cluster_autoscaler_enabled: true
instance_types:
- - t3.medium
+ # 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!
+ # 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-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:
+ - system:masters
+ - aws_team_role: poweruser
+ groups:
+ - idp:poweruser
+ - aws_team_role: observer
+ groups:
+ - idp:observer
+ - aws_team_role: planner
+ groups:
+ - idp:observer
+ - aws_team: terraform
+ groups:
+ - system:masters
+
+ # 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
+
+ # Set to false if you are not using Karpenter
+ karpenter_iam_role_enabled: true
+
+ # 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
+```
+
+### 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 | 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).
+
+### 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).
+
+> [!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
+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
```
+> [!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
+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
+
+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:
+
+```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
+ # 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
+ # 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
+ # 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. 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:
+ coredns:
+ 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.
+
+For more information on upgrading EKS Addons, see
+["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
+
+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:
+ 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 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:
+
+ ```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 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 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:
+
+ ```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 |
+| [random](#requirement\_random) | >= 3.0 |
## Providers
| Name | Version |
|------|---------|
| [aws](#provider\_aws) | >= 4.9.0 |
+| [random](#provider\_random) | >= 3.0 |
## Modules
| 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 |
-| [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 |
+| [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 |
+| [coredns\_fargate\_profile](#module\_coredns\_fargate\_profile) | cloudposse/eks-fargate-profile/aws | 1.3.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 |
| [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 |
| [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 |
+| [utils](#module\_utils) | cloudposse/utils/aws | 1.3.0 |
+| [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
| 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 |
+| [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
| 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\_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 |
+| [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\_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 |
| [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 |
| [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 |
@@ -122,39 +568,33 @@ components:
| [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 |
+| [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 |
| [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 |
-| [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) | **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 = 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 |
+| [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 |
| [region](#input\_region) | AWS Region | `string` | n/a | yes |
@@ -162,11 +602,14 @@ components:
| [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
| 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 |
@@ -186,10 +629,17 @@ components:
| [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
+
+- [EKS Foundational Platform](https://docs.cloudposse.com/layers/eks/)
## 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..078820a93
--- /dev/null
+++ b/modules/eks/cluster/addons.tf
@@ -0,0 +1,221 @@
+# 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 = 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 = [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 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
+ 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)
+
+ 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_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
+ ]
+
+ 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_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 ||
+ # 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(
+ (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[*])
+ } : {}),
+ # 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
+ )
+}
+
+# `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_sa_needed ? 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_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"
+}
+
+module "vpc_cni_eks_iam_role" {
+ source = "cloudposse/eks-iam-role/aws"
+ version = "2.1.1"
+
+ enabled = local.vpc_cni_sa_needed
+
+ 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
+}
+
+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 = 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 = "${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)
+
+ 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
+# 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.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"
+}
+
+module "aws_ebs_csi_driver_eks_iam_role" {
+ source = "cloudposse/eks-iam-role/aws"
+ version = "2.1.1"
+
+ enabled = local.ebs_csi_sa_needed
+
+ eks_cluster_oidc_issuer_url = local.eks_cluster_oidc_issuer_url
+
+ service_account_name = "ebs-csi-controller-sa"
+ 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 = 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 = "${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)
+
+ attributes = ["ebs-csi"]
+ context = module.this.context
+}
+
+# `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.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"
+}
+
+module "aws_efs_csi_driver_eks_iam_role" {
+ source = "cloudposse/eks-iam-role/aws"
+ version = "2.1.1"
+
+ enabled = local.efs_csi_sa_needed
+
+ 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
+}
diff --git a/modules/eks/cluster/aws-sso.tf b/modules/eks/cluster/aws-sso.tf
new file mode 100644
index 000000000..5e2eaf36f
--- /dev/null
+++ b/modules/eks/cluster/aws-sso.tf
@@ -0,0 +1,26 @@
+# 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 {
+
+ aws_sso_access_entry_map = {
+ 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
+ }
+ }
+}
+
+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}"
+ )
+ }
+ }
+}
diff --git a/modules/eks/cluster/eks-node-groups.tf b/modules/eks/cluster/eks-node-groups.tf
index f061f7cf1..a0d43ea77 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
@@ -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
@@ -38,23 +40,96 @@ 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
- 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
+ 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
aws_ssm_agent_enabled = var.aws_ssm_agent_enabled
vpc_id = local.vpc_id
- # See "Ensure ordering of resource creation" comment above for explanation
- # of "module_depends_on"
- module_depends_on = module.eks_cluster.kubernetes_config_map_id
+ block_device_map = lookup(local.legacy_converted_block_device_map, each.key, local.block_device_map_w_defaults[each.key])
} : null
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/fargate-profiles.tf b/modules/eks/cluster/fargate-profiles.tf
index d0eb07857..17e494572 100644
--- a/modules/eks/cluster/fargate-profiles.tf
+++ b/modules/eks/cluster/fargate-profiles.tf
@@ -1,19 +1,49 @@
locals {
- fargate_profiles = local.enabled ? var.fargate_profiles : {}
+ fargate_profiles = local.enabled ? var.fargate_profiles : {}
+ 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)
+ )
}
+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 = local.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.1.0"
+ version = "1.3.0"
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
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/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 8a1a80d95..c9677bb9b 100644
--- a/modules/eks/cluster/main.tf
+++ b/modules/eks/cluster/main.tf
@@ -1,65 +1,79 @@
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
-
- 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)
-
- 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)
- groups = role.groups
- }]
+ enabled = module.this.enabled
+ vpc_outputs = module.vpc.outputs
+
+ attributes = flatten(concat(module.this.attributes, [var.color]))
+
+ this_account_name = module.iam_roles.current_account_account_name
- 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)
- 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]
+ 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
+ }
+ }
+
+ ## 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
}
- ]
+ }
- map_additional_iam_roles = concat(
- local.primary_iam_roles,
- local.delegated_iam_roles,
- var.map_additional_iam_roles,
- local.map_fargate_profile_roles,
- )
+ 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 = 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 = local.enabled ? 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_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.
+ # 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
@@ -70,78 +84,110 @@ locals {
module.vpc_ingress[k].outputs.vpc_cidr
]
)
+
+ 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
+ # 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 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)
+
+ # Infer the availability zones from the private subnets if var.availability_zones is empty:
+ 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" {
+ 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" {
source = "cloudposse/eks-cluster/aws"
- version = "2.5.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 explict
- # 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
- # 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 = 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
- # 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])
+ # EKS addons
+ addons = local.addons
- # 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
+ 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
cluster_encryption_config_enabled = var.cluster_encryption_config_enabled
cluster_encryption_config_kms_key_id = var.cluster_encryption_config_kms_key_id
@@ -152,4 +198,3 @@ module "eks_cluster" {
context = module.this.context
}
-
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..8172f0a05 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 {
@@ -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.6.0"
+ version = "3.0.1"
enabled = local.enabled
@@ -57,13 +63,14 @@ 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
- }] : []
+ # 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
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..a167d6ae1 100644
--- a/modules/eks/cluster/modules/node_group_by_az/variables.tf
+++ b/modules/eks/cluster/modules/node_group_by_az/variables.tf
@@ -20,21 +20,45 @@ 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
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)
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/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/modules/node_group_by_region/variables.tf b/modules/eks/cluster/modules/node_group_by_region/variables.tf
index b413c2181..7b902c186 100644
--- a/modules/eks/cluster/modules/node_group_by_region/variables.tf
+++ b/modules/eks/cluster/modules/node_group_by_region/variables.tf
@@ -21,21 +21,46 @@ 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
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
+ module_depends_on = optional(any)
resources_to_tag = list(string)
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/outputs.tf b/modules/eks/cluster/outputs.tf
index e32789647..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" {
@@ -80,15 +80,35 @@ 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" {
+ 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
+}
+
+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/providers.tf b/modules/eks/cluster/providers.tf
index 9610c5073..8ad77541f 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,19 +9,15 @@ 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
}
}
module "iam_roles" {
- source = "../../account-map/modules/iam-roles"
- context = module.this.context
-}
+ source = "../../account-map/modules/iam-roles"
-variable "import_role_arn" {
- type = string
- default = null
- description = "IAM Role ARN to use when importing a resource"
+ profiles_enabled = false
+
+ context = module.this.context
}
diff --git a/modules/eks/cluster/remote-state.tf b/modules/eks/cluster/remote-state.tf
index bac0ec31e..0ad0cd62d 100644
--- a/modules/eks/cluster/remote-state.tf
+++ b/modules/eks/cluster/remote-state.tf
@@ -1,67 +1,48 @@
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 = local.enabled ? {
+ 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
+ } : {}
}
-module "vpc" {
- source = "cloudposse/stack-config/yaml//modules/remote-state"
- 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"
+module "iam_arns" {
+ source = "../../account-map/modules/roles-to-principals"
- 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)
+ role_map = local.role_map
context = module.this.context
}
-module "team_roles" {
+module "vpc" {
source = "cloudposse/stack-config/yaml//modules/remote-state"
- version = "1.3.1"
+ version = "1.5.0"
- component = "aws-teams"
+ bypass = !local.enabled
+ component = var.vpc_component_name
- tenant = local.iam_primary_roles_tenant_name
- environment = var.iam_roles_environment_name
- stage = var.iam_primary_roles_stage_name
+ defaults = {
+ public_subnet_ids = []
+ private_subnet_ids = []
+ vpc = {
+ subnet_type_tag_key = ""
+ }
+ vpc_cidr = null
+ vpc_id = null
+ }
context = module.this.context
}
-module "delegated_roles" {
+module "vpc_ingress" {
source = "cloudposse/stack-config/yaml//modules/remote-state"
- version = "1.3.1"
+ version = "1.5.0"
- component = "aws-team-roles"
+ for_each = local.accounts_with_vpc
- environment = var.iam_roles_environment_name
+ component = var.vpc_component_name
+ environment = try(each.value.environment, module.this.environment)
+ stage = try(each.value.stage, module.this.stage)
+ tenant = try(each.value.tenant, module.this.tenant)
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.3.1"
-
- component = var.eks_component_name
-
- defaults = {
- eks_managed_node_workers_role_arns = []
- fargate_profile_role_arns = []
- }
-
- context = module.this.context
-}
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 161e01601..4d1e1d884 100644
--- a/modules/eks/cluster/variables.tf
+++ b/modules/eks/cluster/variables.tf
@@ -7,7 +7,21 @@ 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.
+ 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 = []
}
@@ -16,6 +30,8 @@ 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,320 +42,393 @@ 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"
-}
-
-variable "map_additional_aws_accounts" {
- description = "Additional AWS account numbers to add to `aws-auth` ConfigMap"
- type = list(string)
- default = []
-}
-
-variable "map_additional_worker_roles" {
- description = "AWS IAM Role ARNs of worker nodes to add to `aws-auth` ConfigMap"
- type = list(string)
- default = []
-}
-
-variable "primary_iam_roles" {
- description = "Primary IAM roles to add to `aws-auth` ConfigMap"
-
+# 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({
- role = string
- groups = list(string)
+ 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 "delegated_iam_roles" {
- description = "Delegated IAM roles to add to `aws-auth` ConfigMap"
-
+variable "aws_sso_permission_sets_rbac" {
type = list(object({
- role = string
- groups = list(string)
+ aws_sso_permission_set = string
+ groups = list(string)
}))
- default = []
+ 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
+
+ default = []
+ 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" {
- description = "Additional IAM roles to add to `config-map-aws-auth` ConfigMap"
-
type = list(object({
rolearn = string
- username = string
+ username = optional(string)
groups = list(string)
}))
- default = []
+ 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
}
variable "map_additional_iam_users" {
- description = "Additional IAM users to add to `aws-auth` ConfigMap"
-
type = list(object({
userarn = string
- username = string
+ username = optional(string)
groups = list(string)
}))
- default = []
+ 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
}
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" {
# 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
- availability_zones = list(string)
+ # 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 = 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)
+ 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 = 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"
default = {}
+ nullable = false
}
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), {})
+ 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)
+ })), [])
+ 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)
})
+
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
+ # 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 behavior.
+ 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
+ }
+ }
+ }
}
-}
-
-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
+ 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
-}
-
-variable "kubeconfig_file" {
- type = string
- default = ""
- description = "Name of `kubeconfig` file to use to configure Kubernetes provider"
-}
-
-variable "kubeconfig_file_enabled" {
- type = bool
- default = false
- 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
-}
-
-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"
+ 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.
@@ -351,18 +440,23 @@ variable "allow_ingress_from_vpc_accounts" {
tenant = "core"
}
EOF
+
+ default = []
+ nullable = false
}
-variable "eks_component_name" {
+variable "vpc_component_name" {
type = string
- description = "The name of the eks component"
- default = "eks/cluster"
+ 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" {
@@ -370,14 +464,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" {
@@ -385,3 +482,87 @@ 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 = 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)
+ }))
+
+ description = "Manages [EKS addons](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_addon) resources"
+ default = {}
+ nullable = false
+}
+
+variable "deploy_addons_to_fargate" {
+ type = bool
+ description = "Set to `true` (not recommended) to deploy addons to Fargate instead of initial node pool"
+ default = false
+ nullable = false
+}
+
+variable "addons_depends_on" {
+ type = bool
+
+ description = <<-EOT
+ 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 = true
+ 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
+}
+
+variable "legacy_do_not_create_karpenter_instance_profile" {
+ type = bool
+ description = <<-EOT
+ **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`.
+ 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 cc73ffd35..601150b50 100644
--- a/modules/eks/cluster/versions.tf
+++ b/modules/eks/cluster/versions.tf
@@ -1,10 +1,23 @@
terraform {
- required_version = ">= 1.0.0"
+ required_version = ">= 1.3.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 4.9.0"
}
+ random = {
+ 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"
+ # }
}
}
diff --git a/modules/eks/datadog-agent/CHANGELOG.md b/modules/eks/datadog-agent/CHANGELOG.md
new file mode 100644
index 000000000..06748cc00
--- /dev/null
+++ b/modules/eks/datadog-agent/CHANGELOG.md
@@ -0,0 +1,66 @@
+## 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 `values.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/datadog-agent/README.md b/modules/eks/datadog-agent/README.md
similarity index 76%
rename from modules/datadog-agent/README.md
rename to modules/eks/datadog-agent/README.md
index 24c18d9af..23a4d2419 100644
--- a/modules/datadog-agent/README.md
+++ b/modules/eks/datadog-agent/README.md
@@ -1,15 +1,15 @@
-# Component: `datadog-agent`
+---
+tags:
+ - component/eks/datadog-agent
+ - layer/datadog
+ - provider/aws
+ - provider/helm
+ - provider/datadog
+---
-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.
+# Component: `eks/datadog-agent`
-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]'
-```
+This component installs the `datadog-agent` for EKS clusters.
## Usage
@@ -26,21 +26,47 @@ 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.29.2"
+ 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,33 +77,69 @@ 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"
- "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.
+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
+### 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
-## Monitoring Cluster Checks
+> [!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)
-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).
+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).
```yaml
https-checks:
@@ -89,7 +151,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
@@ -104,7 +166,7 @@ https-checks:
new_host_delay: 0
new_group_delay: 0
no_data_timeframe: 2
- threshold_windows: { }
+ threshold_windows: {}
thresholds:
critical: 1
warning: 1
@@ -113,53 +175,47 @@ 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
| Name | Version |
|------|---------|
| [terraform](#requirement\_terraform) | >= 1.0.0 |
-| [aws](#requirement\_aws) | ~> 4.0 |
-| [helm](#requirement\_helm) | >= 2.3.0 |
-| [utils](#requirement\_utils) | >= 0.3.0 |
+| [aws](#requirement\_aws) | >= 4.9.0 |
+| [helm](#requirement\_helm) | >= 2.7 |
+| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 |
+| [utils](#requirement\_utils) | >= 1.10.0 |
## Providers
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | ~> 4.0 |
-| [kubernetes](#provider\_kubernetes) | n/a |
+| [aws](#provider\_aws) | >= 4.9.0 |
## Modules
| Name | Source | Version |
|------|--------|---------|
-| [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 |
-| [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 |
+| [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 |
+| [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 |
## 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 |
-| [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
@@ -174,8 +230,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 |
@@ -186,17 +240,16 @@ 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 |
| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `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\_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 |
@@ -210,11 +263,11 @@ 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 |
| [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 |
@@ -222,10 +275,13 @@ 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 |
+
## 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
+
+- 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/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/eks/datadog-agent/context.tf b/modules/eks/datadog-agent/context.tf
new file mode 100644
index 000000000..5e0ef8856
--- /dev/null
+++ b/modules/eks/datadog-agent/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/datadog-agent/helm-variables.tf b/modules/eks/datadog-agent/helm-variables.tf
new file mode 100644
index 000000000..fade04b95
--- /dev/null
+++ b/modules/eks/datadog-agent/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-agent/main.tf b/modules/eks/datadog-agent/main.tf
similarity index 58%
rename from modules/datadog-agent/main.tf
rename to modules/eks/datadog-agent/main.tf
index 876e0e809..a22c81898 100644
--- a/modules/datadog-agent/main.tf
+++ b/modules/eks/datadog-agent/main.tf
@@ -3,19 +3,15 @@ locals {
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
+ 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
- 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
@@ -28,10 +24,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 +36,27 @@ 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)
- }]
+ }
+ ]
+}
+
+module "datadog_configuration" {
+ source = "../../datadog-configuration/modules/datadog_keys"
+ context = module.this.context
}
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
@@ -69,37 +71,43 @@ module "datadog_cluster_check_yaml_config" {
context = module.this.context
}
-resource "kubernetes_namespace" "default" {
- count = local.enabled && var.create_namespace ? 1 : 0
-
- metadata {
- name = var.kubernetes_namespace
+module "values_merge" {
+ source = "cloudposse/config/yaml//modules/deepmerge"
+ version = "1.0.2"
- labels = local.tags
- }
+ # Merge in order: datadog values, var.values
+ maps = [
+ yamldecode(
+ file("${path.module}/values.yaml")
+ ),
+ var.values,
+ ]
}
+
module "datadog_agent" {
source = "cloudposse/helm-release/aws"
- version = "0.6.0"
-
- name = module.this.name
- chart = var.chart
- description = var.description
- repository = var.repository
- chart_version = var.chart_version
- 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.10.0"
+
+ 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
values = [
- file("${path.module}/values.yaml")
+ yamlencode(module.values_merge.merged)
]
set_sensitive = [
@@ -112,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
}
]
@@ -125,8 +138,10 @@ module "datadog_agent" {
name = "datadog.clusterName"
type = "string"
value = module.eks.outputs.eks_cluster_id
- }
+ },
], local.set_datadog_cluster_checks)
- depends_on = [kubernetes_namespace.default]
+ iam_role_enabled = false
+
+ context = module.this.context
}
diff --git a/modules/datadog-agent/outputs.tf b/modules/eks/datadog-agent/outputs.tf
similarity index 65%
rename from modules/datadog-agent/outputs.tf
rename to modules/eks/datadog-agent/outputs.tf
index 331f07a18..f63ad1975 100644
--- a/modules/datadog-agent/outputs.tf
+++ b/modules/eks/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/eks/datadog-agent/provider-helm.tf b/modules/eks/datadog-agent/provider-helm.tf
new file mode 100644
index 000000000..91cc7f6d4
--- /dev/null
+++ b/modules/eks/datadog-agent/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/datadog-agent/providers.tf b/modules/eks/datadog-agent/providers.tf
new file mode 100644
index 000000000..89ed50a98
--- /dev/null
+++ b/modules/eks/datadog-agent/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/datadog-agent/remote-state.tf b/modules/eks/datadog-agent/remote-state.tf
new file mode 100644
index 000000000..c1ec8226d
--- /dev/null
+++ b/modules/eks/datadog-agent/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/datadog-agent/values.yaml b/modules/eks/datadog-agent/values.yaml
new file mode 100644
index 000000000..b8215b2ab
--- /dev/null
+++ b/modules/eks/datadog-agent/values.yaml
@@ -0,0 +1,117 @@
+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: true
+ socketEnabled: true
+ useSocketVolume: true
+ serviceMonitoring:
+ enabled: true
+ processAgent:
+ enabled: true
+ processCollection: true
+ systemProbe:
+ enableTCPQueueLength: true
+ enableOOMKill: true
+ collectDNSStats: true
+ enableConntrack: true
+ bpfDebug: false
+ orchestratorExplorer:
+ enabled: true
+ networkMonitoring:
+ enabled: false
+ clusterTagger:
+ collectKubernetesTags: true
+ clusterChecksRunner:
+ enabled: false
+ clusterChecks:
+ enabled: true
+ dogstatsd:
+ useHostPort: true
+ nonLocalTraffic: true
+ securityAgent:
+ runtime:
+ enabled: false
+ compliance:
+ enabled: true
+ helmCheck:
+ 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.
+ 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:
+ pullPolicy: IfNotPresent
+ metricsProvider:
+ enabled: false
+ resources:
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ limits:
+ 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
+ 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
+ tolerations:
+ - effect: NoSchedule
+ operator: Exists
+ - effect: NoExecute
+ operator: Exists
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: eks.amazonaws.com/compute-type
+ operator: NotIn
+ values:
+ - fargate
diff --git a/modules/datadog-agent/variables.tf b/modules/eks/datadog-agent/variables.tf
similarity index 69%
rename from modules/datadog-agent/variables.tf
rename to modules/eks/datadog-agent/variables.tf
index bdc7fa99d..89d16a61b 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"
@@ -56,3 +38,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/eks/datadog-agent/versions.tf b/modules/eks/datadog-agent/versions.tf
new file mode 100644
index 000000000..b104e91ca
--- /dev/null
+++ b/modules/eks/datadog-agent/versions.tf
@@ -0,0 +1,22 @@
+terraform {
+ required_version = ">= 1.0.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 4.9.0"
+ }
+ helm = {
+ source = "hashicorp/helm"
+ version = ">= 2.7"
+ }
+ utils = {
+ source = "cloudposse/utils"
+ version = ">= 1.10.0"
+ }
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = ">= 2.14.0, != 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..5dc0fb54a
--- /dev/null
+++ b/modules/eks/echo-server/CHANGELOG.md
@@ -0,0 +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.
+- 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 dc94bec58..8ad731f57 100644
--- a/modules/eks/echo-server/README.md
+++ b/modules/eks/echo-server/README.md
@@ -1,28 +1,51 @@
+---
+tags:
+ - component/eks/echo-server
+ - layer/eks
+ - provider/aws
+ - provider/echo-server
+---
+
# 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
-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.
-Therefore, it requires several other components.
-At the moment, it supports 2 configurations:
+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.
+
+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)
+
+- 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`
- [kubernetes-sigs/external-dns](https://github.com/kubernetes-sigs/external-dns)
@@ -31,10 +54,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
@@ -42,6 +64,26 @@ 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:
@@ -60,11 +102,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
@@ -73,7 +119,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
@@ -85,8 +131,8 @@ 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 |
+| [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 |
@@ -94,9 +140,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
@@ -116,11 +160,9 @@ 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 |
-| [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 |
@@ -128,7 +170,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 |
@@ -153,8 +196,11 @@ components:
| Name | Description |
|------|-------------|
+| [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/echo-server/charts/echo-server/Chart.yaml b/modules/eks/echo-server/charts/echo-server/Chart.yaml
index 03519d53b..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.2.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.2.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..1eade38de 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,35 @@ 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:
+ {{- with index . "limits" }}
+ limits:
+ cpu: {{ index . "cpu" | default "50m" }}
+ memory: {{ index . "memory" | default "128Mi" }}
+ {{- end }}
+ {{- with index . "requests" }}
+ requests:
+ cpu: {{ index . "cpu" | default "50m" }}
+ memory: {{ index . "memory" | default "128Mi" }}
+ {{- end }}
+ {{- 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 f5e6473fa..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,46 +2,57 @@
{{- $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" }}
- {{- 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" }}
- 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:
- - {{ .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 }}
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 c11f21f9c..5d24c2681 100644
--- a/modules/eks/echo-server/main.tf
+++ b/modules/eks/echo-server/main.tf
@@ -1,12 +1,13 @@
locals {
- enabled = module.this.enabled
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.7.0"
+ version = "0.10.1"
name = module.this.name
chart = "${path.module}/charts/echo-server"
@@ -30,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"
},
{
@@ -52,4 +53,3 @@ module "echo_server" {
context = module.this.context
}
-
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/provider-helm.tf b/modules/eks/echo-server/provider-helm.tf
index 20e4d3837..91cc7f6d4 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.
@@ -15,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" {
@@ -36,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" {
@@ -45,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 = 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"
+ 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
@@ -95,14 +133,17 @@ 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 = 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" {
@@ -113,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)
- 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
+ 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 ? ["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"
@@ -132,21 +174,22 @@ 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
- # 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
+ 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 ? ["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/providers.tf b/modules/eks/echo-server/providers.tf
index 2775903d2..89ed50a98 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 = assume_role.value
}
}
}
@@ -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/remote-state.tf b/modules/eks/echo-server/remote-state.tf
index 90c6ab1a8..c1ec8226d 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.5.0"
component = var.eks_component_name
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/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/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
diff --git a/modules/eks/eks-without-spotinst/default.auto.tfvars b/modules/eks/eks-without-spotinst/default.auto.tfvars
deleted file mode 100644
index bccc95614..000000000
--- a/modules/eks/eks-without-spotinst/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/external-dns/README.md b/modules/eks/external-dns/README.md
index 823d5782f..0b8f02345 100644
--- a/modules/eks/external-dns/README.md
+++ b/modules/eks/external-dns/README.md
@@ -1,6 +1,16 @@
+---
+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 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
@@ -25,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
# ---
@@ -43,105 +57,115 @@ 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")
```
+
## 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 |
+| 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
| 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 |
-| [external\_dns](#module\_external\_dns) | cloudposse/helm-release/aws | 0.7.0 |
+| [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 |
+| [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 |
## 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\_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\_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 |
-| [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 |
+| 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 |
+
## References
diff --git a/modules/eks/external-dns/main.tf b/modules/eks/external-dns/main.tf
index 9292bd032..058aa7ccf 100644
--- a/modules/eks/external-dns/main.tf
+++ b/modules/eks/external-dns/main.tf
@@ -8,7 +8,9 @@ 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,
+ flatten([for k, v in module.additional_dns_components : [for i, j in v.outputs.zones : j.zone_id]])
))
}
@@ -18,7 +20,7 @@ data "aws_partition" "current" {
module "external_dns" {
source = "cloudposse/helm-release/aws"
- version = "0.7.0"
+ version = "0.10.0"
name = module.this.name
chart = var.chart
@@ -96,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"),
diff --git a/modules/eks/external-dns/provider-helm.tf b/modules/eks/external-dns/provider-helm.tf
index 9bb5edb6f..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 = 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"
+ 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
@@ -101,16 +133,17 @@ 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
] : []
# 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/providers.tf b/modules/eks/external-dns/providers.tf
index c2419aabb..89ed50a98 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 = assume_role.value
}
}
}
@@ -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-dns/remote-state.tf b/modules/eks/external-dns/remote-state.tf
index a6d442848..9f15458c7 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.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.3.1"
+ version = "1.5.0"
component = "dns-delegated"
environment = var.dns_gbl_delegated_environment_name
@@ -20,3 +20,30 @@ module "dns_gbl_delegated" {
zones = {}
}
}
+
+module "dns_gbl_primary" {
+ source = "cloudposse/stack-config/yaml//modules/remote-state"
+ version = "1.5.0"
+
+ component = "dns-primary"
+ environment = var.dns_gbl_primary_environment_name
+
+ context = module.this.context
+
+ ignore_errors = true
+
+ defaults = {
+ 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 a63780e4a..8689b5530 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" {
@@ -126,6 +126,22 @@ 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 "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"
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/CHANGELOG.md b/modules/eks/external-secrets-operator/CHANGELOG.md
new file mode 100644
index 000000000..2a073f4d6
--- /dev/null
+++ b/modules/eks/external-secrets-operator/CHANGELOG.md
@@ -0,0 +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.
+
+## Fixes
+
+- Set default chart
diff --git a/modules/eks/external-secrets-operator/README.md b/modules/eks/external-secrets-operator/README.md
new file mode 100644
index 000000000..413d201fa
--- /dev/null
+++ b/modules/eks/external-secrets-operator/README.md
@@ -0,0 +1,207 @@
+---
+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
+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: 30s
+ secretStoreRef:
+ name: "secret-store-parameter-store" # Must match name of the Cluster Secret Store created by this component
+ kind: ClusterSecretStore
+ target:
+ creationPolicy: Owner
+ name: app-secrets
+ dataFrom:
+ - 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"
+```
+
+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/recipes.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"
+ 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: {}
+ kms_aliases_allow_decrypt: []
+ # - "alias/foo/bar"
+```
+
+
+
+## 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, != 2.21.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | >= 4.0 |
+| [kubernetes](#provider\_kubernetes) | >= 2.7.1, != 2.21.0, != 2.21.0 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [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.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 |
+
+## 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 |
+| [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
+
+| 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` | `"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` | `"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 |
+| [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 |
+| [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 |
+| [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 |
+| [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 |
+| [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
+
+- [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/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/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..ea4928d7a
--- /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: 30s
+ secretStoreRef:
+ name: "secret-store-parameter-store" # Must match name of the Cluster Secret Store created by this component
+ kind: ClusterSecretStore
+ target:
+ creationPolicy: Owner
+ name: app-secrets
+ dataFrom:
+ - 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
new file mode 100644
index 000000000..b88414ef2
--- /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: 30s
+ secretStoreRef:
+ name: "secret-store-parameter-store" # Must match name of the Cluster Secret Store created by this component
+ kind: ClusterSecretStore
+ target:
+ creationPolicy: Owner
+ name: single-secret
+ data:
+ - secretKey: good_secret
+ remoteRef:
+ key: /app/good_secret
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..a0b007642
--- /dev/null
+++ b/modules/eks/external-secrets-operator/helm-variables.tf
@@ -0,0 +1,71 @@
+variable "kubernetes_namespace" {
+ type = string
+ description = "The namespace to install the release into."
+}
+
+variable "chart_description" {
+ type = string
+ description = "Set release description attribute (visible in the history)."
+ 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 = "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 = "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 = "0.6.0-rc1"
+ # using RC to address this bug https://github.com/external-secrets/external-secrets/issues/1511
+}
+
+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 = null
+}
+
+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..bd79a0400
--- /dev/null
+++ b/modules/eks/external-secrets-operator/main.tf
@@ -0,0 +1,158 @@
+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.10.1"
+
+ name = "" # avoid redundant release name in IAM role: ...-ekc-cluster-external-secrets-operator-external-secrets-operator@secrets
+ description = var.chart_description
+
+ 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
+ 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://", "")
+
+ service_account_name = module.this.name
+ service_account_namespace = var.kubernetes_namespace
+
+ iam_role_enabled = true
+ iam_policy = [{
+ statements = concat([
+ {
+ sid = "ReadParameterStore"
+ effect = "Allow"
+ actions = [
+ "ssm:GetParameter*"
+ ]
+ 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"
+ effect = "Allow"
+ actions = [
+ "ssm:DescribeParameter*"
+ ]
+ resources = [
+ "arn:aws:ssm:${var.region}:${local.account}:*"
+ ]
+ }],
+ 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
+ }
+ ] : []
+ )
+ }]
+
+ values = compact([
+ yamlencode({
+ serviceAccount = {
+ name = module.this.name
+ }
+ rbac = {
+ create = var.rbac_enabled
+ }
+ }),
+ # additional 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==externalsecrets.external-secrets.io"
+}
+
+module "external_ssm_secrets" {
+ source = "cloudposse/helm-release/aws"
+ version = "0.10.1"
+
+ 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."
+
+ 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,
+ ]
+}
+
+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/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..91cc7f6d4
--- /dev/null
+++ b/modules/eks/external-secrets-operator/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/external-secrets-operator/providers.tf b/modules/eks/external-secrets-operator/providers.tf
new file mode 100644
index 000000000..89ed50a98
--- /dev/null
+++ b/modules/eks/external-secrets-operator/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/external-secrets-operator/remote-state.tf b/modules/eks/external-secrets-operator/remote-state.tf
new file mode 100644
index 000000000..7863c9586
--- /dev/null
+++ b/modules/eks/external-secrets-operator/remote-state.tf
@@ -0,0 +1,20 @@
+module "eks" {
+ source = "cloudposse/stack-config/yaml//modules/remote-state"
+ version = "1.5.0"
+
+ component = var.eks_component_name
+
+ context = module.this.context
+}
+
+module "account_map" {
+ 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
+}
diff --git a/modules/eks/external-secrets-operator/variables.tf b/modules/eks/external-secrets-operator/variables.tf
new file mode 100644
index 000000000..48b22c69e
--- /dev/null
+++ b/modules/eks/external-secrets-operator/variables.tf
@@ -0,0 +1,42 @@
+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."
+}
+
+variable "kms_aliases_allow_decrypt" {
+ type = list(string)
+ description = "A list of KMS aliases that the SecretStore is allowed to decrypt."
+ default = []
+}
diff --git a/modules/eks/external-secrets-operator/versions.tf b/modules/eks/external-secrets-operator/versions.tf
new file mode 100644
index 000000000..46584b569
--- /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, != 2.21.0, != 2.21.0"
+ }
+ }
+}
diff --git a/modules/eks/github-actions-runner/CHANGELOG.md b/modules/eks/github-actions-runner/CHANGELOG.md
new file mode 100644
index 000000000..d086975da
--- /dev/null
+++ b/modules/eks/github-actions-runner/CHANGELOG.md
@@ -0,0 +1,167 @@
+## 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:
+
+- 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.
+
+```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`, `atomic`, 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..0c511f62d
--- /dev/null
+++ b/modules/eks/github-actions-runner/README.md
@@ -0,0 +1,473 @@
+---
+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)
+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
+
+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.
+
+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
+```
+
+### 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
+```
+
+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)
+- 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
+
+| 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 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 |
+| [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/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)
+- [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..91cc7f6d4
--- /dev/null
+++ b/modules/eks/github-actions-runner/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/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"
+ }
+ }
+}
diff --git a/modules/eks/idp-roles/README.md b/modules/eks/idp-roles/README.md
index d59e8f6bd..2f92cd320 100644
--- a/modules/eks/idp-roles/README.md
+++ b/modules/eks/idp-roles/README.md
@@ -1,6 +1,15 @@
+---
+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 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 several pre-determined
+permission levels for cluster users and come with bindings that make them easy to assign to Users and Groups.
## Usage
@@ -21,6 +30,7 @@ components:
kubeconfig_exec_auth_api_version: "client.authentication.k8s.io/v1beta1"
```
+
## Requirements
@@ -29,6 +39,7 @@ components:
| [terraform](#requirement\_terraform) | >= 1.0.0 |
| [aws](#requirement\_aws) | >= 4.0 |
| [helm](#requirement\_helm) | >= 2.0 |
+| [kubernetes](#requirement\_kubernetes) | >= 2.14.0, != 2.21.0 |
## Providers
@@ -40,18 +51,16 @@ 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.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.10.0 |
| [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 |
## Inputs
@@ -72,18 +81,17 @@ 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 |
| [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\_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) | Kubernetes namespace to install the release into | `string` | `"kube-system"` | no |
@@ -108,6 +116,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/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"
diff --git a/modules/eks/idp-roles/main.tf b/modules/eks/idp-roles/main.tf
index 7103ea5af..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.6.0"
+ version = "0.10.0"
# Required arguments
name = module.this.name
diff --git a/modules/eks/idp-roles/provider-helm.tf b/modules/eks/idp-roles/provider-helm.tf
index d04bccf3d..91cc7f6d4 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.
@@ -15,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" {
@@ -36,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" {
@@ -45,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/v1alpha1"
+ 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 = 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"
+ 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
@@ -95,14 +133,17 @@ 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 = 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" {
@@ -113,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)
- 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
+ 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 ? ["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"
@@ -132,21 +174,22 @@ 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
- # 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
+ 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 ? ["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/providers.tf b/modules/eks/idp-roles/providers.tf
index 2775903d2..89ed50a98 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 = assume_role.value
}
}
}
@@ -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/remote-state.tf b/modules/eks/idp-roles/remote-state.tf
index 89a89a442..c1ec8226d 100644
--- a/modules/eks/idp-roles/remote-state.tf
+++ b/modules/eks/idp-roles/remote-state.tf
@@ -1,9 +1,8 @@
module "eks" {
source = "cloudposse/stack-config/yaml//modules/remote-state"
- version = "1.1.0"
+ version = "1.5.0"
component = var.eks_component_name
context = module.this.context
}
-
diff --git a/modules/eks/idp-roles/versions.tf b/modules/eks/idp-roles/versions.tf
index cbf605948..ec64f8a4f 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, != 2.21.0"
+ }
}
}
diff --git a/modules/eks/karpenter-node-pool/CHANGELOG.md b/modules/eks/karpenter-node-pool/CHANGELOG.md
new file mode 100644
index 000000000..c8402e80e
--- /dev/null
+++ b/modules/eks/karpenter-node-pool/CHANGELOG.md
@@ -0,0 +1,17 @@
+## 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
+
+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
new file mode 100644
index 000000000..8bfefb308
--- /dev/null
+++ b/modules/eks/karpenter-node-pool/README.md
@@ -0,0 +1,240 @@
+---
+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.
+
+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, 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 |
+| [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..d43d8d2ac
--- /dev/null
+++ b/modules/eks/karpenter-node-pool/main.tf
@@ -0,0 +1,82 @@
+# 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 }
+ 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/
+
+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 = {
+ metadata = {
+ labels = coalesce(each.value.labels, {})
+ annotations = coalesce(each.value.annotations, {})
+ }
+ 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
+ },
+ try(local.kubelet_specs[each.key], null) == null ? {} : {
+ kubelet = local.kubelet_specs[each.key]
+ }
+ )
+ }
+ }
+ }
+
+ 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/efs/providers.tf b/modules/eks/karpenter-node-pool/providers.tf
similarity index 100%
rename from modules/eks/efs/providers.tf
rename to modules/eks/karpenter-node-pool/providers.tf
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..522e79e77
--- /dev/null
+++ b/modules/eks/karpenter-node-pool/variables.tf
@@ -0,0 +1,132 @@
+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, 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, {})
+ }))
+ 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
new file mode 100644
index 000000000..55e5d90f3
--- /dev/null
+++ b/modules/eks/karpenter/CHANGELOG.md
@@ -0,0 +1,144 @@
+## Release 1.470.0
+
+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](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.
+
+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](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.
+
+## 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
+```
+
+> [!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`.
+
+#### 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 2cd9c3c91..4234e3cff 100644
--- a/modules/eks/karpenter/README.md
+++ b/modules/eks/karpenter/README.md
@@ -1,40 +1,42 @@
+---
+tags:
+ - component/eks/karpenter
+ - layer/eks
+ - provider/aws
+ - provider/helm
+---
+
# Component: `eks/karpenter`
-This component provisions [Karpenter](https://karpenter.sh) on an EKS cluster.
+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
**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:
type: abstract
- settings:
- spacelift:
- workspace_enabled: true
vars:
enabled: true
- root_account_tenant_name: core
- 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"
- 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"
@@ -46,39 +48,45 @@ components:
atomic: true
wait: true
rbac_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
+ # "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"
+ # 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
-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:
@@ -86,15 +94,23 @@ 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
+
+- 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
+### 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.
-EKS Fargate Profile for Karpenter and IAM Role for Nodes launched by Karpenter are provisioned by the `eks/cluster` component:
+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,50 +121,35 @@ 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__:
- - 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)
+> [!NOTE]
+>
+> 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.
+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.
-```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.
-```
+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)
+See
+[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).
+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
@@ -157,17 +158,29 @@ 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 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)
+- 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)
+
+Create a stack config for the blue Karpenter component 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
+```
Run the following commands to provision the Karpenter component on the blue EKS cluster:
@@ -176,126 +189,162 @@ 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`.
+### 3. Provision `karpenter-node-pool` component
+
+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.
+
+> [!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:
```yaml
- eks/karpenter-blue:
+components:
+ terraform:
+ eks/karpenter-node-pool:
metadata:
- component: eks/karpenter
- inherits:
- - eks/karpenter
+ type: abstract
vars:
- eks_component_name: eks/cluster-blue
+ 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"]
```
-### 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.
+Now, create the stack config for the blue Karpenter NodePool component in `stacks/catalog/eks/clusters/blue.yaml`:
-__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.
+```yaml
+eks/karpenter-node-pool/blue:
+ metadata:
+ component: eks/karpenter-node-pool
+ inherits:
+ - eks/karpenter-node-pool
+ vars:
+ eks_component_name: eks/cluster-blue
+```
-Run the following commands to deploy the Karpenter provisioners on the blue EKS cluster:
+Finally, run the following commands to deploy the Karpenter NodePools 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
+atmos terraform plan eks/karpenter-node-pool/blue -s plat-ue2-dev
+atmos terraform apply eks/karpenter-node-pool/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`.
+## Node Interruption
-```yaml
- eks/karpenter-provisioner-blue:
- metadata:
- component: eks/karpenter-provisioner
- inherits:
- - eks/karpenter-provisioner
- vars:
- attributes:
- - blue
- eks_component_name: eks/cluster-blue
-```
+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:
-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"
- ```
+- Spot Interruption Warnings
+- Scheduled Change Health Events (Maintenance Events)
+- Instance Terminating Events
+- Instance Stopping Events
- - Total CPU and memory limits for all pods running on the EC2 instances launched by Karpenter:
+> [!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.
- ```yaml
- total_cpu_limit: "1k"
- total_memory_limit: "1000Gi"
- ```
+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)
- - 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):
+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.
- ```yaml
- ttl_seconds_after_empty: 30
- ```
+## Custom Resource Definition (CRD) Management
- - 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):
+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.
- ```yaml
- ttl_seconds_until_expired: 2592000
- ```
+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.)
-For more details, refer to:
+## Troubleshooting
- - https://karpenter.sh/v0.18.0/provisioner/#specrequirements
- - https://karpenter.sh/v0.18.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
+For Karpenter issues, checkout the [Karpenter Troubleshooting Guide](https://karpenter.sh/docs/troubleshooting/)
+### References
+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.36/getting-started/getting-started-with-karpenter/
+- https://aws.github.io/aws-eks-best-practices/karpenter
+
+
## Requirements
| 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 |
+| [kubernetes](#requirement\_kubernetes) | >= 2.7.1, != 2.21.0 |
## Providers
@@ -307,17 +356,25 @@ 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.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.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_iam_instance_profile.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource |
+| [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 |
+| [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
@@ -333,37 +390,41 @@ 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 |
-| [create\_namespace](#input\_create\_namespace) | Create the namespace if it does not yet exist. Defaults to `false` | `bool` | `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 |
| [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` | `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 |
+| [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 |
| [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\_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 |
+| [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 |
@@ -374,28 +435,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..d2c5f6b29
--- /dev/null
+++ b/modules/eks/karpenter/controller-policy-v1alpha.tf
@@ -0,0 +1,89 @@
+#####
+# 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.
+#
+# 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 {
+ 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": {
+ "ec2:ResourceTag/karpenter.k8s.aws/cluster": "${local.eks_cluster_id}"
+ },
+ "StringLike": {
+ "ec2: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
+}
+
+# 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/controller-policy.tf b/modules/eks/karpenter/controller-policy.tf
new file mode 100644
index 000000000..f2b4924f2
--- /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 needing 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
new file mode 100644
index 000000000..56a3334f7
--- /dev/null
+++ b/modules/eks/karpenter/interruption_handler.tf
@@ -0,0 +1,99 @@
+# 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)
+
+ 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"]
+ }
+ }
+ }
+}
+
+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/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
diff --git a/modules/eks/karpenter/main.tf b/modules/eks/karpenter/main.tf
index f7f47e844..3d930b117 100644
--- a/modules/eks/karpenter/main.tf
+++ b/modules/eks/karpenter/main.tf
@@ -1,33 +1,60 @@
# 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_role_enabled = local.enabled && length(local.karpenter_iam_role_name) > 0
+ # 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")
+
+ # 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
+
+ karpenter_node_role_arn = module.eks.outputs.karpenter_iam_role_arn
+
+ # 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"
}
-resource "aws_iam_instance_profile" "default" {
- count = local.karpenter_role_enabled ? 1 : 0
+data "aws_partition" "current" {
+ count = local.enabled ? 1 : 0
+}
+
+
+# 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 by EKS/Kubernetes by default
+ kubernetes_namespace = local.kubernetes_namespace
- name = local.karpenter_iam_role_name
- role = local.karpenter_iam_role_name
- tags = module.this.tags
+ eks_cluster_oidc_issuer_url = coalesce(replace(local.eks_cluster_identity_oidc_issuer, "https://", ""), "deleted")
+
+ context = module.this.context
}
# Deploy Karpenter helm chart
module "karpenter" {
source = "cloudposse/helm-release/aws"
- version = "0.7.0"
+ version = "0.10.1"
chart = var.chart
repository = var.chart_repository
@@ -38,88 +65,63 @@ 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 = 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 = var.kubernetes_namespace
-
- iam_role_enabled = local.karpenter_role_enabled
-
- # 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 = [
- {
- 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/*"]
- }
- ]
+ 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_json]
values = compact([
- # standard k8s object settings
yamlencode({
fullnameOverride = module.this.name
serviceAccount = {
name = module.this.name
}
- resources = var.resources
- rbac = {
- create = var.rbac_enabled
+ controller = {
+ resources = var.resources
}
+ replicas = var.replicas
}),
- # karpenter-specific values
+ # karpenter-specific values
yamlencode({
- aws = {
- defaultInstanceProfile = one(aws_iam_instance_profile.default[*].name)
+ logConfig = {
+ enabled = var.logging.enabled
+ logLevel = {
+ controller = var.logging.level.controller
+ global = var.logging.level.global
+ webhook = var.logging.level.webhook
+ }
}
- clusterName = local.eks_cluster_id
- clusterEndpoint = local.eks_cluster_endpoint
- }),
+ 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)
])
context = module.this.context
- depends_on = [aws_iam_instance_profile.default]
+ depends_on = [
+ module.karpenter_crd,
+ 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 9bb5edb6f..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 = 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"
+ 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
@@ -101,16 +133,17 @@ 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
] : []
# 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/providers.tf b/modules/eks/karpenter/providers.tf
index c2419aabb..89ed50a98 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 = assume_role.value
}
}
}
@@ -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/remote-state.tf b/modules/eks/karpenter/remote-state.tf
index 90c6ab1a8..723da0a44 100644
--- a/modules/eks/karpenter/remote-state.tf
+++ b/modules/eks/karpenter/remote-state.tf
@@ -1,8 +1,16 @@
module "eks" {
source = "cloudposse/stack-config/yaml//modules/remote-state"
- version = "1.3.1"
+ version = "1.5.0"
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 6aaa6b4fb..0c1117fa0 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({
@@ -39,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"
@@ -91,3 +92,54 @@ variable "eks_component_name" {
description = "The name of the eks component"
default = "eks/cluster"
}
+
+variable "interruption_handler_enabled" {
+ type = bool
+ default = true
+ description = <
+
+## 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.10.0 |
+| [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 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 |
+| [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) | 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 |
+| [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. |
+| [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
+
+- [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/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..fad7fe50d
--- /dev/null
+++ b/modules/eks/keda/main.tf
@@ -0,0 +1,48 @@
+module "keda" {
+ source = "cloudposse/helm-release/aws"
+ version = "0.10.0"
+
+ 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 = true
+
+ iam_policy_statements = [
+ {
+ sid = "KedaOperatorSQS"
+ effect = "Allow"
+ actions = ["SQS:GetQueueAttributes"]
+ resources = ["*"]
+ }
+ ]
+
+ values = compact([
+ yamlencode({
+ serviceAccount = {
+ name = module.this.name
+ }
+ rbac = {
+ create = var.rbac_enabled
+ }
+ }),
+ var.resources != null ? yamlencode({ resources = var.resources }) : "",
+ ])
+
+ context = module.this.context
+}
diff --git a/modules/eks/keda/outputs.tf b/modules/eks/keda/outputs.tf
new file mode 100644
index 000000000..cab379b79
--- /dev/null
+++ b/modules/eks/keda/outputs.tf
@@ -0,0 +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" {
+ description = "Block status of the deployed release."
+ value = module.keda.metadata
+}
diff --git a/modules/eks/keda/provider-helm.tf b/modules/eks/keda/provider-helm.tf
new file mode 100644
index 000000000..91cc7f6d4
--- /dev/null
+++ b/modules/eks/keda/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/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..d461f86d4
--- /dev/null
+++ b/modules/eks/keda/variables.tf
@@ -0,0 +1,81 @@
+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 = 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" {
+ 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"
+ }
+ }
+}
diff --git a/modules/eks/loki/README.md b/modules/eks/loki/README.md
new file mode 100644
index 000000000..3b96994cf
--- /dev/null
+++ b/modules/eks/loki/README.md
@@ -0,0 +1,150 @@
+---
+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
+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..e7d78f2d9
--- /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 = 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/metrics-server/README.md b/modules/eks/metrics-server/README.md
index 3ecaf303d..743edc51e 100644
--- a/modules/eks/metrics-server/README.md
+++ b/modules/eks/metrics-server/README.md
@@ -1,6 +1,15 @@
-# Component: `metrics-server`
+---
+tags:
+ - component/eks/metrics-server
+ - layer/eks
+ - provider/aws
+ - provider/helm
+---
-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.
+# 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.
## Usage
@@ -37,36 +46,36 @@ components:
chart_values: {}
```
+
## Requirements
| 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, != 2.21.0 |
## Providers
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | ~> 4.0 |
-| [kubernetes](#provider\_kubernetes) | n/a |
+| [aws](#provider\_aws) | >= 4.9.0 |
## Modules
| 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.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.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
@@ -76,34 +85,33 @@ 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 `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 |
| [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 |
| [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\_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 |
+| [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 |
@@ -113,7 +121,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 |
@@ -126,6 +134,7 @@ components:
|------|-------------|
| [metadata](#output\_metadata) | Block status of the deployed release |
+
## References
diff --git a/modules/eks/metrics-server/default.auto.tfvars b/modules/eks/metrics-server/default.auto.tfvars
deleted file mode 100644
index 2e2708d6d..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 = "5.11.4"
-
-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/main.tf b/modules/eks/metrics-server/main.tf
index 620599d70..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.5.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/provider-helm.tf b/modules/eks/metrics-server/provider-helm.tf
index 20e4d3837..91cc7f6d4 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.
@@ -15,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" {
@@ -36,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" {
@@ -45,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 = 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"
+ 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
@@ -95,14 +133,17 @@ 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 = 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" {
@@ -113,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)
- 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
+ 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 ? ["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"
@@ -132,21 +174,22 @@ 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
- # 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
+ 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 ? ["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/providers.tf b/modules/eks/metrics-server/providers.tf
index 74ff8e62c..89ed50a98 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 = assume_role.value
}
}
}
@@ -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/metrics-server/remote-state.tf b/modules/eks/metrics-server/remote-state.tf
index 6ef90fd26..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 = "0.22.4"
+ version = "1.5.0"
component = var.eks_component_name
diff --git a/modules/eks/metrics-server/variables.tf b/modules/eks/metrics-server/variables.tf
index 29173515e..eb563b1ee 100644
--- a/modules/eks/metrics-server/variables.tf
+++ b/modules/eks/metrics-server/variables.tf
@@ -12,17 +12,20 @@ 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."
+ # TODO: Chart should default to https://kubernetes-sigs.github.io/metrics-server/
+ 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 +40,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
+ description = "Create the namespace if it does not yet exist. Defaults to `true`."
+ 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/metrics-server/versions.tf b/modules/eks/metrics-server/versions.tf
index 58318d20e..9f0f54df7 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, != 2.21.0"
+ }
}
}
diff --git a/modules/eks/prometheus-scraper/README.md b/modules/eks/prometheus-scraper/README.md
new file mode 100644
index 000000000..fc6754aa8
--- /dev/null
+++ b/modules/eks/prometheus-scraper/README.md
@@ -0,0 +1,166 @@
+---
+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
+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..ecefac8bd
--- /dev/null
+++ b/modules/eks/promtail/README.md
@@ -0,0 +1,131 @@
+---
+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.
+
+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/eks/redis-operator/README.md b/modules/eks/redis-operator/README.md
index 28768a7f5..0504d982b 100644
--- a/modules/eks/redis-operator/README.md
+++ b/modules/eks/redis-operator/README.md
@@ -1,6 +1,16 @@
+---
+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 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 +56,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,31 +72,33 @@ components:
inherits:
- eks/redis-operator/defaults
vars: {}
-
```
+
+
## Requirements
| 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, != 2.21.0 |
## Providers
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | ~> 4.0 |
-| [kubernetes](#provider\_kubernetes) | n/a |
+| [aws](#provider\_aws) | >= 4.0 |
+| [kubernetes](#provider\_kubernetes) | >= 2.0, != 2.21.0 |
## Modules
| 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.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.10.0 |
| [this](#module\_this) | cloudposse/label/null | 0.25.0 |
## Resources
@@ -117,17 +128,16 @@ 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 |
| [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\_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 |
@@ -154,7 +164,9 @@ components:
|------|-------------|
| [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/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-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-operator/main.tf b/modules/eks/redis-operator/main.tf
index a91e0e8f8..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.5.0"
+ version = "0.10.0"
chart = var.chart
repository = var.chart_repository
diff --git a/modules/eks/redis-operator/provider-helm.tf b/modules/eks/redis-operator/provider-helm.tf
index 20e4d3837..91cc7f6d4 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.
@@ -15,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" {
@@ -36,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" {
@@ -45,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 = 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"
+ 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
@@ -95,14 +133,17 @@ 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 = 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" {
@@ -113,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)
- 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
+ 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 ? ["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"
@@ -132,21 +174,22 @@ 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
- # 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
+ 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 ? ["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/providers.tf b/modules/eks/redis-operator/providers.tf
index 74ff8e62c..89ed50a98 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 = assume_role.value
}
}
}
@@ -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-operator/remote-state.tf b/modules/eks/redis-operator/remote-state.tf
index 6ef90fd26..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 = "0.22.4"
+ version = "1.5.0"
component = var.eks_component_name
diff --git a/modules/eks/redis-operator/versions.tf b/modules/eks/redis-operator/versions.tf
index 58318d20e..14c085342 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, != 2.21.0"
+ }
}
}
diff --git a/modules/eks/redis/README.md b/modules/eks/redis/README.md
index 7435b4d93..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.
@@ -8,7 +16,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 +58,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,32 +74,33 @@ components:
inherits:
- eks/redis/defaults
vars: {}
-
```
+
## Requirements
| 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, != 2.21.0 |
## Providers
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | ~> 4.0 |
-| [kubernetes](#provider\_kubernetes) | n/a |
+| [aws](#provider\_aws) | >= 4.0 |
+| [kubernetes](#provider\_kubernetes) | >= 2.0, != 2.21.0 |
## Modules
| 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.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.10.0 |
| [this](#module\_this) | cloudposse/label/null | 0.25.0 |
## Resources
@@ -123,17 +130,16 @@ 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 |
| [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\_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 |
@@ -160,7 +166,9 @@ components:
|------|-------------|
| [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/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/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/eks/redis/main.tf b/modules/eks/redis/main.tf
index f612b45a8..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.5.0"
+ version = "0.10.0"
chart = var.chart
repository = var.chart_repository
diff --git a/modules/eks/redis/provider-helm.tf b/modules/eks/redis/provider-helm.tf
index 20e4d3837..91cc7f6d4 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.
@@ -15,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" {
@@ -36,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" {
@@ -45,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 = 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"
+ 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
@@ -95,14 +133,17 @@ 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 = 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" {
@@ -113,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)
- 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
+ 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 ? ["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"
@@ -132,21 +174,22 @@ 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
- # 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
+ 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 ? ["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/providers.tf b/modules/eks/redis/providers.tf
index 74ff8e62c..89ed50a98 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 = assume_role.value
}
}
}
@@ -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/remote-state.tf b/modules/eks/redis/remote-state.tf
index 6ef90fd26..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 = "0.22.4"
+ version = "1.5.0"
component = var.eks_component_name
diff --git a/modules/eks/redis/versions.tf b/modules/eks/redis/versions.tf
index 58318d20e..14c085342 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, != 2.21.0"
+ }
}
}
diff --git a/modules/eks/reloader/README.md b/modules/eks/reloader/README.md
index f921e50af..9e720e55f 100644
--- a/modules/eks/reloader/README.md
+++ b/modules/eks/reloader/README.md
@@ -1,8 +1,15 @@
+---
+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 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 +36,7 @@ components:
timeout: 180
```
+
## Requirements
@@ -37,7 +45,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,13 +53,13 @@ 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
| 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.5.0 |
| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a |
| [this](#module\_this) | cloudposse/label/null | 0.25.0 |
@@ -81,17 +89,16 @@ 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 |
| [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\_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 |
@@ -118,7 +125,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/reloader/provider-helm.tf b/modules/eks/reloader/provider-helm.tf
index 20e4d3837..91cc7f6d4 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.
@@ -15,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" {
@@ -36,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" {
@@ -45,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 = 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"
+ 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
@@ -95,14 +133,17 @@ 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 = 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" {
@@ -113,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)
- 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
+ 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 ? ["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"
@@ -132,21 +174,22 @@ 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
- # 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
+ 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 ? ["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/providers.tf b/modules/eks/reloader/providers.tf
index c2419aabb..89ed50a98 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 = assume_role.value
}
}
}
@@ -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/reloader/remote-state.tf b/modules/eks/reloader/remote-state.tf
index 90c6ab1a8..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.3.1"
+ version = "1.5.0"
component = var.eks_component_name
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"
}
}
}
diff --git a/modules/eks/storage-class/README.md b/modules/eks/storage-class/README.md
new file mode 100644
index 000000000..a9c64d06e
--- /dev/null
+++ b/modules/eks/storage-class/README.md
@@ -0,0 +1,216 @@
+---
+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
+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 `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.
+
+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:
+
+```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
+```
+
+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
+# 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"}}}'
+```
+
+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
+# 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
+```
+
+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
+ efs_component_name: "efs" # Replace with the name of the EFS component, previously "eks/efs"
+```
+
+
+
+## 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 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 |
+| [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..91cc7f6d4
--- /dev/null
+++ b/modules/eks/storage-class/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/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/karpenter-provisioner/versions.tf b/modules/eks/storage-class/versions.tf
similarity index 91%
rename from modules/eks/karpenter-provisioner/versions.tf
rename to modules/eks/storage-class/versions.tf
index 57cc9f927..fba2b45f9 100644
--- a/modules/eks/karpenter-provisioner/versions.tf
+++ b/modules/eks/storage-class/versions.tf
@@ -6,13 +6,13 @@ terraform {
source = "hashicorp/aws"
version = ">= 4.9.0"
}
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = ">= 2.22.0"
+ }
helm = {
source = "hashicorp/helm"
version = ">= 2.0"
}
- kubernetes = {
- source = "hashicorp/kubernetes"
- version = ">= 2.14.0"
- }
}
}
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/datadog-agent/provider-helm.tf b/modules/eks/tailscale/provider-kubernetes.tf
similarity index 83%
rename from modules/datadog-agent/provider-helm.tf
rename to modules/eks/tailscale/provider-kubernetes.tf
index 20e4d3837..00cfd1542 100644
--- a/modules/datadog-agent/provider-helm.tf
+++ b/modules/eks/tailscale/provider-kubernetes.tf
@@ -110,32 +110,6 @@ data "aws_eks_cluster_auth" "eks" {
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)
diff --git a/modules/eks/karpenter-provisioner/providers.tf b/modules/eks/tailscale/providers.tf
similarity index 100%
rename from modules/eks/karpenter-provisioner/providers.tf
rename to modules/eks/tailscale/providers.tf
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"
+ }
+ }
+}
diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md
index d5578e4c8..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.
@@ -18,8 +25,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,13 +68,14 @@ components:
value: lK
```
+
## Requirements
| Name | Version |
|------|---------|
| [terraform](#requirement\_terraform) | >= 1.0.0 |
-| [aws](#requirement\_aws) | ~> 4.0 |
+| [aws](#requirement\_aws) | >= 4.0 |
## Providers
@@ -77,13 +85,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.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 | 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.5.0 |
+| [vpc\_ingress](#module\_vpc\_ingress) | cloudposse/stack-config/yaml//modules/remote-state | 1.5.0 |
## Resources
@@ -95,6 +103,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 |
@@ -112,13 +121,12 @@ 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 |
| [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 |
@@ -136,9 +144,11 @@ No resources.
|------|-------------|
| [redis\_clusters](#output\_redis\_clusters) | Redis cluster objects |
+
## 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/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/elasticache-redis/main.tf b/modules/elasticache-redis/main.tf
index 2f4003da5..0f2f91638 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) :
@@ -38,6 +36,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
@@ -50,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
}
@@ -65,13 +65,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 a82d1288b..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 = "0.44.0"
+ version = "1.4.1"
name = var.cluster_name
@@ -20,8 +20,10 @@ 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
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 +32,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..1c9af10cd 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"
@@ -49,10 +55,12 @@ 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
automatic_failover_enabled = bool
+ auto_minor_version_upgrade = bool
auth_token_enabled = bool
})
description = "Cluster attributes"
@@ -66,6 +74,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/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/elasticache-redis/providers.tf b/modules/elasticache-redis/providers.tf
index efa9ede5d..ef923e10a 100644
--- a/modules/elasticache-redis/providers.tf
+++ b/modules/elasticache-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 = assume_role.value
}
}
}
@@ -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/elasticache-redis/remote-state.tf b/modules/elasticache-redis/remote-state.tf
index 5b320c9a9..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 = "0.22.3"
+ version = "1.5.0"
component = "vpc"
@@ -9,7 +9,7 @@ module "vpc" {
module "eks" {
source = "cloudposse/stack-config/yaml//modules/remote-state"
- version = "0.22.3"
+ 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 = "0.22.3"
+ 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 = "0.22.3"
+ version = "1.5.0"
for_each = toset(var.allow_ingress_from_vpc_stages)
diff --git a/modules/elasticache-redis/variables.tf b/modules/elasticache-redis/variables.tf
index 3b25582c5..b059c6c36 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"
@@ -59,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"
@@ -69,6 +81,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 = []
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/elasticsearch/README.md b/modules/elasticsearch/README.md
index b82625c22..710e244eb 100644
--- a/modules/elasticsearch/README.md
+++ b/modules/elasticsearch/README.md
@@ -1,6 +1,14 @@
+---
+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 Logstash.
+This component is responsible for provisioning an Elasticsearch cluster with built-in integrations with Kibana and
+Logstash.
## Usage
@@ -11,12 +19,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"
@@ -26,31 +35,33 @@ components:
domain_hostname_enabled: true
```
+
## Requirements
| 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 |
-| [random](#provider\_random) | n/a |
+| [aws](#provider\_aws) | >= 4.9.0 |
+| [random](#provider\_random) | >= 3.0 |
## Modules
| Name | Source | Version |
|------|--------|---------|
-| [dns\_delegated](#module\_dns\_delegated) | cloudposse/stack-config/yaml//modules/remote-state | 0.17.0 |
-| [elasticsearch](#module\_elasticsearch) | cloudposse/elasticsearch/aws | 0.33.0 |
-| [elasticsearch\_log\_cleanup](#module\_elasticsearch\_log\_cleanup) | cloudposse/lambda-elasticsearch-cleanup/aws | 0.12.3 |
+| [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.24.1 |
-| [vpc](#module\_vpc) | cloudposse/stack-config/yaml//modules/remote-state | 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 |
## Resources
@@ -65,12 +76,16 @@ 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\_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 |
-| [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 |
+| [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 |
| [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 |
@@ -80,21 +95,22 @@ 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 |
| [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
@@ -111,8 +127,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/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/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..25f8d5756 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
@@ -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
@@ -99,7 +101,7 @@ resource "aws_ssm_parameter" "elasticsearch_kibana_endpoint" {
module "elasticsearch_log_cleanup" {
source = "cloudposse/lambda-elasticsearch-cleanup/aws"
- version = "0.12.3"
+ version = "0.14.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..ef923e10a 100644
--- a/modules/elasticsearch/providers.tf
+++ b/modules/elasticsearch/providers.tf
@@ -1,17 +1,19 @@
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 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
}
-
-variable "import_profile_name" {
- type = string
- default = null
- description = "IAM Profile to use when importing a resource"
-}
diff --git a/modules/elasticsearch/remote-state.tf b/modules/elasticsearch/remote-state.tf
index bd8a75ff2..950d6d996 100644
--- a/modules/elasticsearch/remote-state.tf
+++ b/modules/elasticsearch/remote-state.tf
@@ -1,21 +1,18 @@
module "vpc" {
source = "cloudposse/stack-config/yaml//modules/remote-state"
- version = "0.17.0"
+ version = "1.5.0"
- 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 = "0.17.0"
+ version = "1.5.0"
- stack_config_local_path = "../../../stacks"
- component = "dns-delegated"
+ component = "dns-delegated"
+ environment = var.dns_delegated_environment_name
context = module.this.context
- enabled = true
}
diff --git a/modules/elasticsearch/variables.tf b/modules/elasticsearch/variables.tf
index a5328ce3b..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`)"
@@ -87,3 +99,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"
+}
diff --git a/modules/elasticsearch/versions.tf b/modules/elasticsearch/versions.tf
index 207f9f727..4a6389362 100644
--- a/modules/elasticsearch/versions.tf
+++ b/modules/elasticsearch/versions.tf
@@ -1,10 +1,14 @@
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"
+ version = ">= 3.0"
}
}
}
diff --git a/modules/eventbridge/README.md b/modules/eventbridge/README.md
new file mode 100644
index 000000000..a406e4d7e
--- /dev/null
+++ b/modules/eventbridge/README.md
@@ -0,0 +1,119 @@
+---
+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
+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"
+ }
+ }
+}
diff --git a/modules/github-action-token-rotator/README.md b/modules/github-action-token-rotator/README.md
index 8a4091e64..dd566a83c 100644
--- a/modules/github-action-token-rotator/README.md
+++ b/modules/github-action-token-rotator/README.md
@@ -1,6 +1,14 @@
+---
+tags:
+ - component/github-action-token-rotator
+ - layer/github
+ - provider/aws
+---
+
# 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 +16,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,15 +34,18 @@ 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
| Name | Version |
|------|---------|
| [terraform](#requirement\_terraform) | >= 1.0.0 |
-| [aws](#requirement\_aws) | ~> 4.0 |
+| [aws](#requirement\_aws) | >= 4.0 |
## Providers
@@ -66,8 +78,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 |
@@ -88,9 +98,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/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-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-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-action-token-rotator/providers.tf b/modules/github-action-token-rotator/providers.tf
index 08ee01b2a..ef923e10a 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 = assume_role.value
}
}
}
@@ -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/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/github-oidc-provider/README.md b/modules/github-oidc-provider/README.md
index 734fbbcfe..e2d38fa7a 100644
--- a/modules/github-oidc-provider/README.md
+++ b/modules/github-oidc-provider/README.md
@@ -1,18 +1,25 @@
+---
+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 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,13 +33,39 @@ 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
-[scripts/get_github_oidc_thumbprint.sh](./scripts/get_github_oidc_thumbprint.sh)
+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`.
+
+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
+
+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
@@ -72,7 +105,6 @@ to get the new thumbprint and add it to the list in `var.thumbprint_list`.
| [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 |
@@ -82,9 +114,10 @@ to get the new thumbprint and add it to the list in `var.thumbprint_list`.
| [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"
]
| no |
+| [thumbprint\_list](#input\_thumbprint\_list) | List of OIDC provider certificate thumbprints | `list(string)` | [
"6938fd4d98bab03faadb97b34396831e3780aea1",
"1c58a3a8518e8759bf075b76b750d4f2df264fcd"
]
| no |
## Outputs
@@ -92,10 +125,11 @@ to get the new thumbprint and add it to the list in `var.thumbprint_list`.
|------|-------------|
| [oidc\_provider\_arn](#output\_oidc\_provider\_arn) | GitHub OIDC provider ARN |
-
+
## 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-provider/providers.tf b/modules/github-oidc-provider/providers.tf
index f1096ef8d..13a1a5b6a 100644
--- a/modules/github-oidc-provider/providers.tf
+++ b/modules/github-oidc-provider/providers.tf
@@ -1,26 +1,50 @@
+# 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
- # 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 = !var.superadmin && module.iam_roles.profiles_enabled ? module.iam_roles.terraform_profile_name : null
dynamic "assume_role" {
- for_each = var.import_role_arn == null ? (module.iam_roles.org_role_arn != null ? [true] : []) : ["import"]
+ 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 = coalesce(var.import_role_arn, module.iam_roles.org_role_arn)
+ role_arn = assume_role.value
}
}
}
+
module "iam_roles" {
source = "../account-map/modules/iam-roles"
- privileged = true
- context = module.this.context
+ privileged = var.superadmin
+
+ context = module.this.context
}
-variable "import_role_arn" {
- type = string
- default = null
- description = "IAM Role ARN to use when importing a resource"
+variable "superadmin" {
+ type = bool
+ default = false
+ description = "Set `true` if running as the SuperAdmin user"
}
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..5667f8f13 100755
--- a/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh
+++ b/modules/github-oidc-provider/scripts/get_github_oidc_thumbprint.sh
@@ -2,8 +2,15 @@
########################################################################################################################
# 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.
+#
+# 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"]
}
diff --git a/modules/github-oidc-role/README.md b/modules/github-oidc-role/README.md
new file mode 100644
index 000000000..e4bce3939
--- /dev/null
+++ b/modules/github-oidc-role/README.md
@@ -0,0 +1,257 @@
+---
+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.
+
+## 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/main/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 = < [!TIP]
+>
+> 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
@@ -13,16 +27,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"
@@ -60,13 +74,233 @@ 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).
+
+### 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
+
+
+
+### Creating Registration Token
+
+> [!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
+
+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.
+
+> [!TIP]
+>
+> 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)
+
+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
+```
+
+# 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 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
+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
@@ -87,13 +321,13 @@ chamber write github/runners/