Two pipelines. One cluster. Built in 72 hours.
Provisioned a production-grade Kubernetes cluster on AWS EKS with Terraform, deployed a personalized portfolio application via Helm, and automated the full deployment lifecycle with two independent CI/CD pipelines — Jenkins on main, GitHub Actions + ArgoCD on gitops. Built and running in under 72 hours.
Key architectural decision: The ALB is defined as an explicit Terraform resource rather than letting the Helm ALB Ingress Controller provision it dynamically. When the controller manages the ALB, it creates resources outside of Terraform's state — which means terraform destroy fails because it can't remove the VPC while the controller-managed ALB is still holding subnets. By owning the ALB in Terraform, the full infrastructure lifecycle runs cleanly from a single command.
Rather than deploying a generic Hello World, I built a personalized portfolio page that pulls live Kubernetes cluster data at request time using the Downward API. The pod injects POD_NAME, NODE_NAME, and NAMESPACE as environment variables — rendering real cluster data on every page load. If the pod reschedules to a different node, the page reflects it immediately.
Wrote five Terraform files provisioning a complete AWS environment: custom VPC with public/private subnets across three AZs, NAT gateway, EKS cluster with managed node groups, AWS ALB with listener and target group, IAM role for the ALB controller via OIDC, dedicated IAM policy for controller permissions, pre-destroy cleanup hook, and a Jenkins EC2 instance with full bootstrapping via user_data.
Packaged the application as a Helm chart with templated deployment, service, ingress, HPA, and service account resources. Configured Horizontal Pod Autoscaling to scale between 1 and 3 replicas at 50% CPU or memory utilization. Added Kubernetes Downward API env vars to the deployment template so each pod exposes its own metadata to the running application.
Jenkins runs on a dedicated EC2 instance provisioned by Terraform. A declarative Jenkinsfile defines four stages: Checkout, Build Docker Image, Push to ECR (tagged with Jenkins build number), and Deploy to EKS via helm upgrade --install. AWS credentials and kubeconfig are injected via the Jenkins credential store using the AWS Credentials and Kubernetes CLI plugins.
GitHub Actions handles CI on the gitops branch — building and pushing images tagged with the commit SHA, then updating values.yaml and committing back to the repo. ArgoCD watches the branch continuously and auto-syncs the cluster when it detects a change. Self-healing is enabled: if any resource drifts from what the repo declares, ArgoCD corrects it. Git is the source of truth.
Installed the AWS Load Balancer Controller into the EKS cluster via Helm. The controller uses an IAM role bound to its Kubernetes service account via OIDC federation — no static credentials required. The role is provisioned by Terraform using the iam-role-for-service-accounts-eks module, with a dedicated IAM policy granting the specific API permissions the controller needs.
A null_resource with a local-exec destroy provisioner runs automatically before Terraform removes the VPC. It updates kubeconfig, deletes all Kubernetes ingress resources, and waits 60 seconds for the controller-managed ALB to terminate — preventing the subnet dependency violations that plagued the first run. Full teardown runs from a single terraform destroy.
CREATE_FAILED — nodes joining cluster in NotReady state. The vpc-cni addon wasn't installed before nodes were provisioned, causing the aws-node pod to crash loop with ec2:DescribeNetworkInterfaces permission errors.
before_compute = true to the vpc-cni addon block, forcing addon installation before node provisioning. Also added AmazonEKS_CNI_Policy to iam_role_additional_policies in the node group so nodes have the required EC2 API permissions from the start.
terraform destroy failed with DependencyViolation on the first run. The ALB Ingress Controller had created an ALB dynamically — outside of Terraform's state — which was still holding subnets and the internet gateway when Terraform tried to tear down the VPC.
aws_lb, aws_security_group, aws_lb_listener, and aws_lb_target_group. Added a null_resource pre-destroy hook that deletes Kubernetes ingresses and waits 60 seconds before VPC teardown. Full lifecycle now runs clean.
AccessDenied on elasticloadbalancing:DescribeListenerAttributes. Ingress address never populated. This API call isn't covered by ElasticLoadBalancingFullAccess.
AWSLoadBalancerControllerIAMPolicy granting the specific permissions required and attached it to the load-balancer-controller IAM role. Both the policy and attachment are Terraform resources. Restarted the controller — ingress address populated within seconds.
-bash: git: command not found. The pipeline failed immediately at the SCM checkout stage.
sudo yum install -y git, then permanently added it to the user_data bootstrap script in jenkins.tf so future deployments never require this manual step.
metadata.annotations: Too long: must have at most 262144 bytes. Pods stuck in Pending after install — single t3.small node was at capacity with existing workloads.
--server-side --force-conflicts flags to bypass the annotation size limit. Scaled the node group to 2 nodes — the desired_size = 2 in Terraform is intentional, as ArgoCD requires the second t3.small node to run alongside the application.
| Jenkins | GitHub Actions + ArgoCD | |
|---|---|---|
| Deployment model | Push-based | Pull-based |
| Source of truth | Pipeline script | Git repository |
| Self-healing | No | Yes — ArgoCD corrects drift automatically |
| Audit trail | Build logs | Git commit history |
| Image tagging | Jenkins build number | Git commit SHA |
| Infrastructure needed | Jenkins EC2 server | ArgoCD in-cluster |
| Best for | Enterprise, complex pipelines | Cloud-native, GitOps workflows |