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
25 changes: 25 additions & 0 deletions infrastructure/aws/iam/cert_manager/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 44 additions & 22 deletions infrastructure/aws/iam/cert_manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,25 @@

## Description

Creates an IAM role and policy for cert-manager on EKS, enabling DNS01 ACME challenge validation via Route53 hosted zones using IRSA
Creates an IAM role and policy for cert-manager on EKS, enabling DNS01 ACME challenge validation via Route53. Supports both IRSA (OIDC federation) and EKS Pod Identity as the identity mechanism.

## Architecture

An aws_iam_policy is created granting Route53 permissions (GetChange, ChangeResourceRecordSets, ListResourceRecordSets, ListHostedZonesByName) scoped to the provided public and/or private hosted zone ARNs. The terraform-aws-modules/iam iam-role-for-service-accounts module creates an aws_iam_role with an OIDC trust policy bound to the cert-manager Kubernetes service account in the cert-manager namespace. The OIDC provider ARN and cluster name flow into the role naming and trust relationship, while the hosted zone IDs are dynamically filtered and converted to ARNs via a local for expression. The role ARN is exposed as an output for use by the cert-manager Helm release or Kubernetes service account annotation.
An aws_iam_policy is created granting Route53 permissions (GetChange, ChangeResourceRecordSets, ListResourceRecordSets, ListHostedZonesByName) scoped to the provided public and/or private hosted zone ARNs. The `identity_mode` variable selects the authentication mechanism: in `irsa` mode the terraform-aws-modules/iam community module creates an aws_iam_role with an OIDC trust policy; in `pod_identity` mode a native aws_iam_role is created with a trust policy for `pods.eks.amazonaws.com` and an `aws_eks_pod_identity_association` binds it to the cert-manager service account. The role ARN is exposed as an output in both modes.

## Features

- Creates an IAM role with OIDC trust scoped to the cert-manager Kubernetes service account via IRSA
- Supports IRSA (OIDC) and EKS Pod Identity via `identity_mode` variable (default: `irsa`)
- Creates an IAM policy granting Route53 permissions required for DNS01 ACME challenge validation
- Supports both public and private Route53 hosted zones with dynamic ARN construction
- Enforces that at least one of public or private hosted zone IDs is provided via input validation
- Scopes Route53 ChangeResourceRecordSets and ListResourceRecordSets permissions to only the specified hosted zones
- Outputs the cert-manager IAM role ARN for use in service account annotations or Helm chart values
- Outputs the cert-manager IAM role ARN in both identity modes

## Basic Usage

### IRSA (default)

```hcl
module "cert_manager" {
source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/cert_manager?ref=v5.2.0"
Expand All @@ -28,6 +30,17 @@ module "cert_manager" {
}
```

### EKS Pod Identity

```hcl
module "cert_manager" {
source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/aws/iam/cert_manager?ref=v5.2.0"

cluster_name = "your-cluster-name"
identity_mode = "pod_identity"
}
```

## Using Outputs

```hcl
Expand All @@ -43,69 +56,78 @@ resource "example_resource" "this" {
## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | n/a |
| ---- | ------- |
| <a name="provider_aws"></a> [aws](#provider\_aws) | 6.52.0 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| ---- | ------ | ------- |
| <a name="module_nullplatform_cert_manager_role"></a> [nullplatform\_cert\_manager\_role](#module\_nullplatform\_cert\_manager\_role) | terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts | n/a |

## Resources

| Name | Type |
|------|------|
| ---- | ---- |
| [aws_eks_pod_identity_association.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eks_pod_identity_association) | resource |
| [aws_iam_policy.nullplatform_cert_manager_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
| [aws_iam_role.pod_identity](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy_attachment.pod_identity](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_aws_iam_openid_connect_provider_arn"></a> [aws\_iam\_openid\_connect\_provider\_arn](#input\_aws\_iam\_openid\_connect\_provider\_arn) | ARN of the AWS IAM OIDC provider for EKS service account authentication | `string` | n/a | yes |
| ---- | ----------- | ---- | ------- | :------: |
| <a name="input_aws_iam_openid_connect_provider_arn"></a> [aws\_iam\_openid\_connect\_provider\_arn](#input\_aws\_iam\_openid\_connect\_provider\_arn) | ARN of the AWS IAM OIDC provider. Required when identity\_mode is 'irsa'; ignored when identity\_mode is 'pod\_identity'. | `string` | `null` | no |
| <a name="input_cluster_name"></a> [cluster\_name](#input\_cluster\_name) | Name of the cluster where the policy runs | `string` | n/a | yes |
| <a name="input_hosted_zone_private_id"></a> [hosted\_zone\_private\_id](#input\_hosted\_zone\_private\_id) | ID of the private Route53 hosted zone for DNS validation. Set to null or an empty string to omit it from the IAM policy. At least one of hosted\_zone\_public\_id or hosted\_zone\_private\_id must be provided. | `string` | `null` | no |
| <a name="input_hosted_zone_public_id"></a> [hosted\_zone\_public\_id](#input\_hosted\_zone\_public\_id) | ID of the public Route53 hosted zone for DNS validation. Set to null or an empty string to omit it from the IAM policy. At least one of hosted\_zone\_public\_id or hosted\_zone\_private\_id must be provided. | `string` | `null` | no |
| <a name="input_identity_mode"></a> [identity\_mode](#input\_identity\_mode) | IAM identity mode: 'irsa' uses OIDC federation via the community iam-role-for-service-accounts module; 'pod\_identity' creates a native IAM role trusted by pods.eks.amazonaws.com with an EKS Pod Identity association. Default 'irsa' is backward compatible with v4.5.x — no state changes required on upgrade. Note: switching between modes on an existing deployment replaces the IAM role; cert-manager will lose permissions during the transition until apply completes. | `string` | `"irsa"` | no |

## Outputs

| Name | Description |
|------|-------------|
| ---- | ----------- |
| <a name="output_nullplatform_cert_manager_role_arn"></a> [nullplatform\_cert\_manager\_role\_arn](#output\_nullplatform\_cert\_manager\_role\_arn) | ARN of the cert-manager role |
<!-- END_TF_DOCS -->

<!-- BEGIN_AI_METADATA
{
"name": "cert_manager",
"description": "Creates an IAM role and policy for cert-manager on EKS, enabling DNS01 ACME challenge validation via Route53 hosted zones using IRSA",
"architecture": "An aws_iam_policy is created granting Route53 permissions (GetChange, ChangeResourceRecordSets, ListResourceRecordSets, ListHostedZonesByName) scoped to the provided public and/or private hosted zone ARNs. The terraform-aws-modules/iam iam-role-for-service-accounts module creates an aws_iam_role with an OIDC trust policy bound to the cert-manager Kubernetes service account in the cert-manager namespace. The OIDC provider ARN and cluster name flow into the role naming and trust relationship, while the hosted zone IDs are dynamically filtered and converted to ARNs via a local for expression. The role ARN is exposed as an output for use by the cert-manager Helm release or Kubernetes service account annotation.",
"description": "Creates an IAM role and policy for cert-manager on EKS, enabling DNS01 ACME challenge validation via Route53. Supports IRSA and EKS Pod Identity via identity_mode variable.",
"architecture": "An aws_iam_policy is created granting Route53 permissions scoped to the provided hosted zone ARNs. The identity_mode variable selects the authentication mechanism: 'irsa' uses the terraform-aws-modules/iam community module to create a role with OIDC trust; 'pod_identity' creates a native IAM role trusted by pods.eks.amazonaws.com and an aws_eks_pod_identity_association binding it to the cert-manager service account. The role ARN is exposed as an output in both modes.",
"features": [
"Creates an IAM role with OIDC trust scoped to the cert-manager Kubernetes service account via IRSA",
"Supports IRSA (OIDC) and EKS Pod Identity via identity_mode variable (default: irsa)",
"Creates an IAM policy granting Route53 permissions required for DNS01 ACME challenge validation",
"Supports both public and private Route53 hosted zones with dynamic ARN construction",
"Enforces that at least one of public or private hosted zone IDs is provided via input validation",
"Scopes Route53 ChangeResourceRecordSets and ListResourceRecordSets permissions to only the specified hosted zones",
"Outputs the cert-manager IAM role ARN for use in service account annotations or Helm chart values"
"Outputs the cert-manager IAM role ARN in both identity modes"
],
"inputs": [
{
"name": "aws_iam_openid_connect_provider_arn",
"description": "ARN of the AWS IAM OIDC provider for EKS service account authentication",
"required": true
},
{
"name": "cluster_name",
"description": "Name of the cluster where the policy runs",
"required": true
},
{
"name": "aws_iam_openid_connect_provider_arn",
"description": "ARN of the AWS IAM OIDC provider. Required when identity_mode is 'irsa'; ignored when identity_mode is 'pod_identity'.",
"required": false
},
{
"name": "identity_mode",
"description": "IAM identity mode: 'irsa' or 'pod_identity'. Default: irsa.",
"required": false
},
{
"name": "hosted_zone_public_id",
"description": "ID of the public Route53 hosted zone for DNS validation. Set to null or an empty string to omit it from the IAM policy. At least one of hosted_zone_public_id or hosted_zone_private_id must be provided.",
"description": "ID of the public Route53 hosted zone for DNS validation.",
"required": false
},
{
"name": "hosted_zone_private_id",
"description": "ID of the private Route53 hosted zone for DNS validation. Set to null or an empty string to omit it from the IAM policy. At least one of hosted_zone_public_id or hosted_zone_private_id must be provided.",
"description": "ID of the private Route53 hosted zone for DNS validation.",
"required": false
}
],
Expand Down
48 changes: 47 additions & 1 deletion infrastructure/aws/iam/cert_manager/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ locals {
"arn:aws:route53:::hostedzone/${id}"
if id != null && id != ""
]

cert_manager_service_accounts = [
{ namespace = "cert-manager", service_account = "cert-manager" }
]
}

# Create IAM role with OIDC provider trust for Kubernetes service account
# IRSA: OIDC federation via community module
module "nullplatform_cert_manager_role" {
count = var.identity_mode == "irsa" ? 1 : 0
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts"
name = "nullplatform-${var.cluster_name}-cert-manager-role"
use_name_prefix = false
Expand All @@ -24,6 +29,47 @@ module "nullplatform_cert_manager_role" {
}
}

# Pod Identity: native IAM role trusted by the EKS Pod Identity agent
resource "aws_iam_role" "pod_identity" {
count = var.identity_mode == "pod_identity" ? 1 : 0
name = "nullplatform-${var.cluster_name}-cert-manager-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "pods.eks.amazonaws.com" }
Action = ["sts:AssumeRole", "sts:TagSession"]
}]
})
}

resource "aws_iam_role_policy_attachment" "pod_identity" {
count = var.identity_mode == "pod_identity" ? 1 : 0
role = one(aws_iam_role.pod_identity[*].name)
policy_arn = aws_iam_policy.nullplatform_cert_manager_policy.arn
}

resource "aws_eks_pod_identity_association" "this" {
for_each = var.identity_mode == "pod_identity" ? {
for sa in local.cert_manager_service_accounts :
"${sa.namespace}:${sa.service_account}" => sa
} : {}

cluster_name = var.cluster_name
namespace = each.value.namespace
service_account = each.value.service_account
role_arn = one(aws_iam_role.pod_identity[*].arn)
}

# Backward-compat (IRSA path only): count was added to this module call in v4.6.0;
# consumers upgrading from a prior version have state at the un-indexed address.
# Pod Identity deployments are new in v4.6.0 and have no prior state to migrate.
moved {
from = module.nullplatform_cert_manager_role
to = module.nullplatform_cert_manager_role[0]
}

# Grant permissions to manage Route 53 DNS records for DNS01 challenge
resource "aws_iam_policy" "nullplatform_cert_manager_policy" {
name = "nullplatform-${var.cluster_name}-cert-manager-policy"
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/aws/iam/cert_manager/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
output "nullplatform_cert_manager_role_arn" {
description = "ARN of the cert-manager role"
value = module.nullplatform_cert_manager_role.arn
value = var.identity_mode == "irsa" ? one(module.nullplatform_cert_manager_role[*].arn) : one(aws_iam_role.pod_identity[*].arn)
}
25 changes: 25 additions & 0 deletions infrastructure/aws/iam/cert_manager/tests/cert_manager.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,28 @@ run "rejects_when_both_zones_empty_strings" {

expect_failures = [var.hosted_zone_public_id]
}

run "irsa_mode_creates_module_and_no_pod_identity_resources" {
command = plan

assert {
condition = length(module.nullplatform_cert_manager_role) == 1
error_message = "IRSA mode must create exactly one module instance"
}
assert {
condition = length(aws_iam_role.pod_identity) == 0
error_message = "IRSA mode must not create a pod_identity IAM role"
}
assert {
condition = length(aws_eks_pod_identity_association.this) == 0
error_message = "IRSA mode must not create any Pod Identity associations"
}
assert {
condition = length(aws_iam_role_policy_attachment.pod_identity) == 0
error_message = "IRSA mode must not create a pod_identity policy attachment"
}
assert {
condition = output.nullplatform_cert_manager_role_arn != null
error_message = "IRSA mode must produce a non-null role ARN output"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
mock_provider "aws" {
override_resource {
target = aws_iam_policy.nullplatform_cert_manager_policy
values = {
arn = "arn:aws:iam::123456789012:policy/nullplatform-test-cluster-cert-manager-policy"
}
}
override_resource {
target = aws_iam_role.pod_identity
values = {
arn = "arn:aws:iam::123456789012:role/nullplatform-test-cluster-cert-manager-role"
}
}
}

variables {
cluster_name = "test-cluster"
hosted_zone_private_id = "Z0987654321DEF"
identity_mode = "pod_identity"
}

run "creates_pod_identity_role" {
command = plan

assert {
condition = length(aws_iam_role.pod_identity) == 1
error_message = "Pod Identity mode should create exactly one native IAM role"
}
assert {
condition = strcontains(aws_iam_role.pod_identity[0].assume_role_policy, "pods.eks.amazonaws.com")
error_message = "Pod Identity role trust must use the pods.eks.amazonaws.com principal"
}
assert {
condition = strcontains(aws_iam_role.pod_identity[0].assume_role_policy, "sts:TagSession")
error_message = "Pod Identity role trust must allow sts:TagSession"
}
assert {
condition = length(aws_iam_role_policy_attachment.pod_identity) == 1
error_message = "Pod Identity mode must create exactly one policy attachment"
}
assert {
condition = aws_iam_role_policy_attachment.pod_identity[0].role == "nullplatform-test-cluster-cert-manager-role"
error_message = "Policy attachment must reference the Pod Identity role"
}
}

run "creates_single_association" {
command = plan

assert {
condition = length(aws_eks_pod_identity_association.this) == 1
error_message = "cert-manager should create exactly one Pod Identity association"
}
assert {
condition = aws_eks_pod_identity_association.this["cert-manager:cert-manager"].service_account == "cert-manager"
error_message = "Association must target the cert-manager service account"
}
assert {
condition = aws_eks_pod_identity_association.this["cert-manager:cert-manager"].namespace == "cert-manager"
error_message = "Association must target the cert-manager namespace"
}
}

run "pod_identity_mode_does_not_create_irsa_module" {
command = plan

assert {
condition = length(module.nullplatform_cert_manager_role) == 0
error_message = "pod_identity mode must not instantiate the IRSA community module"
}
assert {
condition = output.nullplatform_cert_manager_role_arn == "arn:aws:iam::123456789012:role/nullplatform-test-cluster-cert-manager-role"
error_message = "pod_identity mode must output the Pod Identity role ARN"
}
}

run "pod_identity_does_not_require_oidc_arn" {
command = plan

variables {
aws_iam_openid_connect_provider_arn = null
}

assert {
condition = length(aws_iam_role.pod_identity) == 1
error_message = "Pod Identity mode must succeed without aws_iam_openid_connect_provider_arn"
}
}

run "rejects_invalid_identity_mode" {
command = plan

variables {
identity_mode = "wireguard"
}

expect_failures = [var.identity_mode]
}
Loading