diff --git a/modules/dns-delegated/README.md b/modules/dns-delegated/README.md index c39b1710d..e4c1ee951 100644 --- a/modules/dns-delegated/README.md +++ b/modules/dns-delegated/README.md @@ -7,17 +7,34 @@ tags: # Component: `dns-delegated` -This component is responsible for provisioning a DNS zone which delegates nameservers to the DNS zone in the primary DNS +This component is responsible for provisioning a DNS zone which manages subdomains delegated from a DNS zone in the primary DNS account. The primary DNS zone is expected to already be provisioned via [the `dns-primary` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-primary). +If you are deploying a root zone (e.g `example.com`) rather than a subdomain delegated from a root zone (e.g `prod.example.com`), +and only a single account needs to manage or update the zone you are deploying, then you should use `dns-primary` instead to deploy +that root zone into the target account. See +[Why not use dns-delegated for all vanity domains?](https://docs.cloudposse.com/layers/network/faq/#why-not-use-dns-delegated-for-all-vanity-domains) +for more details on that. + This component also provisions a wildcard ACM certificate for the given subdomain. +This component should only be deployed globally, which is to say once per account. See +[Why should the dns-delegated component be deployed globally rather than regionally?](https://docs.cloudposse.com/layers/network/faq/#why-should-the-dns-delegated-component-be-deployed-globally-rather-than-regionally) +for details on why. + +Note that once you delegate a subdomain (e.g. `prod.example.com`) to an account, that +account can deploy multiple levels of sub-subdomains (e.g. `api.use1.prod.example.com`) without further configuration, +although you will need to create additional TLS certificates, as the wildcard in a wildcard TLS certificate +only matches a single level. You can use [our `acm` component](https://github.com/cloudposse/terraform-aws-components/tree/readme-global-only/modules/acm) +for that. + ## Usage -**Stack Level**: Global or Regional +**Stack Level**: Global + -Here's an example snippet for how to use this component. Use this component in global or regional stacks for any +Here's an example snippet for how to use this component. Use this component in global stacks for any accounts where you host services that need DNS records on a given subdomain (e.g. delegated zone) of the root domain (e.g. primary zone). @@ -243,5 +260,10 @@ Takeaway - [cloudposse/terraform-aws-components](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-delegated) - Cloud Posse's upstream component +- [The `dns-primary` component](https://github.com/cloudposse/terraform-aws-components/tree/main/modules/dns-primary). +- [The `acm` component](https://github.com/cloudposse/terraform-aws-components/tree/readme-global-only/modules/acm) +component for that. +- [Why not use dns-delegated for all vanity domains?](https://docs.cloudposse.com/layers/network/faq/#why-not-use-dns-delegated-for-all-vanity-domains) +- [Why should the dns-delegated component be deployed globally rather than regionally?](https://docs.cloudposse.com/layers/network/faq/#why-should-the-dns-delegated-component-be-deployed-globally-rather-than-regionally) [](https://cpco.io/component) diff --git a/modules/eks/spacelift-worker-pool-controller/README.md b/modules/eks/spacelift-worker-pool-controller/README.md new file mode 100644 index 000000000..3ddfa2cd8 --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/README.md @@ -0,0 +1,143 @@ +--- +tags: + - component/eks/spacelift-worker-pool-controller + - layer/spacelift + - provider/aws + - provider/helm +--- + +# Component: `eks/spacelift-worker-pool-controller` + +This component provisions the controller part of the +[Kubernetes Operator](https://docs.spacelift.io/concepts/worker-pools/kubernetes-workers#kubernetes-workers) for +[Spacelift Worker Pools](https://docs.spacelift.io/concepts/worker-pools#kubernetes) into an EKS cluster. It must be +installed in the cluster before installing the `eks/spacelift-worker-pool` component. + +The `eks/spacelift-worker-pool-controller` component must be provisioned only once per EKS cluster. You can deploy the +`eks/spacelift-worker-pool` component multiple times. + +## Usage + +**Stack Level**: Regional + +```yaml +# stacks/catalog/eks/spacelift-worker-pool-controller/defaults.yaml +components: + terraform: + eks/spacelift-worker-pool-controller: + vars: + enabled: true + name: "spacelift-controller" + eks_component_name: eks/cluster + # https://github.com/spacelift-io/spacelift-helm-charts/tree/main/spacelift-workerpool-controller + # https://docs.spacelift.io/concepts/worker-pools#kubernetes + chart: "spacelift-workerpool-controller" + chart_repository: "https://downloads.spacelift.io/helm" + chart_version: "0.19.0" + chart_description: "Helm chart for deploying Spacelift worker pool controller and WorkerPool CRD" + create_namespace_with_kubernetes: true + kubernetes_namespace: "spacelift-worker-pool" + timeout: 180 + cleanup_on_fail: true + atomic: true + wait: true + chart_values: {} +``` + +## References + +- https://docs.spacelift.io/concepts/worker-pools#kubernetes +- https://docs.spacelift.io/integrations/docker#customizing-the-runner-image +- https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/worker_pool +- https://docs.spacelift.io/concepts/worker-pools#installation +- https://github.com/spacelift-io/spacelift-helm-charts/tree/main/spacelift-workerpool-controller +- https://github.com/spacelift-io/spacelift-helm-charts/blob/main/spacelift-workerpool-controller/values.yaml +- https://github.com/hashicorp/terraform-provider-kubernetes/issues/1367 + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.18.1, != 2.21.0 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.8.0 | +| [iam\_roles](#module\_iam\_roles) | ../../account-map/modules/iam-roles | n/a | +| [spacelift\_worker\_pool\_controller](#module\_spacelift\_worker\_pool\_controller) | 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 | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [atomic](#input\_atomic) | If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used | `bool` | `true` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [chart](#input\_chart) | Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended. | `string` | n/a | yes | +| [chart\_description](#input\_chart\_description) | Set release description attribute (visible in the history). | `string` | `null` | no | +| [chart\_repository](#input\_chart\_repository) | Repository URL where to locate the requested chart. | `string` | n/a | yes | +| [chart\_values](#input\_chart\_values) | Additional values to yamlencode as `helm_release` values | `any` | `{}` | no | +| [chart\_version](#input\_chart\_version) | Specify the exact chart version to install. If this is not specified, the latest version is installed | `string` | `null` | no | +| [cleanup\_on\_fail](#input\_cleanup\_on\_fail) | Allow deletion of new resources created in this upgrade when upgrade fails | `bool` | `true` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"namespace": null,
"regex_replace_chars": null,
"stage": null,
"tags": {},
"tenant": null
}
| no | +| [create\_namespace\_with\_kubernetes](#input\_create\_namespace\_with\_kubernetes) | 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 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) | Name of the Kubernetes Namespace this pod is deployed in to | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [namespace](#input\_namespace) | ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | AWS Region | `string` | n/a | yes | +| [stage](#input\_stage) | ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release' | `string` | `null` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no | +| [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` | `bool` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [spacelift\_worker\_pool\_controller\_metadata](#output\_spacelift\_worker\_pool\_controller\_metadata) | Block status of the deployed Spacelift worker pool Kubernetes controller | + + diff --git a/modules/eks/spacelift-worker-pool-controller/context.tf b/modules/eks/spacelift-worker-pool-controller/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/eks/spacelift-worker-pool-controller/main.tf b/modules/eks/spacelift-worker-pool-controller/main.tf new file mode 100644 index 000000000..c2208d781 --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/main.tf @@ -0,0 +1,59 @@ +# https://docs.spacelift.io/concepts/worker-pools#kubernetes +# https://docs.spacelift.io/integrations/docker#customizing-the-runner-image + +locals { + enabled = module.this.enabled + + kubernetes_labels = { for k, v in merge(module.this.tags, { name = var.kubernetes_namespace }) : k => replace(v, "/", "_") if local.enabled } +} + +# Deploy Spacelift worker pool Kubernetes controller Helm chart +# https://docs.spacelift.io/concepts/worker-pools#installation +# https://github.com/spacelift-io/spacelift-helm-charts/tree/main/spacelift-workerpool-controller +# https://github.com/spacelift-io/spacelift-helm-charts/blob/main/spacelift-workerpool-controller/values.yaml +module "spacelift_worker_pool_controller" { + source = "cloudposse/helm-release/aws" + version = "0.10.1" + + chart = var.chart + repository = var.chart_repository + chart_version = var.chart_version + description = var.chart_description + + wait = var.wait + atomic = var.atomic + cleanup_on_fail = var.cleanup_on_fail + timeout = var.timeout + + create_namespace_with_kubernetes = var.create_namespace_with_kubernetes + kubernetes_namespace = var.kubernetes_namespace + kubernetes_namespace_labels = local.kubernetes_labels + + # eks_cluster_oidc_issuer_url is only needed when iam_role_enabled is true, + # but unfortunately the module requires a non-empty value. + eks_cluster_oidc_issuer_url = "not-needed" + + iam_role_enabled = false + + values = compact([ + # standard k8s object settings + yamlencode({ + fullnameOverride = module.this.name + }), + + yamlencode( + { + controllerManager = { + namespaces = [ + var.kubernetes_namespace + ] + } + } + ), + + # additional values + yamlencode(var.chart_values) + ]) + + context = module.this.context +} diff --git a/modules/eks/spacelift-worker-pool-controller/outputs.tf b/modules/eks/spacelift-worker-pool-controller/outputs.tf new file mode 100644 index 000000000..f0cf3a90a --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/outputs.tf @@ -0,0 +1,4 @@ +output "spacelift_worker_pool_controller_metadata" { + value = module.spacelift_worker_pool_controller.metadata + description = "Block status of the deployed Spacelift worker pool Kubernetes controller" +} diff --git a/modules/eks/spacelift-worker-pool-controller/provider-helm.tf b/modules/eks/spacelift-worker-pool-controller/provider-helm.tf new file mode 100644 index 000000000..91cc7f6d4 --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/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-controller/providers.tf b/modules/eks/spacelift-worker-pool-controller/providers.tf new file mode 100644 index 000000000..89ed50a98 --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/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-controller/remote-state.tf b/modules/eks/spacelift-worker-pool-controller/remote-state.tf new file mode 100644 index 000000000..7e2658e6a --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/remote-state.tf @@ -0,0 +1,8 @@ +module "eks" { + source = "cloudposse/stack-config/yaml//modules/remote-state" + version = "1.8.0" + + component = var.eks_component_name + + context = module.this.context +} diff --git a/modules/eks/spacelift-worker-pool-controller/variables.tf b/modules/eks/spacelift-worker-pool-controller/variables.tf new file mode 100644 index 000000000..433703998 --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/variables.tf @@ -0,0 +1,74 @@ +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" { + type = string + description = "Chart name to be installed. The chart name can be local path, a URL to a chart, or the name of the chart if `repository` is specified. It is also possible to use the `/` format here if you are running Terraform on a system that the repository has been added to with `helm repo add` but this is not recommended." +} + +variable "chart_repository" { + type = string + description = "Repository URL where to locate the requested chart." +} + +variable "chart_description" { + type = string + description = "Set release description attribute (visible in the history)." + default = null +} + +variable "chart_version" { + type = string + description = "Specify the exact chart version to install. If this is not specified, the latest version is installed" + default = null +} + +variable "create_namespace_with_kubernetes" { + type = bool + description = "Create the Kubernetes namespace if it does not yet exist" + default = true +} + +variable "kubernetes_namespace" { + type = string + description = "Name of the Kubernetes Namespace this pod is deployed in to" + default = null +} + +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`" + default = null +} + +variable "chart_values" { + type = any + description = "Additional values to yamlencode as `helm_release` values" + default = {} +} diff --git a/modules/eks/spacelift-worker-pool-controller/versions.tf b/modules/eks/spacelift-worker-pool-controller/versions.tf new file mode 100644 index 000000000..45c86bfde --- /dev/null +++ b/modules/eks/spacelift-worker-pool-controller/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.18.1, != 2.21.0" + } + } +} diff --git a/modules/eks/spacelift-worker-pool/README.md b/modules/eks/spacelift-worker-pool/README.md new file mode 100644 index 000000000..3820f9a54 --- /dev/null +++ b/modules/eks/spacelift-worker-pool/README.md @@ -0,0 +1,201 @@ +--- +tags: + - component/eks/spacelift-worker-pool + - layer/spacelift + - provider/aws + - provider/helm + - provider/spacelift +--- + +# Component: `eks/spacelift-worker-pool` + +This component provisions the `WorkerPool` part of the +[Kubernetes Operator](https://docs.spacelift.io/concepts/worker-pools/kubernetes-workers#kubernetes-workers) for +[Spacelift Worker Pools](https://docs.spacelift.io/concepts/worker-pools#kubernetes) into an EKS cluster. You can +provision this component multiple times to create multiple worker pools in a single EKS cluster. + +## Usage + +> [!NOTE] +> +> Before provisioning the `eks/spacelift-worker-pool` component, the `eks/spacelift-worker-pool-controller` component +> must be provisioned first into an EKS cluster to enable the +> [Spacelift Worker Pool Kubernetes Controller](https://docs.spacelift.io/concepts/worker-pools#kubernetes). The +> `eks/spacelift-worker-pool-controller` component must be provisioned only once per EKS cluster. + +The Spacelift worker needs to pull a Docker image from an ECR repository. It will run the Terraform commands inside the +Docker container. In the Cloud Posse reference architecture, this image is the "infra" or "infrastructure" image derived +from [Geodesic](https://github.com/cloudposse/geodesic). The worker service account needs permission to pull the image +from the ECR repository, and the details of where to find the image are configured in the various `ecr_*` variables. + +**Stack Level**: Regional + +```yaml +# stacks/catalog/eks/spacelift-worker-pool/defaults.yaml +components: + terraform: + eks/spacelift-worker-pool: + enabled: true + name: "spacelift-worker-pool" + space_name: root + # aws_config_file is the path in the Docker container to the AWS_CONFIG_FILE. + # "/etc/aws-config/aws-config-spacelift" is the usual path in the "infrastructure" image. + aws_config_file: "/etc/aws-config/aws-config-spacelift" + spacelift_api_endpoint: "https://1898andco.app.spacelift.io" + eks_component_name: "eks/cluster" + worker_pool_size: 40 + kubernetes_namespace: "spacelift-worker-pool" + kubernetes_service_account_enabled: true + kubernetes_service_account_name: "spacelift-worker-pool" + keep_successful_pods: false + kubernetes_role_api_groups: [""] + kubernetes_role_resources: ["*"] + kubernetes_role_resource_names: null + kubernetes_role_verbs: ["get", "list"] + ecr_component_name: ecr + ecr_environment_name: use1 + ecr_stage_name: artifacts + ecr_tenant_name: core + ecr_repo_name: infra +``` + +## References + +- https://docs.spacelift.io/concepts/worker-pools#kubernetes +- https://docs.spacelift.io/integrations/docker#customizing-the-runner-image +- https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/worker_pool +- https://docs.spacelift.io/concepts/worker-pools#installation +- https://github.com/spacelift-io/spacelift-helm-charts/tree/main/spacelift-workerpool-controller +- https://github.com/spacelift-io/spacelift-helm-charts/blob/main/spacelift-workerpool-controller/values.yaml +- https://registry.terraform.io/providers/hashicorp/aws/latest/docs#authentication-and-configuration +- https://github.com/aws/aws-cli/issues/3875 +- https://github.com/boto/botocore/issues/2245 + + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | >= 4.9.0 | +| [helm](#requirement\_helm) | >= 2.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.18.1, != 2.21.0 | +| [spacelift](#requirement\_spacelift) | >= 0.1.2 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 4.9.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.18.1, != 2.21.0 | +| [spacelift](#provider\_spacelift) | >= 0.1.2 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [account\_map](#module\_account\_map) | cloudposse/stack-config/yaml//modules/remote-state | 1.8.0 | +| [ecr](#module\_ecr) | cloudposse/stack-config/yaml//modules/remote-state | 1.8.0 | +| [eks](#module\_eks) | cloudposse/stack-config/yaml//modules/remote-state | 1.8.0 | +| [eks\_iam\_policy](#module\_eks\_iam\_policy) | cloudposse/iam-policy/aws | 2.0.1 | +| [eks\_iam\_role](#module\_eks\_iam\_role) | cloudposse/eks-iam-role/aws | 2.1.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_manifest.spacelift_worker_pool](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/manifest) | resource | +| [kubernetes_role_binding_v1.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_binding_v1) | resource | +| [kubernetes_role_v1.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/role_v1) | resource | +| [kubernetes_secret.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | +| [kubernetes_service_account_v1.default](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service_account_v1) | resource | +| [spacelift_worker_pool.default](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/resources/worker_pool) | resource | +| [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_ssm_parameter.spacelift_key_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.spacelift_key_secret](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [spacelift_spaces.default](https://registry.terraform.io/providers/spacelift-io/spacelift/latest/docs/data-sources/spaces) | 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 | +| [aws\_config\_file](#input\_aws\_config\_file) | The AWS\_CONFIG\_FILE used by the worker. Can be overridden by `/.spacelift/config.yml`. | `string` | n/a | yes | +| [aws\_profile](#input\_aws\_profile) | The AWS\_PROFILE used by the worker. If not specified, `"${var.namespace}-identity"` will be used.
Can be overridden by `/.spacelift/config.yml`. | `string` | `null` | no | +| [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 | +| [ecr\_component\_name](#input\_ecr\_component\_name) | ECR component name | `string` | `"ecr"` | no | +| [ecr\_environment\_name](#input\_ecr\_environment\_name) | The name of the environment where `ecr` is provisioned | `string` | `""` | no | +| [ecr\_repo\_name](#input\_ecr\_repo\_name) | ECR repository name | `string` | n/a | yes | +| [ecr\_stage\_name](#input\_ecr\_stage\_name) | The name of the stage where `ecr` is provisioned | `string` | `"artifacts"` | no | +| [ecr\_tenant\_name](#input\_ecr\_tenant\_name) | The name of the tenant where `ecr` is provisioned.

If the `tenant` label is not used, leave this as `null`. | `string` | `null` | no | +| [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 | +| [grpc\_server\_resources](#input\_grpc\_server\_resources) | Resources for the gRPC server part of the worker pool deployment. The default values are usually sufficient. |
object({
requests = optional(object({
memory = optional(string, "50Mi")
cpu = optional(string, "50m")
}), {})
limits = optional(object({
memory = optional(string, "500Mi")
cpu = optional(string, "500m")
}), {})
})
| `{}` | no | +| [helm\_manifest\_experiment\_enabled](#input\_helm\_manifest\_experiment\_enabled) | Enable storing of the rendered manifest for helm\_release so the full diff of what is changing can been seen in the plan | `bool` | `false` | no | +| [iam\_attributes](#input\_iam\_attributes) | Additional attributes to add to the IDs of the IAM role and policy | `list(string)` | `[]` | no | +| [iam\_override\_policy\_documents](#input\_iam\_override\_policy\_documents) | 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. | `list(string)` | `null` | no | +| [iam\_permissions\_boundary](#input\_iam\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM Role | `string` | `null` | no | +| [iam\_source\_json\_url](#input\_iam\_source\_json\_url) | IAM source JSON policy to download | `string` | `null` | no | +| [iam\_source\_policy\_documents](#input\_iam\_source\_policy\_documents) | 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. | `list(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 | +| [keep\_successful\_pods](#input\_keep\_successful\_pods) | 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. | `bool` | `false` | no | +| [kube\_data\_auth\_enabled](#input\_kube\_data\_auth\_enabled) | If `true`, use an `aws_eks_cluster_auth` data source to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled` or `kube_exec_auth_enabled`. | `bool` | `false` | no | +| [kube\_exec\_auth\_aws\_profile](#input\_kube\_exec\_auth\_aws\_profile) | The AWS config profile for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_aws\_profile\_enabled](#input\_kube\_exec\_auth\_aws\_profile\_enabled) | If `true`, pass `kube_exec_auth_aws_profile` as the `profile` to `aws eks get-token` | `bool` | `false` | no | +| [kube\_exec\_auth\_enabled](#input\_kube\_exec\_auth\_enabled) | If `true`, use the Kubernetes provider `exec` feature to execute `aws eks get-token` to authenticate to the EKS cluster.
Disabled by `kubeconfig_file_enabled`, overrides `kube_data_auth_enabled`. | `bool` | `true` | no | +| [kube\_exec\_auth\_role\_arn](#input\_kube\_exec\_auth\_role\_arn) | The role ARN for `aws eks get-token` to use | `string` | `""` | no | +| [kube\_exec\_auth\_role\_arn\_enabled](#input\_kube\_exec\_auth\_role\_arn\_enabled) | If `true`, pass `kube_exec_auth_role_arn` as the role ARN to `aws eks get-token` | `bool` | `true` | no | +| [kubeconfig\_context](#input\_kubeconfig\_context) | Context to choose from the Kubernetes config file.
If supplied, `kubeconfig_context_format` will be ignored. | `string` | `""` | no | +| [kubeconfig\_context\_format](#input\_kubeconfig\_context\_format) | A format string to use for creating the `kubectl` context name when
`kubeconfig_file_enabled` is `true` and `kubeconfig_context` is not supplied.
Must include a single `%s` which will be replaced with the cluster name. | `string` | `""` | no | +| [kubeconfig\_exec\_auth\_api\_version](#input\_kubeconfig\_exec\_auth\_api\_version) | The Kubernetes API version of the credentials returned by the `exec` auth plugin | `string` | `"client.authentication.k8s.io/v1beta1"` | no | +| [kubeconfig\_file](#input\_kubeconfig\_file) | The Kubernetes provider `config_path` setting to use when `kubeconfig_file_enabled` is `true` | `string` | `""` | no | +| [kubeconfig\_file\_enabled](#input\_kubeconfig\_file\_enabled) | If `true`, configure the Kubernetes provider with `kubeconfig_file` and use that kubeconfig file for authenticating to the EKS cluster | `bool` | `false` | no | +| [kubernetes\_namespace](#input\_kubernetes\_namespace) | Name of the Kubernetes Namespace the Spacelift worker pool is deployed in to | `string` | n/a | yes | +| [kubernetes\_role\_api\_groups](#input\_kubernetes\_role\_api\_groups) | List of APIGroups for the Kubernetes Role created for the Kubernetes Service Account | `list(string)` |
[
""
]
| no | +| [kubernetes\_role\_resource\_names](#input\_kubernetes\_role\_resource\_names) | List of resource names for the Kubernetes Role created for the Kubernetes Service Account | `list(string)` | `null` | no | +| [kubernetes\_role\_resources](#input\_kubernetes\_role\_resources) | List of resources for the Kubernetes Role created for the Kubernetes Service Account | `list(string)` |
[
"*"
]
| no | +| [kubernetes\_role\_verbs](#input\_kubernetes\_role\_verbs) | List of verbs that apply to ALL the ResourceKinds for the Kubernetes Role created for the Kubernetes Service Account | `list(string)` |
[
"get",
"list"
]
| no | +| [kubernetes\_service\_account\_enabled](#input\_kubernetes\_service\_account\_enabled) | Flag to enable/disable Kubernetes service account | `bool` | `false` | no | +| [kubernetes\_service\_account\_name](#input\_kubernetes\_service\_account\_name) | Kubernetes service account name | `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 | +| [space\_name](#input\_space\_name) | The name of the Spacelift Space to create the worker pool in | `string` | `"root"` | no | +| [spacelift\_api\_endpoint](#input\_spacelift\_api\_endpoint) | The Spacelift API endpoint URL (e.g. https://example.app.spacelift.io) | `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 | +| [worker\_pool\_description](#input\_worker\_pool\_description) | Spacelift worker pool description. The default dynamically includes EKS cluster ID and Spacelift Space name. | `string` | `null` | no | +| [worker\_pool\_size](#input\_worker\_pool\_size) | Worker pool size. The number of workers registered with Spacelift. | `number` | `1` | no | +| [worker\_spec](#input\_worker\_spec) | Configuration for the Workers in the worker pool |
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)
})
| `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [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 | +| [spacelift\_worker\_pool\_manifest](#output\_spacelift\_worker\_pool\_manifest) | Spacelift worker pool Kubernetes manifest | +| [worker\_pool\_id](#output\_worker\_pool\_id) | Spacelift worker pool ID | +| [worker\_pool\_name](#output\_worker\_pool\_name) | Spacelift worker pool name | + + diff --git a/modules/eks/spacelift-worker-pool/context.tf b/modules/eks/spacelift-worker-pool/context.tf new file mode 100644 index 000000000..5e0ef8856 --- /dev/null +++ b/modules/eks/spacelift-worker-pool/context.tf @@ -0,0 +1,279 @@ +# +# ONLY EDIT THIS FILE IN github.com/cloudposse/terraform-null-label +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/cloudposse/terraform-null-label/blob/master/exports/context.tf +# and then place it in your Terraform module to automatically get +# Cloud Posse's standard configuration inputs suitable for passing +# to Cloud Posse modules. +# +# curl -sL https://raw.githubusercontent.com/cloudposse/terraform-null-label/master/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "cloudposse/label/null" + version = "0.25.0" # requires Terraform >= 0.13.0 + + enabled = var.enabled + namespace = var.namespace + tenant = var.tenant + environment = var.environment + stage = var.stage + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of cloudposse/terraform-null-label/variables.tf here + +variable "context" { + type = any + default = { + enabled = true + namespace = null + tenant = null + environment = null + stage = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "namespace" { + type = string + default = null + description = "ID element. Usually an abbreviation of your organization name, e.g. 'eg' or 'cp', to help ensure generated IDs are globally unique" +} + +variable "tenant" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. A customer identifier, indicating who this instance of a resource is for" +} + +variable "environment" { + type = string + default = null + description = "ID element. Usually used for region e.g. 'uw2', 'us-west-2', OR role 'prod', 'staging', 'dev', 'UAT'" +} + +variable "stage" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prod', 'staging', 'source', 'build', 'test', 'deploy', 'release'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +#### End of copy of cloudposse/terraform-null-label/variables.tf diff --git a/modules/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..a103f07d3 100644 --- a/modules/elasticache-redis/README.md +++ b/modules/elasticache-redis/README.md @@ -38,6 +38,7 @@ components: 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 +69,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 @@ -109,6 +113,7 @@ No resources. | [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 | | [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 | diff --git a/modules/elasticache-redis/main.tf b/modules/elasticache-redis/main.tf index 0f2f91638..c13c90dbe 100644 --- a/modules/elasticache-redis/main.tf +++ b/modules/elasticache-redis/main.tf @@ -69,6 +69,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..59fc64052 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 diff --git a/modules/elasticache-redis/modules/redis_cluster/variables.tf b/modules/elasticache-redis/modules/redis_cluster/variables.tf index 1c9af10cd..411f00d59 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" }