`,
+# 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/spacelift-worker-pool/iam.tf b/modules/eks/spacelift-worker-pool/iam.tf
new file mode 100644
index 000000000..5498d3a54
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/iam.tf
@@ -0,0 +1,71 @@
+locals {
+ identity_account_name = module.account_map.outputs.identity_account_account_name
+ ecr_repo_arn = module.ecr.outputs.ecr_repo_arn_map[var.ecr_repo_name]
+ role_arn_template = module.account_map.outputs.iam_role_arn_templates[local.identity_account_name]
+}
+
+data "aws_partition" "current" {}
+
+module "eks_iam_policy" {
+ source = "cloudposse/iam-policy/aws"
+ version = "2.0.1"
+
+ enabled = local.kubernetes_service_account_enabled
+
+ iam_source_policy_documents = var.iam_source_policy_documents
+ iam_override_policy_documents = var.iam_override_policy_documents
+ iam_source_json_url = var.iam_source_json_url
+
+ iam_policy_enabled = true
+
+ iam_policy = [{
+ statements = [
+ {
+ sid = "AssumeSpaceliftRole"
+ effect = "Allow"
+ actions = [
+ "sts:AssumeRole",
+ "sts:TagSession",
+ ]
+ resources = formatlist(local.role_arn_template, ["spacelift"])
+ },
+ {
+ sid = "ECRGetAuthorizationToken"
+ effect = "Allow"
+ actions = ["ecr:GetAuthorizationToken"]
+ resources = ["*"]
+ },
+ {
+ sid = "ECRRepoPermissions"
+ effect = "Allow"
+ actions = [
+ "ecr:GetDownloadUrlForLayer",
+ "ecr:BatchGetImage"
+ ]
+ resources = [local.ecr_repo_arn]
+ }
+ ]
+ }]
+
+ attributes = var.iam_attributes
+ context = module.this.context
+}
+
+module "eks_iam_role" {
+ source = "cloudposse/eks-iam-role/aws"
+ version = "2.1.1"
+
+ enabled = local.kubernetes_service_account_enabled
+
+ aws_iam_policy_document = [module.eks_iam_policy.json]
+ aws_partition = data.aws_partition.current.partition
+ eks_cluster_oidc_issuer_url = local.eks_cluster_identity_oidc_issuer
+ service_account_name = var.kubernetes_service_account_name
+ service_account_namespace = var.kubernetes_namespace
+ permissions_boundary = var.iam_permissions_boundary
+
+ attributes = var.iam_attributes
+ context = module.this.context
+
+ depends_on = [module.eks_iam_policy]
+}
diff --git a/modules/eks/spacelift-worker-pool/k8s.tf b/modules/eks/spacelift-worker-pool/k8s.tf
new file mode 100644
index 000000000..c5bcc96d5
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/k8s.tf
@@ -0,0 +1,215 @@
+locals {
+ kubernetes_labels = { for k, v in merge(module.this.tags, { name = var.kubernetes_namespace }) : k => replace(v, "/", "_") if local.enabled }
+ kubernetes_role_name = format("%s-service-account", var.kubernetes_service_account_name)
+ kubernetes_service_account_enabled = local.enabled && var.kubernetes_service_account_enabled
+}
+
+# Create Kubernetes secret for the workers running on Kubernetes to connect to Spacelift servers
+resource "kubernetes_secret" "default" {
+ count = local.enabled ? 1 : 0
+
+ metadata {
+ name = module.this.name
+ namespace = var.kubernetes_namespace
+ labels = local.kubernetes_labels
+ }
+
+ data = {
+ token = one(spacelift_worker_pool.default[*].config)
+ privateKey = one(spacelift_worker_pool.default[*].private_key)
+ }
+}
+
+# https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account_v1
+resource "kubernetes_service_account_v1" "default" {
+ count = local.kubernetes_service_account_enabled ? 1 : 0
+
+ metadata {
+ name = var.kubernetes_service_account_name
+ namespace = var.kubernetes_namespace
+ labels = local.kubernetes_labels
+
+ annotations = {
+ "eks.amazonaws.com/role-arn" = module.eks_iam_role.service_account_role_arn
+ }
+ }
+}
+
+# Before using the service account with a Pod, the service account must be bound to an existing Kubernetes Role,
+# or ClusterRole that includes the Kubernetes permissions that you require for the service account.
+# https://docs.aws.amazon.com/eks/latest/userguide/associate-service-account-role.html
+# https://docs.aws.amazon.com/eks/latest/userguide/pod-configuration.html
+# https://kubernetes.io/docs/reference/access-authn-authz/rbac/
+
+# https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_v1
+resource "kubernetes_role_v1" "default" {
+ count = local.kubernetes_service_account_enabled ? 1 : 0
+
+ metadata {
+ name = local.kubernetes_role_name
+ namespace = var.kubernetes_namespace
+ labels = local.kubernetes_labels
+ }
+
+ rule {
+ api_groups = var.kubernetes_role_api_groups
+ resources = var.kubernetes_role_resources
+ resource_names = var.kubernetes_role_resource_names
+ verbs = var.kubernetes_role_verbs
+ }
+}
+
+# https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding_v1
+resource "kubernetes_role_binding_v1" "default" {
+ count = local.kubernetes_service_account_enabled ? 1 : 0
+
+ metadata {
+ name = local.kubernetes_role_name
+ namespace = var.kubernetes_namespace
+ labels = local.kubernetes_labels
+ }
+
+ role_ref {
+ api_group = "rbac.authorization.k8s.io"
+ kind = "Role"
+ name = try(kubernetes_role_v1.default[0].metadata[0].name, "")
+ }
+
+ subject {
+ api_group = null
+ kind = "ServiceAccount"
+ name = try(kubernetes_service_account_v1.default[0].metadata[0].name, "")
+ namespace = var.kubernetes_namespace
+ }
+}
+
+# Create worker pools in Kubernetes
+# https://docs.spacelift.io/concepts/worker-pools#configuration
+resource "kubernetes_manifest" "spacelift_worker_pool" {
+ count = local.enabled ? 1 : 0
+
+ field_manager {
+ name = "Terraform"
+ force_conflicts = true
+ }
+
+ manifest = {
+ apiVersion = "workers.spacelift.io/v1beta1"
+ kind = "WorkerPool"
+
+ metadata = {
+ name = module.this.id
+ namespace = var.kubernetes_namespace
+ labels = local.kubernetes_labels
+ }
+
+ spec = {
+ poolSize = var.worker_pool_size
+
+ # keepSuccessfulPods indicates whether run Pods should automatically be removed as soon
+ # as they complete successfully, or be kept so that they can be inspected later. By default
+ # run Pods are removed as soon as they complete successfully. Failed Pods are not automatically
+ # removed to allow debugging.
+ keepSuccessfulPods = var.keep_successful_pods
+
+ # `token` and `privateKey` are used by the workers to communicate with Spacelift servers
+ token = {
+ secretKeyRef = {
+ name = module.this.name
+ key = "token"
+ }
+ }
+
+ privateKey = {
+ secretKeyRef = {
+ name = module.this.name
+ key = "privateKey"
+ }
+ }
+
+ pod = merge({
+ serviceAccountName = local.kubernetes_service_account_enabled ? var.kubernetes_service_account_name : null
+ automountServiceAccountToken = local.kubernetes_service_account_enabled ? true : false
+
+ # activeDeadlineSeconds defines the length of time in seconds before which the Pod will
+ # be marked as failed. This can be used to set a deadline for your runs.
+ activeDeadlineSeconds = var.worker_spec.active_deadline_seconds
+
+ terminationGracePeriodSeconds = var.worker_spec.termination_grace_period_seconds
+
+ annotations = var.worker_spec.annotations
+ nodeSelector = var.worker_spec.node_selector
+ tolerations = var.worker_spec.tolerations
+
+ labels = local.kubernetes_labels
+
+ # Init container resource limits only matter if they are greater than the worker container resources.
+ # See: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/#resource-sharing-within-containers
+ # So we give the init container the same resources as the worker container.
+ initContainer = {
+ resources = var.worker_spec.resources
+ }
+
+ grpcServerContainer = {
+ resources = var.grpc_server_resources
+ }
+
+ workerContainer = {
+ resources = var.worker_spec.resources
+ env = [
+ {
+ "name" = "AWS_CONFIG_FILE"
+ "value" = var.aws_config_file
+ },
+ {
+ "name" = "AWS_PROFILE"
+ "value" = coalesce(var.aws_profile, "${module.this.namespace}-identity")
+ },
+ {
+ "name" = "AWS_SDK_LOAD_CONFIG"
+ "value" = true
+ },
+ {
+ name = "SPACELIFT_IN_KUBERNETES"
+ value = true
+ },
+ {
+ name = "SPACELIFT_WHITELIST_ENVS"
+ value = "AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN,AWS_SDK_LOAD_CONFIG,AWS_CONFIG_FILE,AWS_PROFILE,GITHUB_TOKEN,INFRACOST_API_KEY,ATMOS_BASE_PATH,TF_VAR_terraform_user"
+ },
+ {
+ name = "SPACELIFT_MASK_ENVS"
+ value = "AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN,GITHUB_TOKEN,INFRACOST_API_KEY"
+ },
+ {
+ name = "SPACELIFT_LAUNCHER_LOGS_TIMEOUT"
+ value = "30m"
+ },
+ {
+ name = "SPACELIFT_LAUNCHER_RUN_TIMEOUT"
+ value = "120m"
+ }
+ ]
+ }
+ },
+
+ var.worker_spec.tmpfs_enabled ? {
+ workspaceVolume = {
+ name = "workspace"
+ emptyDir = {
+ medium = "Memory"
+ }
+ }
+ } : {}
+ )
+ }
+ }
+
+
+ depends_on = [
+ kubernetes_secret.default,
+ kubernetes_service_account_v1.default,
+ kubernetes_role_v1.default,
+ kubernetes_role_binding_v1.default
+ ]
+}
diff --git a/modules/eks/spacelift-worker-pool/main.tf b/modules/eks/spacelift-worker-pool/main.tf
new file mode 100644
index 000000000..7b939518c
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/main.tf
@@ -0,0 +1,30 @@
+# https://docs.spacelift.io/concepts/worker-pools#kubernetes
+# https://docs.spacelift.io/integrations/docker#customizing-the-runner-image
+
+locals {
+ enabled = module.this.enabled
+
+ eks_outputs = module.eks.outputs
+ eks_cluster_identity_oidc_issuer = try(local.eks_outputs.eks_cluster_identity_oidc_issuer, "")
+
+ existing_spaces = { for i in data.spacelift_spaces.default[0].spaces : i.name => i if local.enabled }
+ space_id = local.enabled ? local.existing_spaces[var.space_name].space_id : ""
+}
+
+# Read all the existing spaces from Spacelift
+data "spacelift_spaces" "default" {
+ count = local.enabled ? 1 : 0
+}
+
+# Create worker pool in Spacelift
+# https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/worker_pool
+resource "spacelift_worker_pool" "default" {
+ count = local.enabled ? 1 : 0
+
+ name = module.this.id
+ space_id = local.space_id
+
+ description = var.worker_pool_description != null && var.worker_pool_description != "" ? var.worker_pool_description : (
+ "Worker Pool on Kubernetes deployed into ${local.eks_cluster_id} EKS cluster in Spacelift ${local.space_id} space"
+ )
+}
diff --git a/modules/eks/spacelift-worker-pool/outputs.tf b/modules/eks/spacelift-worker-pool/outputs.tf
new file mode 100644
index 000000000..a7ab80b60
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/outputs.tf
@@ -0,0 +1,54 @@
+output "worker_pool_id" {
+ value = one(spacelift_worker_pool.default[*].id)
+ description = "Spacelift worker pool ID"
+}
+
+output "worker_pool_name" {
+ value = one(spacelift_worker_pool.default[*].name)
+ description = "Spacelift worker pool name"
+}
+
+output "spacelift_worker_pool_manifest" {
+ value = one(kubernetes_manifest.spacelift_worker_pool[*].manifest)
+ description = "Spacelift worker pool Kubernetes manifest"
+}
+
+output "service_account_namespace" {
+ value = module.eks_iam_role.service_account_namespace
+ description = "Kubernetes Service Account namespace"
+}
+
+output "service_account_name" {
+ value = module.eks_iam_role.service_account_name
+ description = "Kubernetes Service Account name"
+}
+
+output "service_account_role_name" {
+ value = module.eks_iam_role.service_account_role_name
+ description = "IAM role name"
+}
+
+output "service_account_role_unique_id" {
+ value = module.eks_iam_role.service_account_role_unique_id
+ description = "IAM role unique ID"
+}
+
+output "service_account_role_arn" {
+ value = module.eks_iam_role.service_account_role_arn
+ description = "IAM role ARN"
+}
+
+output "service_account_policy_name" {
+ value = module.eks_iam_role.service_account_policy_name
+ description = "IAM policy name"
+}
+
+output "service_account_policy_id" {
+ value = module.eks_iam_role.service_account_policy_id
+ description = "IAM policy ID"
+}
+
+output "service_account_policy_arn" {
+ value = module.eks_iam_role.service_account_policy_arn
+ description = "IAM policy ARN"
+}
diff --git a/modules/eks/spacelift-worker-pool/provider-helm.tf b/modules/eks/spacelift-worker-pool/provider-helm.tf
new file mode 100644
index 000000000..91cc7f6d4
--- /dev/null
+++ b/modules/eks/spacelift-worker-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/spacelift-worker-pool/provider-spacelift.tf b/modules/eks/spacelift-worker-pool/provider-spacelift.tf
new file mode 100644
index 000000000..97c46231a
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/provider-spacelift.tf
@@ -0,0 +1,14 @@
+data "aws_ssm_parameter" "spacelift_key_id" {
+ name = "/spacelift/key_id"
+}
+
+data "aws_ssm_parameter" "spacelift_key_secret" {
+ name = "/spacelift/key_secret"
+}
+
+# This provider always validates its credentials, so we always pass api_key_id and api_key_secret
+provider "spacelift" {
+ api_key_endpoint = var.spacelift_api_endpoint
+ api_key_id = data.aws_ssm_parameter.spacelift_key_id.value
+ api_key_secret = data.aws_ssm_parameter.spacelift_key_secret.value
+}
diff --git a/modules/eks/spacelift-worker-pool/providers.tf b/modules/eks/spacelift-worker-pool/providers.tf
new file mode 100644
index 000000000..89ed50a98
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/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/spacelift-worker-pool/remote-state.tf b/modules/eks/spacelift-worker-pool/remote-state.tf
new file mode 100644
index 000000000..6652f887f
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/remote-state.tf
@@ -0,0 +1,32 @@
+module "account_map" {
+ source = "cloudposse/stack-config/yaml//modules/remote-state"
+ version = "1.8.0"
+
+ component = "account-map"
+ environment = module.iam_roles.global_environment_name
+ stage = module.iam_roles.global_stage_name
+ tenant = module.iam_roles.global_tenant_name
+
+ context = module.this.context
+}
+
+module "eks" {
+ source = "cloudposse/stack-config/yaml//modules/remote-state"
+ version = "1.8.0"
+
+ component = var.eks_component_name
+
+ context = module.this.context
+}
+
+module "ecr" {
+ source = "cloudposse/stack-config/yaml//modules/remote-state"
+ version = "1.8.0"
+
+ component = var.ecr_component_name
+ environment = coalesce(var.ecr_environment_name, module.this.environment)
+ stage = coalesce(var.ecr_stage_name, module.this.stage)
+ tenant = coalesce(var.ecr_tenant_name, module.this.tenant)
+
+ context = module.this.context
+}
diff --git a/modules/eks/spacelift-worker-pool/variables.tf b/modules/eks/spacelift-worker-pool/variables.tf
new file mode 100644
index 000000000..b6c963fe8
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/variables.tf
@@ -0,0 +1,223 @@
+variable "region" {
+ type = string
+ description = "AWS Region"
+}
+
+variable "spacelift_api_endpoint" {
+ type = string
+ description = "The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io)"
+}
+
+variable "eks_component_name" {
+ type = string
+ description = "The name of the eks component"
+ default = "eks/cluster"
+}
+
+variable "space_name" {
+ type = string
+ description = "The name of the Spacelift Space to create the worker pool in"
+ default = "root"
+}
+
+variable "worker_pool_description" {
+ type = string
+ description = "Spacelift worker pool description. The default dynamically includes EKS cluster ID and Spacelift Space name."
+ default = null
+}
+
+variable "worker_pool_size" {
+ type = number
+ description = "Worker pool size. The number of workers registered with Spacelift."
+ default = 1
+}
+
+variable "worker_spec" {
+ type = object({
+ tmpfs_enabled = optional(bool, false)
+ resources = optional(object({
+ limits = optional(object({
+ cpu = optional(string, "1")
+ memory = optional(string, "4500Mi")
+ ephemeral-storage = optional(string, "2G")
+ }), {})
+ requests = optional(object({
+ cpu = optional(string, "750m")
+ memory = optional(string, "4Gi")
+ ephemeral-storage = optional(string, "1G")
+ }), {})
+ }), {})
+ annotations = optional(map(string), {})
+ node_selector = optional(map(string), {})
+ tolerations = optional(list(object({
+ key = optional(string)
+ operator = optional(string)
+ value = optional(string)
+ effect = optional(string)
+ toleration_seconds = optional(number)
+ })), [])
+ # activeDeadlineSeconds defines the length of time in seconds before which the Pod will
+ # be marked as failed. This can be used to set a time limit for your runs.
+ active_deadline_seconds = optional(number, 4200) # 4200 seconds = 70 minutes
+ termination_grace_period_seconds = optional(number, 50)
+ })
+ description = "Configuration for the Workers in the worker pool"
+ default = {}
+}
+
+variable "grpc_server_resources" {
+ type = object({
+ requests = optional(object({
+ memory = optional(string, "50Mi")
+ cpu = optional(string, "50m")
+ }), {})
+ limits = optional(object({
+ memory = optional(string, "500Mi")
+ cpu = optional(string, "500m")
+ }), {})
+ })
+ description = "Resources for the gRPC server part of the worker pool deployment. The default values are usually sufficient."
+ default = {}
+}
+
+variable "keep_successful_pods" {
+ type = bool
+ description = <<-EOT
+ Indicates whether run Pods should automatically be removed as soon
+ as they complete successfully, or be kept so that they can be inspected later. By default
+ run Pods are removed as soon as they complete successfully. Failed Pods are not automatically
+ removed to allow debugging.
+ EOT
+ default = false
+}
+
+variable "iam_attributes" {
+ type = list(string)
+ description = "Additional attributes to add to the IDs of the IAM role and policy"
+ default = []
+}
+
+variable "aws_config_file" {
+ type = string
+ description = "The AWS_CONFIG_FILE used by the worker. Can be overridden by `/.spacelift/config.yml`."
+}
+
+variable "aws_profile" {
+ type = string
+ description = <<-EOT
+ The AWS_PROFILE used by the worker. If not specified, `"$${var.namespace}-identity"` will be used.
+ Can be overridden by `/.spacelift/config.yml`.
+ EOT
+ default = null
+}
+
+variable "ecr_environment_name" {
+ type = string
+ description = "The name of the environment where `ecr` is provisioned"
+ default = ""
+}
+
+variable "ecr_stage_name" {
+ type = string
+ description = "The name of the stage where `ecr` is provisioned"
+ default = "artifacts"
+}
+
+variable "ecr_tenant_name" {
+ type = string
+ description = <<-EOT
+ The name of the tenant where `ecr` is provisioned.
+
+ If the `tenant` label is not used, leave this as `null`.
+ EOT
+ default = null
+}
+
+variable "ecr_component_name" {
+ type = string
+ description = "ECR component name"
+ default = "ecr"
+}
+
+variable "ecr_repo_name" {
+ type = string
+ description = "ECR repository name"
+}
+
+variable "kubernetes_namespace" {
+ type = string
+ description = "Name of the Kubernetes Namespace the Spacelift worker pool is deployed in to"
+}
+
+variable "kubernetes_service_account_name" {
+ type = string
+ description = "Kubernetes service account name"
+ default = null
+}
+
+variable "kubernetes_service_account_enabled" {
+ type = bool
+ description = "Flag to enable/disable Kubernetes service account"
+ default = false
+ nullable = false
+}
+
+variable "kubernetes_role_api_groups" {
+ type = list(string)
+ description = "List of APIGroups for the Kubernetes Role created for the Kubernetes Service Account"
+ default = [""]
+ nullable = false
+}
+
+variable "kubernetes_role_resources" {
+ type = list(string)
+ description = "List of resources for the Kubernetes Role created for the Kubernetes Service Account"
+ default = ["*"]
+ nullable = false
+}
+
+variable "kubernetes_role_resource_names" {
+ type = list(string)
+ description = "List of resource names for the Kubernetes Role created for the Kubernetes Service Account"
+ default = null
+}
+
+variable "kubernetes_role_verbs" {
+ type = list(string)
+ description = "List of verbs that apply to ALL the ResourceKinds for the Kubernetes Role created for the Kubernetes Service Account"
+ default = ["get", "list"]
+ nullable = false
+}
+
+variable "iam_permissions_boundary" {
+ type = string
+ description = "ARN of the policy that is used to set the permissions boundary for the IAM Role"
+ default = null
+}
+
+variable "iam_source_json_url" {
+ type = string
+ description = "IAM source JSON policy to download"
+ default = null
+}
+
+variable "iam_source_policy_documents" {
+ type = list(string)
+ description = <<-EOT
+ List of IAM policy documents that are merged together into the exported document.
+ Statements defined in `iam_source_policy_documents` must have unique SIDs.
+ Statements with the same SID as in statements in documents assigned to the
+ `iam_override_policy_documents` arguments will be overridden.
+ EOT
+ default = null
+}
+
+variable "iam_override_policy_documents" {
+ type = list(string)
+ description = <<-EOT
+ List of IAM policy documents that are merged together into the exported document with higher precedence.
+ In merging, statements with non-blank SIDs will override statements with the same SID
+ from earlier documents in the list and from other "source" documents.
+ EOT
+ default = null
+}
diff --git a/modules/eks/spacelift-worker-pool/versions.tf b/modules/eks/spacelift-worker-pool/versions.tf
new file mode 100644
index 000000000..2ebb56681
--- /dev/null
+++ b/modules/eks/spacelift-worker-pool/versions.tf
@@ -0,0 +1,22 @@
+terraform {
+ required_version = ">= 1.3.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 4.9.0"
+ }
+ spacelift = {
+ source = "spacelift-io/spacelift"
+ version = ">= 0.1.2"
+ }
+ helm = {
+ source = "hashicorp/helm"
+ version = ">= 2.0"
+ }
+ kubernetes = {
+ source = "hashicorp/kubernetes"
+ version = ">= 2.18.1, != 2.21.0"
+ }
+ }
+}
diff --git a/modules/eks/storage-class/README.md b/modules/eks/storage-class/README.md
index a9c64d06e..264c2f5ef 100644
--- a/modules/eks/storage-class/README.md
+++ b/modules/eks/storage-class/README.md
@@ -156,8 +156,8 @@ eks/storage-class:
| [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 |
+| [ebs\_storage\_classes](#input\_ebs\_storage\_classes) | A map of storage class name to EBS parameters to create | map(object({
enabled = optional(bool, true)
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({
enabled = optional(bool, true)
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 |
diff --git a/modules/eks/storage-class/main.tf b/modules/eks/storage-class/main.tf
index e4abdd8fb..511772fe3 100644
--- a/modules/eks/storage-class/main.tf
+++ b/modules/eks/storage-class/main.tf
@@ -1,8 +1,14 @@
locals {
enabled = module.this.enabled
- efs_components = local.enabled ? toset([for k, v in var.efs_storage_classes : v.efs_component_name]) : []
+ efs_storage_classes = {
+ for k, v in var.efs_storage_classes : k => v if v.enabled
+ }
+ efs_components = local.enabled ? toset([for k, v in local.efs_storage_classes : v.efs_component_name]) : []
+ ebs_storage_classes = {
+ for k, v in var.ebs_storage_classes : k => v if v.enabled
+ }
# 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
@@ -24,7 +30,7 @@ locals {
}
resource "kubernetes_storage_class_v1" "ebs" {
- for_each = local.enabled ? var.ebs_storage_classes : {}
+ for_each = local.enabled ? local.ebs_storage_classes : {}
metadata {
name = each.key
@@ -69,7 +75,7 @@ resource "kubernetes_storage_class_v1" "ebs" {
}
resource "kubernetes_storage_class_v1" "efs" {
- for_each = local.enabled ? var.efs_storage_classes : {}
+ for_each = local.enabled ? local.efs_storage_classes : {}
metadata {
name = each.key
diff --git a/modules/eks/storage-class/variables.tf b/modules/eks/storage-class/variables.tf
index 597970e54..38b20af1e 100644
--- a/modules/eks/storage-class/variables.tf
+++ b/modules/eks/storage-class/variables.tf
@@ -12,6 +12,7 @@ variable "eks_component_name" {
variable "ebs_storage_classes" {
type = map(object({
+ enabled = optional(bool, true)
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)
@@ -57,6 +58,7 @@ variable "ebs_storage_classes" {
variable "efs_storage_classes" {
type = map(object({
+ enabled = optional(bool, true)
make_default_storage_class = optional(bool, false)
labels = optional(map(string), null)
efs_component_name = optional(string, "eks/efs")
diff --git a/modules/elasticache-redis/README.md b/modules/elasticache-redis/README.md
index eaec1c2ae..ee1d916eb 100644
--- a/modules/elasticache-redis/README.md
+++ b/modules/elasticache-redis/README.md
@@ -33,11 +33,13 @@ components:
apply_immediately: false
automatic_failover_enabled: false
cloudwatch_metric_alarms_enabled: false
+ snapshot_retention_limit: 1
redis_clusters:
redis-main:
num_replicas: 1
num_shards: 0
replicas_per_shard: 0
+ engine: "redis"
engine_version: 6.0.5
instance_type: cache.t2.small
parameters:
@@ -68,6 +70,9 @@ components:
value: lK
```
+The `engine` can either be `redis` or `valkey`. For more information, see
+[why aws supports valkey](https://aws.amazon.com/blogs/opensource/why-aws-supports-valkey/).
+
## Requirements
@@ -101,40 +106,42 @@ No resources.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
-| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no |
-| [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 |
+| [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 |
-| [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 |
+| [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 |
| [auth\_token\_enabled](#input\_auth\_token\_enabled) | Enable auth token | `bool` | `true` | no |
+| [auto\_minor\_version\_upgrade](#input\_auto\_minor\_version\_upgrade) | 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. | `bool` | `false` | no |
| [automatic\_failover\_enabled](#input\_automatic\_failover\_enabled) | Enable automatic failover | `bool` | n/a | yes |
| [availability\_zones](#input\_availability\_zones) | Availability zone IDs | `list(string)` | `[]` | no |
| [cloudwatch\_metric\_alarms\_enabled](#input\_cloudwatch\_metric\_alarms\_enabled) | Boolean flag to enable/disable CloudWatch metrics alarms | `bool` | n/a | yes |
-| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` | {
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no |
-| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no |
-| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no |
+| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` | {
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no |
+| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no |
+| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no |
| [eks\_component\_names](#input\_eks\_component\_names) | The names of the eks components | `set(string)` | `[]` | no |
| [eks\_security\_group\_enabled](#input\_eks\_security\_group\_enabled) | Use the eks default security group | `bool` | `false` | no |
| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no |
| [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 |
+| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no |
| [ingress\_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 |
+| [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 |
+| [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 |
| [redis\_clusters](#input\_redis\_clusters) | Redis cluster configuration | `map(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 |
+| [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 |
+| [snapshot\_retention\_limit](#input\_snapshot\_retention\_limit) | The number of days for which ElastiCache will retain automatic cache cluster snapshots before deleting them. | `number` | `0` | 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 |
+| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
| [transit\_encryption\_enabled](#input\_transit\_encryption\_enabled) | Enable TLS | `bool` | n/a | yes |
diff --git a/modules/elasticache-redis/main.tf b/modules/elasticache-redis/main.tf
index 0f2f91638..176b5fd4c 100644
--- a/modules/elasticache-redis/main.tf
+++ b/modules/elasticache-redis/main.tf
@@ -52,6 +52,7 @@ locals {
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
+ snapshot_retention_limit = var.snapshot_retention_limit
}
clusters = module.redis_clusters
@@ -69,6 +70,7 @@ module "redis_clusters" {
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 = lookup(each.value, "engine", "redis")
engine_version = each.value.engine_version
create_parameter_group = lookup(each.value, "create_parameter_group", true)
parameters = lookup(each.value, "parameters", null)
diff --git a/modules/elasticache-redis/modules/redis_cluster/main.tf b/modules/elasticache-redis/modules/redis_cluster/main.tf
index 37a3ee332..46f955559 100644
--- a/modules/elasticache-redis/modules/redis_cluster/main.tf
+++ b/modules/elasticache-redis/modules/redis_cluster/main.tf
@@ -10,7 +10,7 @@ locals {
module "redis" {
source = "cloudposse/elasticache-redis/aws"
- version = "1.4.1"
+ version = "1.7.0"
name = var.cluster_name
@@ -29,6 +29,7 @@ module "redis" {
cluster_mode_replicas_per_node_group = var.replicas_per_shard
cluster_size = var.num_replicas
dns_subdomain = var.dns_subdomain
+ engine = var.engine
engine_version = var.engine_version
family = var.cluster_attributes.family
instance_type = var.instance_type
@@ -38,6 +39,7 @@ module "redis" {
port = var.cluster_attributes.port
subnets = var.cluster_attributes.subnets
transit_encryption_enabled = var.cluster_attributes.transit_encryption_enabled
+ snapshot_retention_limit = var.cluster_attributes.snapshot_retention_limit
vpc_id = var.cluster_attributes.vpc_id
zone_id = var.cluster_attributes.zone_id
diff --git a/modules/elasticache-redis/modules/redis_cluster/variables.tf b/modules/elasticache-redis/modules/redis_cluster/variables.tf
index 1c9af10cd..9d87291cb 100644
--- a/modules/elasticache-redis/modules/redis_cluster/variables.tf
+++ b/modules/elasticache-redis/modules/redis_cluster/variables.tf
@@ -11,9 +11,15 @@ variable "create_parameter_group" {
description = "Whether new parameter group should be created. Set to false if you want to use existing parameter group"
}
+variable "engine" {
+ type = string
+ default = "redis"
+ description = "Name of the cache engine to use: either `redis` or `valkey`"
+}
+
variable "engine_version" {
type = string
- description = "Redis Version"
+ description = "Version of the cache engine to use"
default = "6.0.5"
}
@@ -62,6 +68,7 @@ variable "cluster_attributes" {
automatic_failover_enabled = bool
auto_minor_version_upgrade = bool
auth_token_enabled = bool
+ snapshot_retention_limit = number
})
description = "Cluster attributes"
}
diff --git a/modules/elasticache-redis/variables.tf b/modules/elasticache-redis/variables.tf
index b059c6c36..fa00ed154 100644
--- a/modules/elasticache-redis/variables.tf
+++ b/modules/elasticache-redis/variables.tf
@@ -104,3 +104,9 @@ variable "eks_component_names" {
description = "The names of the eks components"
default = []
}
+
+variable "snapshot_retention_limit" {
+ type = number
+ description = "The number of days for which ElastiCache will retain automatic cache cluster snapshots before deleting them."
+ default = 0
+}
diff --git a/modules/runs-on/README.md b/modules/runs-on/README.md
index 3c793f2df..ef24a686c 100644
--- a/modules/runs-on/README.md
+++ b/modules/runs-on/README.md
@@ -72,8 +72,6 @@ vars:
- runs-on
```
-Click to show full stack configuration
-
```yaml
components:
terraform: