id: aws-eks-gitops-deploy
namespace: company.team
description: |
Deploy a containerized application to AWS EKS on every Git push: clone the
repository into a shared working directory, build the image with Kaniko (no
Docker daemon) and push it to Amazon ECR, apply the Kubernetes manifests,
wait for the rolling update to finish, and report the result to Discord.
If any step fails, capture pod status, logs and cluster events and alert
Discord with that evidence.
concurrency:
behavior: QUEUE
limit: 1
inputs:
- id: git_repo
type: STRING
defaults: "https://github.com/your-username/eks-gitops-deploy.git"
description: HTTPS URL of the Git repository to build and deploy. It must
contain a Dockerfile and Kubernetes manifests in k8s/.
- id: git_branch
type: STRING
defaults: "main"
description: Branch to check out, build and deploy.
- id: aws_region
type: STRING
defaults: "ap-south-2"
description: AWS region where the ECR repository and the EKS cluster live.
- id: ecr_registry
type: STRING
defaults: "your-account-id.dkr.ecr.ap-south-2.amazonaws.com"
description: ECR registry host, in the form
<account-id>.dkr.ecr.<region>.amazonaws.com. The image is pushed to
<registry>/eks-gitops-deploy:<execution id>.
- id: eks_cluster_name
type: STRING
defaults: "eks-gitops-deploy-cluster"
description: Name of the EKS cluster to deploy to. It is used to generate the
kubeconfig with aws eks update-kubeconfig.
- id: k8s_namespace
type: STRING
defaults: "production"
description: Kubernetes namespace to deploy into. It is created if it does not
exist, and the namespace in the k8s/ manifests is rewritten to match.
- id: notify_discord
type: BOOL
defaults: true
description: Post the success embed to Discord after a rollout. Requires the
DISCORD_WEBHOOK_URL secret or KV pair; set to false to skip it. Failures
are always reported by the error handler.
tasks:
- id: working_dir
type: io.kestra.plugin.core.flow.WorkingDirectory
description: One shared directory so the clone, the Kaniko build and the kubectl
deployment all see the same files, including the manifest edited with the
new image tag.
tasks:
- id: clone_repository
type: io.kestra.plugin.git.Clone
description: Check out the repository at the configured branch.
url: "{{ inputs.git_repo | trim }}"
branch: "{{ inputs.git_branch | trim }}"
- id: build_and_push_image
type: io.kestra.plugin.scripts.shell.Commands
description: Build the Dockerfile with the Kaniko executor in userspace (no
privileged Docker daemon or mounted docker.sock) and push it to ECR,
tagged with the execution ID so every deployment maps to exactly one
run.
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
containerImage: gcr.io/kaniko-project/executor:debug
env:
AWS_ACCESS_KEY_ID: "{{ kv(key='AWS_ACCESS_KEY_ID', errorOnMissing=false) ??
secret('AWS_ACCESS_KEY_ID') }}"
AWS_SECRET_ACCESS_KEY: "{{ kv(key='AWS_SECRET_ACCESS_KEY', errorOnMissing=false)
?? secret('AWS_SECRET_ACCESS_KEY') }}"
AWS_REGION: "{{ inputs.aws_region | trim }}"
commands:
- /kaniko/executor --context dir://. --dockerfile Dockerfile
--destination {{ inputs.ecr_registry | trim }}/eks-gitops-deploy:{{
execution.id }} --custom-platform=linux/amd64 --ignore-path=/tmp
- id: deploy_and_verify_rollout
type: io.kestra.plugin.scripts.shell.Commands
description: Connect to EKS, inject the new image tag into k8s/deployment.yaml,
apply the manifests, block on kubectl rollout status until the new
pods are ready, then write the Discord success embed (LoadBalancer
URL, pod placement and replica count) to a file for the notification
step.
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
containerImage: alpine/k8s:1.31.0
env:
AWS_ACCESS_KEY_ID: "{{ kv(key='AWS_ACCESS_KEY_ID', errorOnMissing=false) ??
secret('AWS_ACCESS_KEY_ID') }}"
AWS_SECRET_ACCESS_KEY: "{{ kv(key='AWS_SECRET_ACCESS_KEY', errorOnMissing=false)
?? secret('AWS_SECRET_ACCESS_KEY') }}"
AWS_DEFAULT_REGION: "{{ inputs.aws_region | trim }}"
outputFiles:
- discord_success.json
commands:
- aws eks update-kubeconfig --region {{ inputs.aws_region | trim }}
--name {{ inputs.eks_cluster_name | trim }}
- kubectl create namespace {{ inputs.k8s_namespace | trim }}
--dry-run=client -o yaml | kubectl apply -f -
- sed -i "s|IMAGE_PLACEHOLDER|{{ inputs.ecr_registry | trim
}}/eks-gitops-deploy:{{ execution.id }}|g" k8s/deployment.yaml
- 'sed -i "s|namespace: production|namespace: {{ inputs.k8s_namespace
| trim }}|g" k8s/*.yaml'
- kubectl apply -f k8s/
- kubectl rollout status deployment/eks-gitops-deploy -n {{
inputs.k8s_namespace | trim }} --timeout=420s
- |
LB_HOST=$(kubectl get svc eks-gitops-deploy-service -n {{ inputs.k8s_namespace | trim }} -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' 2>/dev/null || echo "")
if [ -z "$LB_HOST" ]; then
LB_HOST=$(kubectl get svc eks-gitops-deploy-service -n {{ inputs.k8s_namespace | trim }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || echo "Pending...")
fi
POD_SUMMARY=$(kubectl get pods -n {{ inputs.k8s_namespace | trim }} -l app=eks-gitops-deploy -o wide --no-headers 2>/dev/null | awk '{print "• "$1" ("$3", Ready: "$2") on Node: "$7}' | head -n 5)
REPLICAS=$(kubectl get deployment eks-gitops-deploy -n {{ inputs.k8s_namespace | trim }} -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "2/2")
jq -n \
--arg cluster "{{ inputs.eks_cluster_name }}" \
--arg region "{{ inputs.aws_region }}" \
--arg ns "{{ inputs.k8s_namespace | trim }}" \
--arg img "{{ inputs.ecr_registry }}/eks-gitops-deploy:{{ execution.id }}" \
--arg execId "{{ flow.id }} - {{ execution.id }}" \
--arg lb "http://$LB_HOST" \
--arg pods "$POD_SUMMARY" \
--arg reps "$REPLICAS Ready" \
'{
username: "eks-gitops-deploy Bot",
avatar_url: "https://kestra.io/logo.png",
embeds: [
{
title: "🚀 AWS EKS Deployment SUCCESS",
description: "**Application**: `eks-gitops-deploy` successfully deployed and rolled out to **{{ inputs.k8s_namespace | trim }}** on AWS EKS.",
color: 3066993,
fields: [
{
name: "🌐 Live Service LoadBalancer URL",
value: $lb,
inline: false
},
{
name: "📦 Container Image",
value: ("`" + $img + "`"),
inline: false
},
{
name: "🎯 Infrastructure Details",
value: ("• **Cluster**: `" + $cluster + "`\n• **Namespace**: `" + $ns + "`\n• **Region**: `" + $region + "`"),
inline: true
},
{
name: "📊 Pod Health & Sizing",
value: ("• **Replicas**: `" + $reps + "`\n• **CPU/RAM**: `100m-300m / 160Mi-320Mi`\n• **Probes**: Liveness & Readiness UP"),
inline: true
},
{
name: "📋 Active Pod Instances",
value: ("```text\n" + (if $pods != "" then $pods else "All pods healthy" end) + "\n```"),
inline: false
},
{
name: "🔗 Telemetry Endpoints Hub",
value: ("[Discovery Map](" + $lb + "/) • [Live System Stats](" + $lb + "/api/system) • [Actuator Health](" + $lb + "/actuator/health) • [App Info](" + $lb + "/api/info)"),
inline: false
}
],
footer: {
text: ("Kestra GitOps Orchestrator • Execution: " + $execId)
},
timestamp: (now | todate)
}
]
}' > discord_success.json
- id: check_discord_enabled
type: io.kestra.plugin.core.flow.If
description: Only post the success embed when notifications are enabled, so the
flow also works for teams that have no Discord webhook.
condition: "{{ inputs.notify_discord }}"
then:
- id: send_success_embed
type: io.kestra.plugin.scripts.shell.Commands
description: POST the success embed built by the deploy task (LoadBalancer URL,
image, replicas and pod placement) to the Discord webhook.
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
containerImage: alpine/k8s:1.31.0
inputFiles:
discord_success.json: "{{
outputs.deploy_and_verify_rollout.outputFiles['discord_success.json\
'] }}"
env:
DISCORD_WEBHOOK_URL: "{{ kv(key='DISCORD_WEBHOOK_URL', errorOnMissing=false) ??
secret('DISCORD_WEBHOOK_URL') }}"
commands:
- 'curl -s -H "Content-Type: application/json" -X POST -d
@discord_success.json "$DISCORD_WEBHOOK_URL"'
else:
- id: log_notifications_disabled
type: io.kestra.plugin.core.log.Log
description: Record that the Discord notification was skipped on purpose.
message: "Deployment of {{ inputs.ecr_registry | trim }}/eks-gitops-deploy:{{
execution.id }} finished. Discord notifications are disabled for this
run."
# Error handler
errors:
- id: capture_pod_diagnostics_and_alert
type: io.kestra.plugin.scripts.shell.Commands
description: When any task fails, collect pod status, recent container logs and
cluster events and send them to Discord, because a failed rollout is only
useful to the team if the evidence arrives with the alert.
taskRunner:
type: io.kestra.plugin.scripts.runner.docker.Docker
containerImage: alpine/k8s:1.31.0
env:
AWS_ACCESS_KEY_ID: "{{ kv(key='AWS_ACCESS_KEY_ID', errorOnMissing=false) ??
secret('AWS_ACCESS_KEY_ID') }}"
AWS_SECRET_ACCESS_KEY: "{{ kv(key='AWS_SECRET_ACCESS_KEY', errorOnMissing=false)
?? secret('AWS_SECRET_ACCESS_KEY') }}"
AWS_DEFAULT_REGION: "{{ inputs.aws_region | trim }}"
DISCORD_WEBHOOK_URL: "{{ kv(key='DISCORD_WEBHOOK_URL', errorOnMissing=false) ??
secret('DISCORD_WEBHOOK_URL') }}"
commands:
- |
set +e
aws eks update-kubeconfig --region {{ inputs.aws_region | trim }} --name {{ inputs.eks_cluster_name | trim }} || true
echo "=== CAPTURING DEPLOYMENT DIAGNOSTICS ==="
POD_STATUS=$(kubectl get pods -n {{ inputs.k8s_namespace | trim }} -l app=eks-gitops-deploy 2>&1 | head -n 6 | head -c 800)
CRASH_LOGS=$(kubectl logs deployment/eks-gitops-deploy -n {{ inputs.k8s_namespace | trim }} --tail=15 --all-containers=true 2>&1 | head -c 800)
EVENTS=$(kubectl get events -n {{ inputs.k8s_namespace | trim }} --sort-by='.metadata.creationTimestamp' 2>&1 | tail -n 6 | head -c 800)
[ -z "$POD_STATUS" ] && POD_STATUS="No pod status available"
[ -z "$CRASH_LOGS" ] && CRASH_LOGS="No container logs available"
[ -z "$EVENTS" ] && EVENTS="No recent events"
jq -n \
--arg cluster "{{ inputs.eks_cluster_name }}" \
--arg region "{{ inputs.aws_region }}" \
--arg ns "{{ inputs.k8s_namespace | trim }}" \
--arg img "{{ inputs.ecr_registry }}/eks-gitops-deploy:{{ execution.id }}" \
--arg execId "{{ flow.id }} - {{ execution.id }}" \
--arg podStatus "$POD_STATUS" \
--arg crashLogs "$CRASH_LOGS" \
--arg events "$EVENTS" \
'{
username: "eks-gitops-deploy Alert",
avatar_url: "https://kestra.io/logo.png",
embeds: [
{
title: "🚨 AWS EKS Deployment FAILED",
description: ("Deployment failed during verification/rollout on cluster **" + $cluster + "**."),
color: 15158332,
fields: [
{
name: "🎯 Infrastructure & Context",
value: ("• **Cluster**: `" + $cluster + "`\n• **Namespace**: `" + $ns + "`\n• **Region**: `" + $region + "`\n• **Attempted Image**: `" + $img + "`"),
inline: false
},
{
name: "📋 Pod Health & Diagnostics",
value: ("```text\n" + $podStatus + "\n```"),
inline: false
},
{
name: "📜 Recent Crash / Error Logs",
value: ("```text\n" + $crashLogs + "\n```"),
inline: false
},
{
name: "⚠️ Cluster Events",
value: ("```text\n" + $events + "\n```"),
inline: false
}
],
footer: {
text: ("Kestra GitOps Orchestrator • Execution: " + $execId)
},
timestamp: (now | todate)
}
]
}' > /tmp/discord_failure.json
if [ -n "$DISCORD_WEBHOOK_URL" ]; then
echo "Sending incident embed to Discord..."
curl -s -H "Content-Type: application/json" -X POST -d @/tmp/discord_failure.json "$DISCORD_WEBHOOK_URL"
else
echo "Warning: DISCORD_WEBHOOK_URL is not set!"
fi
triggers:
- id: github_webhook
type: io.kestra.plugin.core.trigger.Webhook
description: GitHub (or your CI) POSTs here on every push to start a deployment.
The URL is keyed, so treat the key as a secret.
key: "{{ kv(key='DEPLOY_WEBHOOK_KEY', errorOnMissing=false) ??
secret('DEPLOY_WEBHOOK_KEY') }}"
outputs:
- id: deployed_image
type: STRING
description: Full image reference that was built, pushed to ECR and deployed,
e.g.
123456789012.dkr.ecr.ap-south-2.amazonaws.com/eks-gitops-deploy:3Fz8Qk1xYvLm.
value: "{{ inputs.ecr_registry | trim }}/eks-gitops-deploy:{{ execution.id }}"
- id: deployed_namespace
type: STRING
description: Kubernetes namespace the application was deployed into, e.g. production.
value: "{{ inputs.k8s_namespace | trim }}"