Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ terraform validate
terraform test
```

You can also run the local aggregate target:

```bash
make check
```

For example changes, also run the same init and validate commands from the touched example directory.

Do not run `terraform apply`, `terraform destroy`, or destructive state commands unless the user explicitly asks for them and confirms the target environment.
61 changes: 61 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
SHELL := /bin/bash

EXAMPLES := $(wildcard examples/*)

.PHONY: help fmt fmt-check docs docs-check init validate test examples-init examples-validate check hooks

help:
@echo "Available targets:"
@echo " make fmt Format all Terraform files"
@echo " make fmt-check Check Terraform formatting"
@echo " make docs Generate README Terraform docs"
@echo " make docs-check Check README Terraform docs are current"
@echo " make init Initialize the root module"
@echo " make validate Validate the root module"
@echo " make test Run native Terraform tests"
@echo " make examples-init Initialize all examples"
@echo " make examples-validate Validate all examples"
@echo " make check Run local CI-style checks"
@echo " make hooks Enable repo-local Git hooks"

fmt:
terraform fmt -recursive

fmt-check:
terraform fmt -check -recursive

docs:
scripts/terraform-docs.sh

docs-check:
scripts/terraform-docs.sh --check

init:
terraform init -backend=false

validate:
terraform validate

test:
terraform test -no-color

examples-init:
@for example in $(EXAMPLES); do \
if [ -d "$$example" ]; then \
echo "Initializing $$example"; \
terraform -chdir="$$example" init -backend=false; \
fi; \
done

examples-validate:
@for example in $(EXAMPLES); do \
if [ -d "$$example" ]; then \
echo "Validating $$example"; \
terraform -chdir="$$example" validate; \
fi; \
done

check: fmt-check docs-check init validate test examples-init examples-validate

hooks:
git config core.hooksPath .githooks
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,20 @@ aws eks update-kubeconfig --name dev-eks

| Name | Description | Type | Default | Required |
| ---- | ----------- | ---- | ------- | :------: |
| <a name="input_addons"></a> [addons](#input\_addons) | EKS add-ons to install after the managed node groups are created. | <pre>map(object({<br/> configuration_values = optional(string)<br/> resolve_conflicts_on_create = optional(string, "OVERWRITE")<br/> resolve_conflicts_on_update = optional(string, "OVERWRITE")<br/> service_account_role_arn = optional(string)<br/> version = optional(string)<br/> }))</pre> | <pre>{<br/> "coredns": {},<br/> "kube-proxy": {},<br/> "vpc-cni": {}<br/>}</pre> | no |
| <a name="input_access_config"></a> [access\_config](#input\_access\_config) | Optional EKS access configuration for cluster authentication mode and creator admin permissions. | <pre>object({<br/> authentication_mode = optional(string)<br/> bootstrap_cluster_creator_admin_permissions = optional(bool)<br/> })</pre> | `null` | no |
| <a name="input_addons"></a> [addons](#input\_addons) | EKS add-ons to install after the managed node groups are created. | <pre>map(object({<br/> configuration_values = optional(string)<br/> pod_identity_associations = optional(list(object({<br/> role_arn = string<br/> service_account = string<br/> })), [])<br/> resolve_conflicts_on_create = optional(string, "OVERWRITE")<br/> resolve_conflicts_on_update = optional(string, "OVERWRITE")<br/> service_account_role_arn = optional(string)<br/> version = optional(string)<br/> }))</pre> | <pre>{<br/> "coredns": {},<br/> "kube-proxy": {},<br/> "vpc-cni": {}<br/>}</pre> | no |
| <a name="input_cloudwatch_log_group_kms_key_id"></a> [cloudwatch\_log\_group\_kms\_key\_id](#input\_cloudwatch\_log\_group\_kms\_key\_id) | Optional KMS key ID or ARN for encrypting the EKS control plane CloudWatch log group. | `string` | `null` | no |
| <a name="input_cloudwatch_log_retention_days"></a> [cloudwatch\_log\_retention\_days](#input\_cloudwatch\_log\_retention\_days) | Retention in days for the EKS control plane CloudWatch log group. | `number` | `30` | no |
| <a name="input_cluster_encryption_config"></a> [cluster\_encryption\_config](#input\_cluster\_encryption\_config) | Optional EKS encryption configuration for Kubernetes secrets using an existing KMS key. | <pre>object({<br/> provider_key_arn = string<br/> resources = optional(list(string), ["secrets"])<br/> })</pre> | `null` | no |
| <a name="input_cluster_name"></a> [cluster\_name](#input\_cluster\_name) | Optional EKS cluster name. When null, name is used as the cluster name. | `string` | `null` | no |
| <a name="input_cluster_security_group_ids"></a> [cluster\_security\_group\_ids](#input\_cluster\_security\_group\_ids) | Additional security group IDs to associate with the EKS control plane. | `list(string)` | `[]` | no |
| <a name="input_deletion_protection"></a> [deletion\_protection](#input\_deletion\_protection) | Whether to enable deletion protection for the EKS cluster. Leave null to use the AWS/provider default. | `bool` | `null` | no |
| <a name="input_enabled_cluster_log_types"></a> [enabled\_cluster\_log\_types](#input\_enabled\_cluster\_log\_types) | EKS control plane log types to enable. | `list(string)` | <pre>[<br/> "api",<br/> "audit",<br/> "authenticator"<br/>]</pre> | no |
| <a name="input_endpoint_private_access"></a> [endpoint\_private\_access](#input\_endpoint\_private\_access) | Whether the Kubernetes API server endpoint is reachable from within the VPC. | `bool` | `true` | no |
| <a name="input_endpoint_public_access"></a> [endpoint\_public\_access](#input\_endpoint\_public\_access) | Whether the Kubernetes API server endpoint is reachable from the public internet. | `bool` | `true` | no |
| <a name="input_kubernetes_version"></a> [kubernetes\_version](#input\_kubernetes\_version) | Kubernetes version for the EKS cluster and managed node groups. Leave null to use the current AWS default. | `string` | `null` | no |
| <a name="input_name"></a> [name](#input\_name) | Name prefix for module-created resources. Used as the EKS cluster name when cluster\_name is null. | `string` | n/a | yes |
| <a name="input_node_groups"></a> [node\_groups](#input\_node\_groups) | Managed node groups to create. | <pre>map(object({<br/> ami_type = optional(string)<br/> capacity_type = optional(string, "ON_DEMAND")<br/> desired_size = optional(number, 2)<br/> disk_size = optional(number, 20)<br/> instance_types = optional(list(string), ["t3.medium"])<br/> labels = optional(map(string), {})<br/> max_size = optional(number, 3)<br/> min_size = optional(number, 1)<br/> subnet_ids = optional(list(string), [])<br/> update_max_unavailable = optional(number, 1)<br/> taints = optional(list(object({<br/> effect = string<br/> key = string<br/> value = optional(string, "")<br/> })), [])<br/> }))</pre> | <pre>{<br/> "default": {}<br/>}</pre> | no |
| <a name="input_node_groups"></a> [node\_groups](#input\_node\_groups) | Managed node groups to create. | <pre>map(object({<br/> ami_type = optional(string)<br/> capacity_type = optional(string, "ON_DEMAND")<br/> desired_size = optional(number, 2)<br/> disk_size = optional(number, 20)<br/> instance_types = optional(list(string), ["t3.medium"])<br/> labels = optional(map(string), {})<br/> max_size = optional(number, 3)<br/> min_size = optional(number, 1)<br/> subnet_ids = optional(list(string), [])<br/> update_max_unavailable = optional(number, 1)<br/> node_repair_config = optional(object({<br/> enabled = optional(bool)<br/> max_parallel_nodes_repaired_count = optional(number)<br/> max_parallel_nodes_repaired_percentage = optional(number)<br/> max_unhealthy_node_threshold_count = optional(number)<br/> max_unhealthy_node_threshold_percentage = optional(number)<br/> overrides = optional(list(object({<br/> min_repair_wait_time_mins = number<br/> node_monitoring_condition = string<br/> node_unhealthy_reason = string<br/> repair_action = string<br/> })), [])<br/> }))<br/> taints = optional(list(object({<br/> effect = string<br/> key = string<br/> value = optional(string, "")<br/> })), [])<br/> }))</pre> | <pre>{<br/> "default": {}<br/>}</pre> | no |
| <a name="input_public_access_cidrs"></a> [public\_access\_cidrs](#input\_public\_access\_cidrs) | CIDR blocks that can access the public Kubernetes API endpoint. | `list(string)` | <pre>[<br/> "0.0.0.0/0"<br/>]</pre> | no |
| <a name="input_service_ipv4_cidr"></a> [service\_ipv4\_cidr](#input\_service\_ipv4\_cidr) | Optional Kubernetes service IPv4 CIDR. Set only when you need a non-default service CIDR. | `string` | `null` | no |
| <a name="input_subnet_ids"></a> [subnet\_ids](#input\_subnet\_ids) | Subnet IDs for the EKS control plane and default node groups. Use at least two subnets in different Availability Zones. | `list(string)` | n/a | yes |
Expand Down Expand Up @@ -137,6 +142,16 @@ terraform test

Pull requests run the same tests through `.github/workflows/terraform-pr.yml`, along with formatting, generated-docs, validation, and example checks.

## Local Development

Use the Makefile for common local checks:

```bash
make fmt
make docs
make check
```

## Notes

- The module does not create VPC, subnet, route table, NAT gateway, or security baseline resources.
Expand Down
4 changes: 4 additions & 0 deletions examples/advanced/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ More complete example for teams that want to tune the cluster while still supply
This example shows:

- Restricted public Kubernetes API access.
- Optional additional EKS control plane security groups.
- Access entry authentication mode configuration and deletion protection.
- Optional KMS encryption for Kubernetes secrets and control-plane logs.
- Full control-plane logging with configurable retention.
- Separate on-demand and spot managed node groups.
- Managed node group node repair configuration.
- Optional node-group subnet overrides.
- Separate module resource prefix and EKS cluster name through `name` and `cluster_name`.
- VPC CNI prefix delegation configuration.
Expand Down
20 changes: 14 additions & 6 deletions examples/advanced/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@ module "eks" {
kubernetes_version = var.kubernetes_version
subnet_ids = var.control_plane_subnet_ids

endpoint_private_access = true
endpoint_public_access = var.endpoint_public_access
public_access_cidrs = var.public_access_cidrs
cluster_security_group_ids = var.cluster_security_group_ids
endpoint_private_access = true
endpoint_public_access = var.endpoint_public_access
public_access_cidrs = var.public_access_cidrs

enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
cloudwatch_log_retention_days = var.cloudwatch_log_retention_days
service_ipv4_cidr = var.service_ipv4_cidr
access_config = var.access_config
deletion_protection = var.deletion_protection
cluster_encryption_config = var.cluster_encryption_config

enabled_cluster_log_types = ["api", "audit", "authenticator", "controllerManager", "scheduler"]
cloudwatch_log_retention_days = var.cloudwatch_log_retention_days
cloudwatch_log_group_kms_key_id = var.cloudwatch_log_group_kms_key_id
service_ipv4_cidr = var.service_ipv4_cidr

node_groups = {
system = {
Expand All @@ -38,6 +44,7 @@ module "eks" {
labels = { workload = "system" }
max_size = 4
min_size = 2
node_repair_config = var.node_repair_config
subnet_ids = local.node_group_subnet_ids
update_max_unavailable = 1
}
Expand All @@ -57,6 +64,7 @@ module "eks" {
}
max_size = 6
min_size = 0
node_repair_config = var.node_repair_config
subnet_ids = local.node_group_subnet_ids
update_max_unavailable = 1
taints = [
Expand Down
59 changes: 59 additions & 0 deletions examples/advanced/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ variable "control_plane_subnet_ids" {
type = list(string)
}

variable "cluster_security_group_ids" {
description = "Additional security group IDs to associate with the EKS control plane."
type = list(string)
default = []
}

variable "node_group_subnet_ids" {
description = "Optional subnet IDs for managed node groups. Defaults to control_plane_subnet_ids."
type = list(string)
Expand All @@ -45,18 +51,71 @@ variable "public_access_cidrs" {
default = ["203.0.113.10/32"]
}

variable "access_config" {
description = "Optional EKS access configuration for cluster authentication mode and creator admin permissions."
type = object({
authentication_mode = optional(string)
bootstrap_cluster_creator_admin_permissions = optional(bool)
})
default = {
authentication_mode = "API_AND_CONFIG_MAP"
bootstrap_cluster_creator_admin_permissions = true
}
}

variable "deletion_protection" {
description = "Whether to enable deletion protection for the EKS cluster."
type = bool
default = true
}

variable "cluster_encryption_config" {
description = "Optional EKS encryption configuration for Kubernetes secrets using an existing KMS key."
type = object({
provider_key_arn = string
resources = optional(list(string), ["secrets"])
})
default = null
}

variable "cloudwatch_log_retention_days" {
description = "Retention in days for EKS control plane logs."
type = number
default = 90
}

variable "cloudwatch_log_group_kms_key_id" {
description = "Optional KMS key ID or ARN for encrypting the EKS control plane CloudWatch log group."
type = string
default = null
}

variable "service_ipv4_cidr" {
description = "Optional Kubernetes service IPv4 CIDR."
type = string
default = null
}

variable "node_repair_config" {
description = "Optional node repair configuration applied to both example managed node groups."
type = object({
enabled = optional(bool)
max_parallel_nodes_repaired_count = optional(number)
max_parallel_nodes_repaired_percentage = optional(number)
max_unhealthy_node_threshold_count = optional(number)
max_unhealthy_node_threshold_percentage = optional(number)
overrides = optional(list(object({
min_repair_wait_time_mins = number
node_monitoring_condition = string
node_unhealthy_reason = string
repair_action = string
})), [])
})
default = {
enabled = true
}
}

variable "ebs_csi_driver_service_account_role_arn" {
description = "Optional IAM role ARN for the aws-ebs-csi-driver add-on. When null, the add-on is not installed by this example."
type = string
Expand Down
1 change: 1 addition & 0 deletions logs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ resource "aws_cloudwatch_log_group" "cluster" {
count = length(var.enabled_cluster_log_types) > 0 ? 1 : 0

name = "/aws/eks/${local.cluster_name}/cluster"
kms_key_id = var.cloudwatch_log_group_kms_key_id
retention_in_days = var.cloudwatch_log_retention_days
tags = local.common_tags
}
61 changes: 58 additions & 3 deletions main.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
resource "aws_eks_cluster" "this" {
name = local.cluster_name
role_arn = aws_iam_role.cluster.arn
version = var.kubernetes_version
name = local.cluster_name
role_arn = aws_iam_role.cluster.arn
version = var.kubernetes_version
deletion_protection = var.deletion_protection

enabled_cluster_log_types = var.enabled_cluster_log_types

Expand All @@ -10,6 +11,28 @@ resource "aws_eks_cluster" "this" {
endpoint_private_access = var.endpoint_private_access
endpoint_public_access = var.endpoint_public_access
public_access_cidrs = var.public_access_cidrs
security_group_ids = var.cluster_security_group_ids
}

dynamic "access_config" {
for_each = var.access_config == null ? [] : [var.access_config]

content {
authentication_mode = access_config.value.authentication_mode
bootstrap_cluster_creator_admin_permissions = access_config.value.bootstrap_cluster_creator_admin_permissions
}
}

dynamic "encryption_config" {
for_each = var.cluster_encryption_config == null ? [] : [var.cluster_encryption_config]

content {
resources = encryption_config.value.resources

provider {
key_arn = encryption_config.value.provider_key_arn
}
}
}

dynamic "kubernetes_network_config" {
Expand Down Expand Up @@ -53,6 +76,29 @@ resource "aws_eks_node_group" "this" {
max_unavailable = each.value.update_max_unavailable
}

dynamic "node_repair_config" {
for_each = each.value.node_repair_config == null ? [] : [each.value.node_repair_config]

content {
enabled = node_repair_config.value.enabled
max_parallel_nodes_repaired_count = node_repair_config.value.max_parallel_nodes_repaired_count
max_parallel_nodes_repaired_percentage = node_repair_config.value.max_parallel_nodes_repaired_percentage
max_unhealthy_node_threshold_count = node_repair_config.value.max_unhealthy_node_threshold_count
max_unhealthy_node_threshold_percentage = node_repair_config.value.max_unhealthy_node_threshold_percentage

dynamic "node_repair_config_overrides" {
for_each = node_repair_config.value.overrides

content {
min_repair_wait_time_mins = node_repair_config_overrides.value.min_repair_wait_time_mins
node_monitoring_condition = node_repair_config_overrides.value.node_monitoring_condition
node_unhealthy_reason = node_repair_config_overrides.value.node_unhealthy_reason
repair_action = node_repair_config_overrides.value.repair_action
}
}
}
}

dynamic "taint" {
for_each = each.value.taints

Expand Down Expand Up @@ -87,6 +133,15 @@ resource "aws_eks_addon" "this" {
service_account_role_arn = each.value.service_account_role_arn
tags = local.common_tags

dynamic "pod_identity_association" {
for_each = each.value.pod_identity_associations

content {
role_arn = pod_identity_association.value.role_arn
service_account = pod_identity_association.value.service_account
}
}

depends_on = [
aws_eks_node_group.this
]
Expand Down
Loading