diff --git a/README.md b/README.md index e10940a..368326f 100644 --- a/README.md +++ b/README.md @@ -376,6 +376,7 @@ module "eks" { ## Examples - [EKS Auto Mode](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/examples/eks-auto-mode): EKS Cluster with EKS Auto Mode +- [EKS Capabilities](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/examples/eks-capabilities): EKS Cluster with EKS Capabilities - [EKS Hybrid Nodes](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/examples/eks-hybrid-nodes): EKS Cluster with EKS Hybrid nodes - [EKS Managed Node Group](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/examples/eks-managed-node-group): EKS Cluster with EKS managed node group(s) - [Karpenter](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/examples/karpenter): EKS Cluster with [Karpenter](https://karpenter.sh/) provisioned for intelligent data plane management diff --git a/examples/eks-auto-mode/README.md b/examples/eks-auto-mode/README.md index 0b338d1..d4c54a4 100644 --- a/examples/eks-auto-mode/README.md +++ b/examples/eks-auto-mode/README.md @@ -1,4 +1,4 @@ -# EKS Auto Mode +# EKS Auto Mode Example ## Usage diff --git a/examples/eks-capabilities/README.md b/examples/eks-capabilities/README.md new file mode 100644 index 0000000..d15d1c1 --- /dev/null +++ b/examples/eks-capabilities/README.md @@ -0,0 +1,74 @@ +# EKS Capabilities Example + +## Usage + +To provision the provided configurations you need to execute: + +```bash +terraform init +terraform plan +terraform apply --auto-approve +``` + +Note that this example may create resources which cost money. Run `terraform destroy` when you don't need these resources. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5.7 | +| [aws](#requirement\_aws) | >= 6.28 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 6.28 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [ack\_eks\_capability](#module\_ack\_eks\_capability) | ../../modules/capability | n/a | +| [argocd\_eks\_capability](#module\_argocd\_eks\_capability) | ../../modules/capability | n/a | +| [disabled\_eks\_capability](#module\_disabled\_eks\_capability) | ../../modules/capability | n/a | +| [eks](#module\_eks) | ../.. | n/a | +| [kro\_eks\_capability](#module\_kro\_eks\_capability) | ../../modules/capability | n/a | +| [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | ~> 6.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | +| [aws_identitystore_group.aws_administrator](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/identitystore_group) | data source | +| [aws_ssoadmin_instances.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssoadmin_instances) | data source | + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [ack\_argocd\_server\_url](#output\_ack\_argocd\_server\_url) | URL of the Argo CD server | +| [ack\_arn](#output\_ack\_arn) | The ARN of the EKS Capability | +| [ack\_iam\_role\_arn](#output\_ack\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [ack\_iam\_role\_name](#output\_ack\_iam\_role\_name) | The name of the IAM role | +| [ack\_iam\_role\_unique\_id](#output\_ack\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [ack\_version](#output\_ack\_version) | The version of the EKS Capability | +| [argocd\_arn](#output\_argocd\_arn) | The ARN of the EKS Capability | +| [argocd\_iam\_role\_arn](#output\_argocd\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [argocd\_iam\_role\_name](#output\_argocd\_iam\_role\_name) | The name of the IAM role | +| [argocd\_iam\_role\_unique\_id](#output\_argocd\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [argocd\_server\_url](#output\_argocd\_server\_url) | URL of the Argo CD server | +| [argocd\_version](#output\_argocd\_version) | The version of the EKS Capability | +| [kro\_argocd\_server\_url](#output\_kro\_argocd\_server\_url) | URL of the Argo CD server | +| [kro\_arn](#output\_kro\_arn) | The ARN of the EKS Capability | +| [kro\_iam\_role\_arn](#output\_kro\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role | +| [kro\_iam\_role\_name](#output\_kro\_iam\_role\_name) | The name of the IAM role | +| [kro\_iam\_role\_unique\_id](#output\_kro\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | +| [kro\_version](#output\_kro\_version) | The version of the EKS Capability | + diff --git a/examples/eks-capabilities/main.tf b/examples/eks-capabilities/main.tf new file mode 100644 index 0000000..a220c7f --- /dev/null +++ b/examples/eks-capabilities/main.tf @@ -0,0 +1,163 @@ +provider "aws" { + region = local.region +} + +data "aws_availability_zones" "available" { + # Exclude local zones + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +data "aws_ssoadmin_instances" "this" {} + +data "aws_identitystore_group" "aws_administrator" { + identity_store_id = one(data.aws_ssoadmin_instances.this.identity_store_ids) + + alternate_identifier { + unique_attribute { + attribute_path = "DisplayName" + attribute_value = "AWSAdministrator" + } + } +} + +locals { + name = "ex-${basename(path.cwd)}" + region = "us-east-1" # will need to match where your AWS Identity Center is configured + + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + + tags = { + Test = local.name + GithubRepo = "terraform-aws-eks" + GithubOrg = "terraform-aws-modules" + } +} + +################################################################################ +# EKS Capability Module +################################################################################ + +module "ack_eks_capability" { + source = "../../modules/capability" + + type = "ACK" + cluster_name = module.eks.cluster_name + + # IAM Role/Policy + iam_role_policies = { + AdministratorAccess = "arn:aws:iam::aws:policy/AdministratorAccess" + } + + tags = local.tags +} + +module "argocd_eks_capability" { + source = "../../modules/capability" + + type = "ARGOCD" + cluster_name = module.eks.cluster_name + + configuration = { + argo_cd = { + aws_idc = { + idc_instance_arn = one(data.aws_ssoadmin_instances.this.arns) + } + namespace = "argocd" + rbac_role_mapping = [{ + role = "ADMIN" + identity = [{ + id = data.aws_identitystore_group.aws_administrator.group_id + type = "SSO_GROUP" + }] + }] + } + } + + # IAM Role/Policy + iam_policy_statements = { + ECRRead = { + actions = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ] + resources = ["*"] + } + } + + tags = local.tags +} + +module "kro_eks_capability" { + source = "../../modules/capability" + + type = "KRO" + cluster_name = module.eks.cluster_name + + tags = local.tags +} + +module "disabled_eks_capability" { + source = "../../modules/capability" + + create = false +} + +################################################################################ +# EKS Module +################################################################################ + +module "eks" { + source = "../.." + + name = local.name + kubernetes_version = "1.34" + endpoint_public_access = true + + enable_cluster_creator_admin_permissions = true + + compute_config = { + enabled = true + node_pools = ["general-purpose"] + } + + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + tags = local.tags +} + +################################################################################ +# Supporting Resources +################################################################################ + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 6.0" + + name = local.name + cidr = local.vpc_cidr + + azs = local.azs + private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 4, k)] + public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 48)] + intra_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 52)] + + enable_nat_gateway = true + single_nat_gateway = true + + public_subnet_tags = { + "kubernetes.io/role/elb" = 1 + } + + private_subnet_tags = { + "kubernetes.io/role/internal-elb" = 1 + } + + tags = local.tags +} diff --git a/examples/eks-capabilities/outputs.tf b/examples/eks-capabilities/outputs.tf new file mode 100644 index 0000000..3023628 --- /dev/null +++ b/examples/eks-capabilities/outputs.tf @@ -0,0 +1,104 @@ +################################################################################ +# Capability - ACK +################################################################################ + +output "ack_arn" { + description = "The ARN of the EKS Capability" + value = module.ack_eks_capability.arn +} + +output "ack_version" { + description = "The version of the EKS Capability" + value = module.ack_eks_capability.version +} + +output "ack_argocd_server_url" { + description = "URL of the Argo CD server" + value = module.ack_eks_capability.argocd_server_url +} + +# IAM Role +output "ack_iam_role_name" { + description = "The name of the IAM role" + value = module.ack_eks_capability.iam_role_name +} + +output "ack_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.ack_eks_capability.iam_role_arn +} + +output "ack_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.ack_eks_capability.iam_role_unique_id +} + +################################################################################ +# Capability - ArgoCD +################################################################################ + +output "argocd_arn" { + description = "The ARN of the EKS Capability" + value = module.argocd_eks_capability.arn +} + +output "argocd_version" { + description = "The version of the EKS Capability" + value = module.argocd_eks_capability.version +} + +output "argocd_server_url" { + description = "URL of the Argo CD server" + value = module.argocd_eks_capability.argocd_server_url +} + +# IAM Role +output "argocd_iam_role_name" { + description = "The name of the IAM role" + value = module.argocd_eks_capability.iam_role_name +} + +output "argocd_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.argocd_eks_capability.iam_role_arn +} + +output "argocd_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.argocd_eks_capability.iam_role_unique_id +} + +################################################################################ +# Capability - KRO +################################################################################ + +output "kro_arn" { + description = "The ARN of the EKS Capability" + value = module.kro_eks_capability.arn +} + +output "kro_version" { + description = "The version of the EKS Capability" + value = module.kro_eks_capability.version +} + +output "kro_argocd_server_url" { + description = "URL of the Argo CD server" + value = module.kro_eks_capability.argocd_server_url +} + +# IAM Role +output "kro_iam_role_name" { + description = "The name of the IAM role" + value = module.kro_eks_capability.iam_role_name +} + +output "kro_iam_role_arn" { + description = "The Amazon Resource Name (ARN) specifying the IAM role" + value = module.kro_eks_capability.iam_role_arn +} + +output "kro_iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = module.kro_eks_capability.iam_role_unique_id +} diff --git a/examples/eks-capabilities/variables.tf b/examples/eks-capabilities/variables.tf new file mode 100644 index 0000000..e69de29 diff --git a/examples/eks-capabilities/versions.tf b/examples/eks-capabilities/versions.tf new file mode 100644 index 0000000..d2afd5f --- /dev/null +++ b/examples/eks-capabilities/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.5.7" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.28" + } + } +} diff --git a/modules/capability/README.md b/modules/capability/README.md new file mode 100644 index 0000000..e1ec510 --- /dev/null +++ b/modules/capability/README.md @@ -0,0 +1,174 @@ +# EKS Capability Module + +Configuration in this directory creates the AWS resources required by EKS capabilities + +## Usage + +### ACK + +```hcl +module "ack_eks_capability" { + source = "terraform-aws-modules/eks/aws//modules/capability" + + name = "example-ack" + cluster_name = "example" + type = "ACK" + + # IAM Role/Policy + iam_role_policies = { + AdministratorAccess = "arn:aws:iam::aws:policy/AdministratorAccess" + } + + tags = { + Environment = "dev" + Terraform = "true" + } +} +``` + +### ArgoCD + +```hcl +module "argocd_eks_capability" { + source = "terraform-aws-modules/eks/aws//modules/capability" + + name = "example-argocd" + cluster_name = "example" + type = "ARGOCD" + + configuration = { + configuration = { + argo_cd = { + aws_idc = { + idc_instance_arn = "arn:aws:sso:::instance/ssoins-1234567890abcdef0" + } + namespace = "argocd" + rbac_role_mapping = [{ + role = "ADMIN" + identity = [{ + id = "686103e0-f051-7068-b225-e6392b959d9e" + type = "SSO_GROUP" + }] + }] + } + } + } + + # IAM Role/Policy + iam_policy_statements = { + ECRRead = { + actions = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + ] + resources = ["*"] + } + } + + tags = { + Environment = "dev" + Terraform = "true" + } +} +``` + +### KRO + +```hcl +module "kro_eks_capability" { + source = "terraform-aws-modules/eks/aws//modules/capability" + + name = "example-kro" + cluster_name = "example" + type = "KRO" + + tags = { + Environment = "dev" + Terraform = "true" + } +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.5.7 | +| [aws](#requirement\_aws) | >= 6.28 | +| [time](#requirement\_time) | >= 0.9 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 6.28 | +| [time](#provider\_time) | >= 0.9 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_eks_capability.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_capability) | resource | +| [aws_iam_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.additional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [time_sleep.this](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | +| [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_service_principal.capabilities_eks](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/service_principal) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [cluster\_name](#input\_cluster\_name) | The name of the EKS cluster | `string` | `""` | no | +| [configuration](#input\_configuration) | Configuration for the capability |
object({
argo_cd = optional(object({
aws_idc = object({
idc_instance_arn = string
idc_region = optional(string)
})
namespace = optional(string)
network_access = optional(object({
vpce_ids = optional(list(string))
}))
rbac_role_mapping = optional(list(object({
identity = list(object({
id = string
type = string
}))
role = string
})))
}))
}) | `null` | no |
+| [create](#input\_create) | Controls if resources should be created (affects nearly all resources) | `bool` | `true` | no |
+| [create\_iam\_role](#input\_create\_iam\_role) | Determines whether an IAM role is created | `bool` | `true` | no |
+| [delete\_propagation\_policy](#input\_delete\_propagation\_policy) | The propagation policy to use when deleting the capability. Valid values: `RETAIN` | `string` | `"RETAIN"` | no |
+| [iam\_policy\_description](#input\_iam\_policy\_description) | IAM policy description | `string` | `null` | no |
+| [iam\_policy\_name](#input\_iam\_policy\_name) | Name of the IAM policy | `string` | `null` | no |
+| [iam\_policy\_path](#input\_iam\_policy\_path) | Path of the IAM policy | `string` | `null` | no |
+| [iam\_policy\_statements](#input\_iam\_policy\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) - used for adding specific IAM permissions as needed | map(object({
sid = optional(string)
actions = optional(list(string))
not_actions = optional(list(string))
effect = optional(string)
resources = optional(list(string))
not_resources = optional(list(string))
principals = optional(list(object({
type = string
identifiers = list(string)
})))
not_principals = optional(list(object({
type = string
identifiers = list(string)
})))
condition = optional(list(object({
test = string
values = list(string)
variable = string
})))
})) | `null` | no |
+| [iam\_policy\_use\_name\_prefix](#input\_iam\_policy\_use\_name\_prefix) | Determines whether the name of the IAM policy (`iam_policy_name`) is used as a prefix | `bool` | `true` | no |
+| [iam\_role\_arn](#input\_iam\_role\_arn) | The ARN of the IAM role that provides permissions for the capability | `string` | `null` | no |
+| [iam\_role\_description](#input\_iam\_role\_description) | IAM role description | `string` | `null` | no |
+| [iam\_role\_max\_session\_duration](#input\_iam\_role\_max\_session\_duration) | Maximum API session duration in seconds between 3600 and 43200 | `number` | `null` | no |
+| [iam\_role\_name](#input\_iam\_role\_name) | Name of the IAM role | `string` | `null` | no |
+| [iam\_role\_override\_assume\_policy\_documents](#input\_iam\_role\_override\_assume\_policy\_documents) | A list of IAM policy documents to override the default assume role policy document for the Karpenter controller IAM role | `list(string)` | `[]` | no |
+| [iam\_role\_path](#input\_iam\_role\_path) | Path of the IAM role | `string` | `null` | no |
+| [iam\_role\_permissions\_boundary\_arn](#input\_iam\_role\_permissions\_boundary\_arn) | Permissions boundary ARN to use for the IAM role | `string` | `null` | no |
+| [iam\_role\_policies](#input\_iam\_role\_policies) | Policies to attach to the IAM role in `{'static_name' = 'policy_arn'}` format | `map(string)` | `{}` | no |
+| [iam\_role\_source\_assume\_policy\_documents](#input\_iam\_role\_source\_assume\_policy\_documents) | A list of IAM policy documents to use as a source for the assume role policy document for the Karpenter controller IAM role | `list(string)` | `[]` | no |
+| [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add the the IAM role | `map(string)` | `{}` | no |
+| [iam\_role\_use\_name\_prefix](#input\_iam\_role\_use\_name\_prefix) | Determines whether the name of the IAM role (`iam_role_name`) is used as a prefix | `bool` | `true` | no |
+| [name](#input\_name) | The name of the capability to add to the cluster | `string` | `""` | no |
+| [region](#input\_region) | Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration | `string` | `null` | no |
+| [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no |
+| [timeouts](#input\_timeouts) | Create, update, and delete timeout configurations for the capability | object({
create = optional(string)
update = optional(string)
delete = optional(string)
}) | `null` | no |
+| [type](#input\_type) | Type of the capability. Valid values: `ACK`, `KRO`, `ARGOCD` | `string` | `""` | no |
+| [wait\_duration](#input\_wait\_duration) | Duration to wait between creating the IAM role/policy and creating the capability | `string` | `"20s"` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [argocd\_server\_url](#output\_argocd\_server\_url) | URL of the Argo CD server |
+| [arn](#output\_arn) | The ARN of the EKS Capability |
+| [iam\_role\_arn](#output\_iam\_role\_arn) | The Amazon Resource Name (ARN) specifying the IAM role |
+| [iam\_role\_name](#output\_iam\_role\_name) | The name of the IAM role |
+| [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role |
+| [version](#output\_version) | The version of the EKS Capability |
+
+
+## License
+
+Apache 2 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/LICENSE) for full details.
diff --git a/modules/capability/main.tf b/modules/capability/main.tf
new file mode 100644
index 0000000..51c7bd6
--- /dev/null
+++ b/modules/capability/main.tf
@@ -0,0 +1,222 @@
+data "aws_service_principal" "capabilities_eks" {
+ count = var.create ? 1 : 0
+
+ service_name = "capabilities.eks"
+}
+
+# It appears that the EKS capability API checks for the IAM role trust policy *VERY* early in the process
+# Our standard approach to ordering IAM role/permission dependencies does not work here, so we add an explicit wait
+resource "time_sleep" "this" {
+ count = var.create ? 1 : 0
+
+ create_duration = var.wait_duration
+
+ triggers = {
+ iam_role_arn = local.create_iam_role ? aws_iam_role.this[0].arn : var.iam_role_arn
+ }
+}
+
+################################################################################
+# Capability
+################################################################################
+
+resource "aws_eks_capability" "this" {
+ count = var.create ? 1 : 0
+
+ region = var.region
+
+ capability_name = try(coalesce(var.name, lower(var.type)))
+ cluster_name = var.cluster_name
+
+ dynamic "configuration" {
+ for_each = var.configuration != null ? [var.configuration] : []
+
+ content {
+ dynamic "argo_cd" {
+ for_each = configuration.value.argo_cd != null ? [configuration.value.argo_cd] : []
+
+ content {
+ dynamic "aws_idc" {
+ for_each = [argo_cd.value.aws_idc]
+
+ content {
+ idc_instance_arn = aws_idc.value.idc_instance_arn
+ idc_region = aws_idc.value.idc_region
+ }
+ }
+
+ namespace = argo_cd.value.namespace
+
+ dynamic "network_access" {
+ for_each = argo_cd.value.network_access != null ? [argo_cd.value.network_access] : []
+
+ content {
+ vpce_ids = network_access.value.vpce_ids
+ }
+ }
+
+ dynamic "rbac_role_mapping" {
+ for_each = argo_cd.value.rbac_role_mapping != null ? argo_cd.value.rbac_role_mapping : []
+
+ content {
+ dynamic "identity" {
+ for_each = rbac_role_mapping.value.identity
+
+ content {
+ id = identity.value.id
+ type = identity.value.type
+ }
+ }
+
+ role = rbac_role_mapping.value.role
+ }
+ }
+ }
+ }
+ }
+ }
+
+ delete_propagation_policy = var.delete_propagation_policy
+ role_arn = time_sleep.this[0].triggers["iam_role_arn"]
+ type = var.type
+
+ dynamic "timeouts" {
+ for_each = var.timeouts != null ? [var.timeouts] : []
+
+ content {
+ create = timeouts.value.create
+ delete = timeouts.value.delete
+ update = timeouts.value.update
+ }
+ }
+
+ tags = var.tags
+
+ depends_on = [
+ aws_iam_role_policy_attachment.this,
+ aws_iam_role_policy_attachment.additional,
+ ]
+}
+
+################################################################################
+# IAM Role
+################################################################################
+
+locals {
+ create_iam_role = var.create && var.create_iam_role
+ iam_role_name = try(coalesce(var.iam_role_name, var.name), "")
+}
+
+data "aws_iam_policy_document" "assume_role" {
+ count = local.create_iam_role ? 1 : 0
+
+ override_policy_documents = var.iam_role_override_assume_policy_documents
+ source_policy_documents = var.iam_role_source_assume_policy_documents
+
+ statement {
+ sid = "EKSCapabilitiesAssumeRole"
+ actions = [
+ "sts:AssumeRole",
+ "sts:TagSession",
+ ]
+
+ principals {
+ type = "Service"
+ identifiers = [data.aws_service_principal.capabilities_eks[0].name]
+ }
+ }
+}
+
+resource "aws_iam_role" "this" {
+ count = local.create_iam_role ? 1 : 0
+
+ name = var.iam_role_use_name_prefix ? null : local.iam_role_name
+ name_prefix = var.iam_role_use_name_prefix ? "${local.iam_role_name}-" : null
+ path = var.iam_role_path
+ description = coalesce(var.iam_role_description, "EKS Capability IAM role for ${var.type} capability in cluster ${var.cluster_name}")
+
+ assume_role_policy = data.aws_iam_policy_document.assume_role[0].json
+ max_session_duration = var.iam_role_max_session_duration
+ permissions_boundary = var.iam_role_permissions_boundary_arn
+ force_detach_policies = true
+
+ tags = merge(var.tags, var.iam_role_tags)
+}
+
+################################################################################
+# IAM Role Policy
+################################################################################
+
+locals {
+ create_iam_role_policy = local.create_iam_role && var.iam_policy_statements != null
+ iam_policy_name = try(coalesce(var.iam_policy_name, local.iam_role_name), "")
+}
+
+data "aws_iam_policy_document" "this" {
+ count = local.create_iam_role_policy ? 1 : 0
+
+ dynamic "statement" {
+ for_each = var.iam_policy_statements != null ? var.iam_policy_statements : {}
+
+ content {
+ sid = try(coalesce(statement.value.sid, statement.key))
+ actions = statement.value.actions
+ not_actions = statement.value.not_actions
+ effect = statement.value.effect
+ resources = statement.value.resources
+ not_resources = statement.value.not_resources
+
+ dynamic "principals" {
+ for_each = statement.value.principals != null ? statement.value.principals : []
+
+ content {
+ type = principals.value.type
+ identifiers = principals.value.identifiers
+ }
+ }
+
+ dynamic "not_principals" {
+ for_each = statement.value.not_principals != null ? statement.value.not_principals : []
+
+ content {
+ type = not_principals.value.type
+ identifiers = not_principals.value.identifiers
+ }
+ }
+
+ dynamic "condition" {
+ for_each = statement.value.condition != null ? statement.value.condition : []
+
+ content {
+ test = condition.value.test
+ values = condition.value.values
+ variable = condition.value.variable
+ }
+ }
+ }
+ }
+}
+
+resource "aws_iam_policy" "this" {
+ count = local.create_iam_role_policy ? 1 : 0
+
+ name = var.iam_policy_use_name_prefix ? null : local.iam_policy_name
+ name_prefix = var.iam_policy_use_name_prefix ? "${local.iam_policy_name}-" : null
+ path = var.iam_policy_path
+ description = coalesce(var.iam_policy_description, "IAM policy for EKS Capability ${var.type} capability in cluster ${var.cluster_name}")
+ policy = data.aws_iam_policy_document.this[0].json
+}
+
+resource "aws_iam_role_policy_attachment" "this" {
+ count = local.create_iam_role_policy ? 1 : 0
+
+ role = aws_iam_role.this[0].name
+ policy_arn = aws_iam_policy.this[0].arn
+}
+
+resource "aws_iam_role_policy_attachment" "additional" {
+ for_each = { for k, v in var.iam_role_policies : k => v if local.create_iam_role }
+
+ role = aws_iam_role.this[0].name
+ policy_arn = each.value
+}
diff --git a/modules/capability/outputs.tf b/modules/capability/outputs.tf
new file mode 100644
index 0000000..b812a91
--- /dev/null
+++ b/modules/capability/outputs.tf
@@ -0,0 +1,37 @@
+################################################################################
+# Capability
+################################################################################
+
+output "arn" {
+ description = "The ARN of the EKS Capability"
+ value = try(aws_eks_capability.this[0].arn, null)
+}
+
+output "version" {
+ description = "The version of the EKS Capability"
+ value = try(aws_eks_capability.this[0].version, null)
+}
+
+output "argocd_server_url" {
+ description = "URL of the Argo CD server"
+ value = try(aws_eks_capability.this[0].configuration[0].argo_cd[0].server_url, null)
+}
+
+################################################################################
+# IAM Role
+################################################################################
+
+output "iam_role_name" {
+ description = "The name of the IAM role"
+ value = try(aws_iam_role.this[0].name, null)
+}
+
+output "iam_role_arn" {
+ description = "The Amazon Resource Name (ARN) specifying the IAM role"
+ value = try(aws_iam_role.this[0].arn, null)
+}
+
+output "iam_role_unique_id" {
+ description = "Stable and unique string identifying the IAM role"
+ value = try(aws_iam_role.this[0].unique_id, null)
+}
diff --git a/modules/capability/variables.tf b/modules/capability/variables.tf
new file mode 100644
index 0000000..4458453
--- /dev/null
+++ b/modules/capability/variables.tf
@@ -0,0 +1,215 @@
+variable "create" {
+ description = "Controls if resources should be created (affects nearly all resources)"
+ type = bool
+ default = true
+}
+
+variable "tags" {
+ description = "A map of tags to add to all resources"
+ type = map(string)
+ default = {}
+}
+
+variable "region" {
+ description = "Region where the resource(s) will be managed. Defaults to the Region set in the provider configuration"
+ type = string
+ default = null
+}
+
+################################################################################
+# Capabilities
+################################################################################
+
+variable "name" {
+ description = "The name of the capability to add to the cluster"
+ type = string
+ default = ""
+}
+
+variable "cluster_name" {
+ description = "The name of the EKS cluster"
+ type = string
+ default = ""
+}
+
+variable "configuration" {
+ description = "Configuration for the capability"
+ type = object({
+ argo_cd = optional(object({
+ aws_idc = object({
+ idc_instance_arn = string
+ idc_region = optional(string)
+ })
+ namespace = optional(string)
+ network_access = optional(object({
+ vpce_ids = optional(list(string))
+ }))
+ rbac_role_mapping = optional(list(object({
+ identity = list(object({
+ id = string
+ type = string
+ }))
+ role = string
+ })))
+ }))
+ })
+ default = null
+}
+
+variable "delete_propagation_policy" {
+ description = "The propagation policy to use when deleting the capability. Valid values: `RETAIN`"
+ type = string
+ default = "RETAIN"
+}
+
+variable "type" {
+ description = "Type of the capability. Valid values: `ACK`, `KRO`, `ARGOCD`"
+ type = string
+ default = ""
+}
+
+variable "timeouts" {
+ description = "Create, update, and delete timeout configurations for the capability"
+ type = object({
+ create = optional(string)
+ update = optional(string)
+ delete = optional(string)
+ })
+ default = null
+}
+
+variable "wait_duration" {
+ description = "Duration to wait between creating the IAM role/policy and creating the capability"
+ type = string
+ default = "20s"
+}
+
+################################################################################
+# IAM Role
+################################################################################
+
+variable "create_iam_role" {
+ description = "Determines whether an IAM role is created"
+ type = bool
+ default = true
+}
+
+variable "iam_role_arn" {
+ description = "The ARN of the IAM role that provides permissions for the capability"
+ type = string
+ default = null
+}
+
+variable "iam_role_name" {
+ description = "Name of the IAM role"
+ type = string
+ default = null
+}
+
+variable "iam_role_use_name_prefix" {
+ description = "Determines whether the name of the IAM role (`iam_role_name`) is used as a prefix"
+ type = bool
+ default = true
+}
+
+variable "iam_role_path" {
+ description = "Path of the IAM role"
+ type = string
+ default = null
+}
+
+variable "iam_role_description" {
+ description = "IAM role description"
+ type = string
+ default = null
+}
+
+variable "iam_role_max_session_duration" {
+ description = "Maximum API session duration in seconds between 3600 and 43200"
+ type = number
+ default = null
+}
+
+variable "iam_role_permissions_boundary_arn" {
+ description = "Permissions boundary ARN to use for the IAM role"
+ type = string
+ default = null
+}
+
+variable "iam_role_tags" {
+ description = "A map of additional tags to add the the IAM role"
+ type = map(string)
+ default = {}
+}
+
+################################################################################
+# IAM Role Policy
+################################################################################
+
+variable "iam_policy_name" {
+ description = "Name of the IAM policy"
+ type = string
+ default = null
+}
+
+variable "iam_policy_use_name_prefix" {
+ description = "Determines whether the name of the IAM policy (`iam_policy_name`) is used as a prefix"
+ type = bool
+ default = true
+}
+
+variable "iam_policy_path" {
+ description = "Path of the IAM policy"
+ type = string
+ default = null
+}
+
+variable "iam_policy_description" {
+ description = "IAM policy description"
+ type = string
+ default = null
+}
+
+variable "iam_role_override_assume_policy_documents" {
+ description = "A list of IAM policy documents to override the default assume role policy document for the Karpenter controller IAM role"
+ type = list(string)
+ default = []
+}
+
+variable "iam_role_source_assume_policy_documents" {
+ description = "A list of IAM policy documents to use as a source for the assume role policy document for the Karpenter controller IAM role"
+ type = list(string)
+ default = []
+}
+
+variable "iam_policy_statements" {
+ description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) - used for adding specific IAM permissions as needed"
+ type = map(object({
+ sid = optional(string)
+ actions = optional(list(string))
+ not_actions = optional(list(string))
+ effect = optional(string)
+ resources = optional(list(string))
+ not_resources = optional(list(string))
+ principals = optional(list(object({
+ type = string
+ identifiers = list(string)
+ })))
+ not_principals = optional(list(object({
+ type = string
+ identifiers = list(string)
+ })))
+ condition = optional(list(object({
+ test = string
+ values = list(string)
+ variable = string
+ })))
+ }))
+ default = null
+}
+
+variable "iam_role_policies" {
+ description = "Policies to attach to the IAM role in `{'static_name' = 'policy_arn'}` format"
+ type = map(string)
+ default = {}
+}
diff --git a/modules/capability/versions.tf b/modules/capability/versions.tf
new file mode 100644
index 0000000..25ff202
--- /dev/null
+++ b/modules/capability/versions.tf
@@ -0,0 +1,20 @@
+terraform {
+ required_version = ">= 1.5.7"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 6.28"
+ }
+ time = {
+ source = "hashicorp/time"
+ version = ">= 0.9"
+ }
+ }
+
+ provider_meta "aws" {
+ user_agent = [
+ "github.com/terraform-aws-modules/terraform-aws-eks"
+ ]
+ }
+}
diff --git a/modules/eks-managed-node-group/README.md b/modules/eks-managed-node-group/README.md
index ca0e63e..f3ca2fb 100644
--- a/modules/eks-managed-node-group/README.md
+++ b/modules/eks-managed-node-group/README.md
@@ -219,3 +219,7 @@ module "eks_managed_node_group" {
| [security\_group\_arn](#output\_security\_group\_arn) | Amazon Resource Name (ARN) of the security group |
| [security\_group\_id](#output\_security\_group\_id) | ID of the security group |
+
+## License
+
+Apache 2 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/LICENSE) for full details.
diff --git a/modules/fargate-profile/README.md b/modules/fargate-profile/README.md
index 5e0506f..4af5fd3 100644
--- a/modules/fargate-profile/README.md
+++ b/modules/fargate-profile/README.md
@@ -96,3 +96,7 @@ No modules.
| [iam\_role\_name](#output\_iam\_role\_name) | The name of the IAM role |
| [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role |
+
+## License
+
+Apache 2 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/LICENSE) for full details.
diff --git a/modules/hybrid-node-role/README.md b/modules/hybrid-node-role/README.md
index 4d85f48..874021b 100644
--- a/modules/hybrid-node-role/README.md
+++ b/modules/hybrid-node-role/README.md
@@ -157,3 +157,7 @@ No modules.
| [name](#output\_name) | The name of the node IAM role |
| [unique\_id](#output\_unique\_id) | Stable and unique string identifying the node IAM role |
+
+## License
+
+Apache 2 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/LICENSE) for full details.
diff --git a/modules/karpenter/README.md b/modules/karpenter/README.md
index 5b25eaa..69c03af 100644
--- a/modules/karpenter/README.md
+++ b/modules/karpenter/README.md
@@ -7,6 +7,7 @@ Configuration in this directory creates the AWS resources required by Karpenter
### All Resources (Default)
In the following example, the Karpenter module will create:
+
- An IAM role for use with Pod Identity and a scoped IAM policy for the Karpenter controller
- A Pod Identity association to grant Karpenter controller access provided by the IAM Role
- A Node IAM role that Karpenter will use to create an Instance Profile for the nodes to receive IAM permissions
@@ -40,6 +41,7 @@ module "karpenter" {
### Re-Use Existing Node IAM Role
In the following example, the Karpenter module will create:
+
- An IAM role for use with Pod Identity and a scoped IAM policy for the Karpenter controller
- SQS queue and EventBridge event rules for Karpenter to utilize for spot termination handling, capacity re-balancing, etc.
@@ -200,3 +202,7 @@ No modules.
| [queue\_url](#output\_queue\_url) | The URL for the created Amazon SQS queue |
| [service\_account](#output\_service\_account) | Service Account associated with the Karpenter Pod Identity |
+
+## License
+
+Apache 2 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/LICENSE) for full details.
diff --git a/modules/self-managed-node-group/README.md b/modules/self-managed-node-group/README.md
index 21f55a9..fb267b2 100644
--- a/modules/self-managed-node-group/README.md
+++ b/modules/self-managed-node-group/README.md
@@ -225,3 +225,7 @@ module "self_managed_node_group" {
| [security\_group\_id](#output\_security\_group\_id) | ID of the security group |
| [user\_data](#output\_user\_data) | Base64 encoded user data |
+
+## License
+
+Apache 2 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-eks/tree/master/LICENSE) for full details.