Skip to content

Instantly share code, notes, and snippets.

@dthtvwls
Created April 18, 2025 13:47
Show Gist options
  • Select an option

  • Save dthtvwls/a16855029953d6da259e1496d61df3a9 to your computer and use it in GitHub Desktop.

Select an option

Save dthtvwls/a16855029953d6da259e1496d61df3a9 to your computer and use it in GitHub Desktop.
Fully automated, GitOps-driven CI/CD pipeline to deploy a sample microservice application to an AWS EKS cluster.
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
*.tfplan
# Crash log files
crash.log
# Exclude all .tfvars files, which are likely to contain sentitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/hashicorp/aws" {
version = "5.94.1"
constraints = "~> 5.94.1"
hashes = [
"h1:GFhSbVTuiLgDi1RTXMMZd6HGtU9wSCjHd7VkIZDOiMo=",
"zh:2cdc129ba213e949e48ec8edd43b288a403de879a953fdf65b5a261d06e0a41b",
"zh:30729876ada83b3a87863097adcf43ba0f523adf0c123b64d6b07854252e3fa1",
"zh:3d3b0a09fce307848871f145a89edf26ab8dc6138d03bbb6effa280e904a7590",
"zh:4751905b38328d1358343adb1ca4de375cb5d04444b1b9ca65ede5f8673e99ea",
"zh:90f679d2c9ef92307b93e345f8617e2b8901d71b3843fd463e2206e632060d55",
"zh:a6afd6da08b27630e5d723983de2e5d72f559a4acd2cbe5d4f52c9d054958e02",
"zh:b718c6f1cfc19e61104f0c058e173db6a3720a7ffca226d2d0a16d899845d27d",
"zh:b738cfde4bbf22446d8fc0f1952f754ed3996c1a13395fc2d17c4fd904e9c110",
"zh:e0c0b8166abc935e30aaf834091424b710d1ce7b18d2da2e4619ee70f065904e",
"zh:eaf138b103328edf5317e64af3013e25ba337d20fa1854c2a4199fcb2a13cbf0",
]
}
provider "registry.opentofu.org/hashicorp/cloudinit" {
version = "2.3.6"
constraints = ">= 2.0.0"
hashes = [
"h1:QOs7/ygYQFnXvh4vVux/WBmHXYXOwzBQgVNOUr+EfgE=",
"zh:0ad497b645e83bc82fac2fc6dbf63ff4b465dd18e9b0265b1852462e12a8cd4c",
"zh:10666a26ba19fdccdaf011f6b0c10ad9b80182afd1444ab28f669739a06d19a0",
"zh:57de6a85287c081647fdc57096c164b0f84ca36327aabaee66c425547117ac13",
"zh:57f7dc71c6fb22ad6b42fdcfdf8e1fa14ddb5cf0185dfd292ce73e02f050480d",
"zh:631ee274a22afcaa46b792f0cac56e8ea8894cbeb60f1119ae421b9c263f7b32",
"zh:6fd78c16cca8ec454a7c9a1b76d08617d17af5dd2bfb546e30da8940539d3aa7",
"zh:843f882ed06b9cb77fef3bd178b493117d7121d99c8eb36bc81e5e11b6979606",
"zh:a18e078db3c49264a45dfedbe99e8b3cb170fa7b8a9b253a176e9e96f1da8f9f",
"zh:a9aa2a5a3300254b27385c265201aece8bf1406d7229da4b4225263070a3c809",
"zh:b6225ae80101db018d4b372c6dc776412c45607ed859eb9bcd7e8589cc2b9393",
]
}
provider "registry.opentofu.org/hashicorp/helm" {
version = "2.17.0"
hashes = [
"h1:ShIag7wqd5Rs+zYpVMpjAh+T0ozr4XGYfSTKWqceQBY=",
"zh:02690815e35131a42cb9851f63a3369c216af30ad093d05b39001d43da04b56b",
"zh:27a62f12b29926387f4d71aeeee9f7ffa0ccb81a1b6066ee895716ad050d1b7a",
"zh:2d0a5babfa73604b3fefc9dab9c87f91c77fce756c2e32b294e9f1290aed26c0",
"zh:3976400ceba6dda4636e1d297e3097e1831de5628afa534a166de98a70d1dcbe",
"zh:54440ef14f342b41d75c1aded7487bfcc3f76322b75894235b47b7e89ac4bfa4",
"zh:6512e2ab9f2fa31cbb90d9249647b5c5798f62eb1215ec44da2cdaa24e38ad25",
"zh:795f327ca0b8c5368af0ed03d5d4f6da7260692b4b3ca0bd004ed542e683464d",
"zh:ba659e1d94f224bc3f1fd34cbb9d2663e3a8e734108e5a58eb49eda84b140978",
"zh:c5c8575c4458835c2acbc3d1ed5570589b14baa2525d8fbd04295c097caf41eb",
"zh:e0877a5dac3de138e61eefa26b2f5a13305a17259779465899880f70e11314e0",
]
}
provider "registry.opentofu.org/hashicorp/kubernetes" {
version = "2.36.0"
constraints = "~> 2.36.0"
hashes = [
"h1:O/JNwmUFXK8wQ6OEL5lH7akEYt0K5B1ClLpW0/1DvZE=",
"zh:15e42a6f8c6be77e0266edfb3765065c01e8fbcc8d75c794f1442f39cd9e6cfb",
"zh:2387195e7c80bc3ba93162c5c755482828b309a275d170e0c329145894dc6cb2",
"zh:27775d6f0b53a76c3f91689e553461a10ab2e07f7066ccb7cc0dce2cb8abf72f",
"zh:28e77e1dfafe5881bc28bec76e22735e4be2dcde468a8f3f08f32a52c6f1938e",
"zh:89b6571d33b6607499683b7b1fdfb95576ed08ab80539537c5460de1455df424",
"zh:91cdad0f3a5cb4a82669703f6f017b07e434748cff9b8991432043c733ae50a7",
"zh:a566611bc563c4e8ac843f6b6b868cf8dce2eccb908bd7605040b553983dc5e3",
"zh:d4efb26732e5fbd4e6a5e4c10eea62628d669c9c76e52c6485417c1e0a60fd99",
"zh:e87d8edc93920b75b88e7fb8d1a7941f2b7facc89931b72e4c74fcf137baee91",
"zh:f173c9c66edb046715db10adce1d603fe989a3634b26bb81d556de3de548bd94",
]
}
provider "registry.opentofu.org/hashicorp/null" {
version = "3.2.3"
constraints = ">= 3.0.0"
hashes = [
"h1:LN7WjQlMDIYGsXlum1kvMk5M8XzS2gzPTHmbEkxB6B0=",
"zh:1d57d25084effd3fdfd902eca00020b34b1fb020253b84d7dd471301606015ac",
"zh:65b7f9799b88464d9c2ec529713b7f52ea744275b61a8dc86cdedab1b2dcb933",
"zh:80d3e9c95b7b4ae7c54005cd127cae82e5c53d2b7023ef24c147337bac9dadd9",
"zh:841b60c07683e4bf456799ccd718896fdafdcc2c49252ae09967f2e74d8c8a03",
"zh:8fa1c592a9c78222e35713c6edb3f1f818a4c6f3524a30a209f0a7e919827b68",
"zh:bb795cc1429e09466840c09d39a28edf1db5070b1ec76822fc1173906a264572",
"zh:da1784818a89bea29dfe660632f0060a7a843e4e564d74435fbeca002b0f7d2a",
"zh:f409bf21b1cdaa6dac47cd79806f3d93f67e9507fe4dbf33b0165335f53bc2e1",
"zh:fbea7a1ff84b430ba9594698e93196d81d03e4036de3d1cafccb2a96d5b38581",
"zh:fbf0c84663a7e85881388d7d71ac862184f05fbf2d17ecf76bc5d3d7503ea260",
]
}
provider "registry.opentofu.org/hashicorp/time" {
version = "0.13.0"
constraints = ">= 0.9.0"
hashes = [
"h1:71sfVEFZDvqNdiprfsfOfBbpdyqoszvoOtdeuHznEYE=",
"zh:0e0a5f820793f13d8553742fac6c234f76e65bd095703a81f8f9cad38361d6c6",
"zh:11f2d5b5076d17814986886164bd4a4ce6448129baa529c21f658e949687f06f",
"zh:1a59c8d7da0c8155a86dffb1716bb5f2884b221a13167d5e7bcffb2ac192ba3f",
"zh:1e7abd01ef573294c0f2f1e2b30190c05a98afb7815d7a309fc10193bff4b4dd",
"zh:3ca53edfae9adffe1ee9c040e83b076fde89d73e7b2e6dc2de19d408e3f52a40",
"zh:5beb2cd0abe5376ff5e76d4f93d20c4740b333c1abc5ca72083e1cc85ffb29dd",
"zh:a775d153ba932834401eb1d9151f462c1e49d47494152d282d3e6981b3e591c0",
"zh:aac6802f60bf708172f09ead7a13177ab6a47f5a3eca458e935f422ed409f4a6",
"zh:d4e5ad0041d752b94317093e8063e6b766982f647cfe3cc1a3f4a10930383357",
"zh:fef228471c7223a558a1b6054ec7e8837526dc9787ba9da5dc6fbfa1c762cd1b",
]
}
provider "registry.opentofu.org/hashicorp/tls" {
version = "4.0.6"
constraints = ">= 3.0.0"
hashes = [
"h1:0oXaBUFJ5bA0ED7OCajAOa9YSGTTpe8FyUFJP+zO6A4=",
"zh:4b53b372767e5068d9bbfc89199201c1ae4283dde2f0c301974f8abb4215791f",
"zh:5b4c308bd074c6d0bd560220e6ee10a9859ca9a1f29a59367b0477a740ff265e",
"zh:674dd6bc85597677e160ee601d88b21c5a974759a658769812d2904bd94bc042",
"zh:6ccc1c448349b56677ba66112aec7e0a58eb827f66209ca5f4077b81cce240fb",
"zh:8aa6e13a5d722b74230937ea21e8b4994e53340d95b5691cf6cf3518b9f38e6e",
"zh:8b27e55e4c7fa887774860113b95c8f7f68804b002fa47f0eb8e3a485997287e",
"zh:a430b5a3e8753d8f61784de49e538ac4abed19fb665fccd8a10b55402fe9f076",
"zh:b07c978c335ae9fc12f9c221629610775e4ae36691ed4e7ba258d275dd58a243",
"zh:bbec8cb1efc84ee3026c793956a4a4cd0ece20b89d2d4f7d954c68e7f6d596d0",
"zh:e684e247424188dc3b500a543b1a8046d1c0ec08c2a90aedca0c4f6bb56bedbd",
]
}

GitOps-Based Deployment Pipeline on AWS EKS

Fully automated, GitOps-driven CI/CD pipeline to deploy a sample microservice application to an AWS EKS cluster.

The solution is designed to be a minimal implementation of the requirements. Using Argo as suggested atop the hard requirements of EKS and terraform.

This should be considered a proof-of-concept, not a production-ready design. Please follow DevSecOps best practices when deploying anything to the public internet.

Prerequisites

An AWS account. With the exception of the IAM user described in the terraform setup section, all AWS resources are managed by terraform/opentofu and can be removed with terraform destroy/tofu destroy

To further cordon this demo from your other AWS resources, you can create an AWS sandbox account by following the instructions here: https://cheat.readthedocs.io/en/latest/aws/subaccounts.html

Ensure the following command-line tools are installed:

  • Terraform or OpenTofu
  • AWS CLI
  • kubectl
  • eksctl
  • helm
  • argocd with argo rollouts

You can install these via Homebrew: brew install opentofu awscli kubernetes-cli eksctl helm argocd argoproj/tap/kubectl-argo-rollouts

Getting Started

Set up terraform access to AWS

Manually bootstrap an IAM user for terraform in AWS. We will call ours "terraform" and attach the "arn:aws:iam::aws:policy/AdministratorAccess" policy.

Create an access key for the terraform user, with the "Command Line Interface" use case.

Configure the key's id and secret you received from the previous step (note: this will replace your default profile in ~/.aws/credentials; specify --profile argument if you don't want this, or edit the credentials file manually):

aws configure set aws_access_key_id "ABCDEFGHIJKLMNOP"
aws configure set aws_secret_access_key "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcd"

Apply terraform

You can now apply the terraform module:

tofu init
tofu apply

Open Argo

Update your ~/.kube/config, get the initial admin password, and forward the Argo UI port:

aws eks update-kubeconfig --region us-east-1 --name eks-gitops
kubectl -n argo-cd port-forward svc/argo-cd-argocd-server 8443:443

Get the initial password for the admin user: kubectl -n argo-cd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo

Log in on the command line (replace the asterisks by the password you just obtained): argocd login localhost:8443 --username admin --password "****************" --insecure

Open http://localhost:8443 to use the web UI.

GitOps Deployments

Fork this helm chart on GitHub: https://github.com/dthtvwls/helm-guestbook.

git clone https://github.com/*yourname*/helm-guestbook
argocd app create helm-guestbook \
  --sync-policy automated \
  --project default \
  --repo https://github.com/*yourname*/helm-guestbook \
  --path . \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace default
kubectl port-forward svc/helm-guestbook 8080:80

Open http://localhost:8080 to see the Guestbook app.

Push a change to your cloned repository: on values.yaml:9, change the value of tag to 0.2, commit the change and push to GitHub.

After a few minutes, observe the Guestbook app's page header at http://localhost:8080 change to "Fancy Guestbook". (You will need to re-establish the port forwarding.)

Blue-Green / Automated Rollback

Run kubectl argo rollouts dashboard and open http://localhost:3100 to use the Rollouts web app.

Fork this helm chart on GitHub: https://github.com/dthtvwls/helm-blue-green.

Clone the new fork and install the chart:

git clone https://github.com/*yourname*/helm-blue-green
helm install example helm-blue-green/
kubectl port-forward $(kubectl get pods --namespace default -l "app=helm-guestbook,release=example" -o jsonpath="{.items[0].metadata.name}") 8080:80

Open http://localhost:8080 to see the Guestbook app.

Next, helm upgrade example helm-blue-green/ --set image.tag=0.2

Now, two versions will exist in your cluster (and each one has an associated service). You can watch the new service cut over in the rollout: http://localhost:3100/rollouts/rollout/default/example-helm-guestbook

Re-establish the port forwarding to see "Fancy Guestbook", again.

Finally, upgrade the image tag to a version that doesn't exist: helm upgrade example helm-blue-green/ --set image.tag=0.3

The rollout will fail and revision 2 will remain in production.

module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "20.35.0"
cluster_name = var.module_name
cluster_version = "1.32"
cluster_endpoint_public_access = true
enable_cluster_creator_admin_permissions = true
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
eks_managed_node_groups = {
default = {
use_custom_launch_template = false
instance_types = ["m5a.large"]
min_size = 1
max_size = 1
desired_size = 1
}
}
}
provider "helm" {
kubernetes {
host = module.eks.cluster_endpoint
cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name]
}
}
}
resource "helm_release" "argo_cd" {
name = "argo-cd"
repository = "https://argoproj.github.io/argo-helm"
chart = "argo-cd"
version = "7.8.23"
namespace = "argo-cd"
create_namespace = true
}
resource "helm_release" "argo_rollouts" {
name = "argo-rollouts"
repository = "https://argoproj.github.io/argo-helm"
chart = "argo-rollouts"
version = "2.39.5"
namespace = "argo-rollouts"
create_namespace = true
}
/*
* Add the ability to admin kubernetes to the terraform IAM user.
* Please refer to README.md to set up the terraform user.
*/
data "aws_caller_identity" "current" {}
resource "aws_iam_role" "kubernetes_admin" {
name = "KubernetesAdmin"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
}
}
]
})
}
resource "aws_iam_role_policy" "eks_policy" {
name = "EKSPolicy"
role = aws_iam_role.kubernetes_admin.id
policy = jsonencode({
Statement = [
{
Action = "eks:DescribeCluster"
Effect = "Allow"
Resource = "arn:aws:eks:${var.region}:${data.aws_caller_identity.current.account_id}:cluster/${module.eks.cluster_name}"
}
]
})
}
resource "null_resource" "IAMIdentityMapping" {
depends_on = [module.eks, aws_iam_role_policy.eks_policy]
provisioner "local-exec" {
command = <<EOF
eksctl create iamidentitymapping \
--region ${var.region} \
--cluster ${module.eks.cluster_name} \
--arn ${aws_iam_role.kubernetes_admin.arn} \
--group system:masters \
--username admin
EOF
}
}
output "cluster_endpoint" {
description = "Endpoint for EKS control plane"
value = module.eks.cluster_endpoint
}
terraform {
required_version = "~> 1.9.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.94.1"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.36.0"
}
}
}
provider "aws" {
region = var.region
}
variable "module_name" {
default = "eks-gitops"
description = "GitOps-driven CI/CD pipeline to deploy a sample microservice application to an AWS EKS cluster."
}
variable "region" {
default = "us-east-1"
description = "AWS Region Code (See https://docs.aws.amazon.com/global-infrastructure/latest/regions/aws-regions.html)"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.19.0"
name = var.module_name
cidr = "10.0.0.0/16"
azs = slice(data.aws_availability_zones.azs.names, 0, 3)
private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
public_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
}
data "aws_availability_zones" "azs" {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment