The environment is the entry point. The modules do the work.
Part 1 deployed the environment root. Here we follow its implementation. Understanding where each component is declared lets a team adapt the platform without assembling it again by hand.
In environments/dev/main.tf, the shared module receives the environment inputs. This abbreviated excerpt shows the pattern:
module "eks_karpenter" {
source = "../../modules/eks-karpenter"
region = var.region
env = var.environment
cluster_name = var.cluster_name
hosted_zone_id = var.hosted_zone_id
domain_name = var.domain_name
# Network, access, and certificate inputs follow in the file.
}
The module combines AWS resources, helm_release, and kubectl_manifest. YAML inside Terraform’s yaml_body, or rendered through templatefile, is still Terraform-managed platform configuration.
| File under modules/eks-karpenter | Responsibility |
|---|---|
main.tf | VPC, EKS, managed nodes, Karpenter release, NodePool, EC2NodeClass. |
aws-lbc.tf | Load balancer controller, IAM, and Pod Identity. |
external-dns.tf | ExternalDNS release, DNS ownership, and AWS access. |
traefik.tf and its values template | Traefik, cert-manager, issuers, Certificate, and TLSStore. |
argocd.tf and its values template | ArgoCD installation and ingress. |
image-updater.tf and its values template | Updater installation and ECR authentication. |
1. EKS and workload capacity.
main.tf creates the VPC and calls the EKS module. The managed node group uses Bottlerocket, a controller label, and a CriticalAddonsOnly taint. Karpenter’s Helm values target that controller label.
module "karpenter" {
source = "terraform-aws-modules/eks/aws//modules/karpenter"
version = "20.37.2"
cluster_name = module.eks.cluster_name
create_pod_identity_association = true
# Additional node-role policies follow in the source.
}
The Karpenter Helm release consumes the generated service-account name, EKS endpoint, and interruption queue. The EC2NodeClass consumes the node IAM role and discovers subnets/security groups through tags. The NodePool references that class. Those connections are already expressed in the code.
The NodePool configuration lives inside kubectl_manifest.karpenter_node_pool. Edit that Terraform resource to change instance constraints, CPU limits, or consolidation. The source selects c/m/r categories, certain vCPU sizes, Nitro, and amd64, with a CPU limit of 1000. Choose a deliberate pilot capacity limit and explicit purchase policy.
The 20% disruption budget names Empty and Drifted while the policy also permits underutilized consolidation. Review the intended coverage using the NodePool reference and disruption guide. Do not create a second competing NodePool with kubectl to make a lasting platform change.
2. Ingress and DNS use the same environment inputs.
aws-lbc.tf and external-dns.tf declare their controllers, IAM roles, and Pod Identity associations. Traefik’s Helm release then renders its values from your configuration:
values = [
templatefile("${path.module}/values/traefik-values.yaml.tpl", {
allowed_ips = join(",", local.allowed_ips)
domain_name = var.domain_name
subject_alternative_names = var.subject_alternative_names
})
]
depends_on = [
helm_release.aws_lbc,
helm_release.external_dns
]
This is an excerpt from the release in traefik.tf. The template requests an internet-facing NLB with instance targets and adds the wildcard hostname annotation to Traefik’s Service. ExternalDNS uses that annotation to manage the record; Traefik handles HTTP routing behind the NLB.
ExternalDNS’s domain filter, TXT registry, owner ID, and upsert-only policy are declared in external-dns.tf. Scope its IAM policy to the intended zone and plan record cleanup separately. A controller filter does not replace an AWS permission boundary.
Review public exposure, default ingress-class behavior, cross-namespace references, and the backend TLS exception in the Traefik template for your company. Terraform dependencies order resource operations; they do not replace runtime readiness checks.
3. Terraform declares the certificate pipeline.
traefik.tf installs cert-manager, supplies AWS identity, and declares the issuers. The wildcard Certificate uses the Route53 DNS-01 issuer, and Traefik’s TLSStore uses the resulting Secret:
Terraform declarations:
helm_release.cert_manager
aws_eks_pod_identity_association.cert_manager
kubectl_manifest.cert_manager_dns_issuer
kubectl_manifest.traefik-default-certificate-tls
kubectl_manifest.traefik-default-tls
Runtime:
ClusterIssuer/cert-manager-acme-route53-issuer
Certificate/kube-system/wildcard-cert
Secret/kube-system/wildcard-cert
TLSStore/kube-system/default
Terraform manages the Certificate declaration; cert-manager creates and renews its Secret. Your domain, wildcard, and email flow in from variables. There is no separate manual certificate installation in the normal workflow.
For issuer or permission changes, edit the relevant Terraform resource. Use a separate staging issuer/account for issuance rehearsals. The Route53 solver documentation explains the authentication requirements.
4. ArgoCD and storage are included.
argocd.tf installs ArgoCD through Helm, renders its domain configuration, and creates its Traefik ingress and redirect middleware. Its ClusterIP Service sits behind Traefik. Part 3 starts with this installation; it does not install another copy.
image-updater.tf adds the updater release, ECR read permissions, and Pod Identity. Its template keeps the AWS account and region dynamic. Application-specific image selection and write-back behavior still need configuration.
The development root also connects the KMS key and EBS CSI modules through outputs:
# Selected arguments in module "ebs_csi"
ebs_csi_kms = module.kms-key.key_arn
create_default_storage_class = true
cluster_endpoint = module.eks_karpenter.cluster_endpoint
cluster_certificate_authority_data = module.eks_karpenter.cluster_certificate_authority_data
The repository contains an EFS CSI module too; the environment’s module calls determine which integrations it actually deploys.
5. Change the source, then plan the environment.
Use variables for inputs the module exposes. Edit the owning resource or template for settings embedded in the implementation. When a setting must differ by environment, expose it as a module variable and pass it from each root.
terraform fmt -recursive .
terraform -chdir=environments/dev validate
terraform -chdir=environments/dev plan
terraform -chdir=environments/dev apply
Inspect the plan: a chart-value update, node-class change, and network replacement have different effects. Avoid a manual Helm upgrade for a Terraform-owned release. Reconcile emergency live changes back into the source so subsequent applies preserve the intended state.
6. Verify with kubectl; maintain with Terraform.
terraform -chdir=environments/dev state list
helm list -A
kubectl get nodepool default
kubectl describe ec2nodeclass default
kubectl -n kube-system get svc traefik
kubectl -n kube-system logs deployment/external-dns --tail=80
kubectl -n kube-system describe certificate wildcard-cert
kubectl -n argocd get pods
Check controller health, discovered node-class resources, the NLB hostname, DNS, and certificate readiness. For a failure, inspect events/logs, find the responsible source file above, and correct the configuration there.
The optional platform-check manifest is a small application for a pilot endpoint check. Adapt its hostname and image, create its dedicated namespace, and apply only that workload. It does not install infrastructure. For the normal GitOps path, use application YAML through ArgoCD as described next.