<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kiet Tran</title>
    <description>The latest articles on DEV Community by Kiet Tran (@kenzytran).</description>
    <link>https://dev.to/kenzytran</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4048533%2F6b5bde5c-6148-43cc-95d1-4842921a023d.jpg</url>
      <title>DEV Community: Kiet Tran</title>
      <link>https://dev.to/kenzytran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenzytran"/>
    <language>en</language>
    <item>
      <title>From Commit to Amazon EKS: Building a GitOps Pipeline with Argo CD</title>
      <dc:creator>Kiet Tran</dc:creator>
      <pubDate>Mon, 27 Jul 2026 07:02:55 +0000</pubDate>
      <link>https://dev.to/kenzytran/from-commit-to-amazon-eks-building-a-gitops-pipeline-with-argo-cd-3b9e</link>
      <guid>https://dev.to/kenzytran/from-commit-to-amazon-eks-building-a-gitops-pipeline-with-argo-cd-3b9e</guid>
      <description>&lt;p&gt;Deploying an application to Kubernetes is easy to demonstrate with one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The harder question is what happens after the first deployment.&lt;/p&gt;

&lt;p&gt;How do you know which commit is running? How do you enforce code-quality checks? How does a new container image reach the cluster without giving the CI pipeline broad Kubernetes access? And how do you clean everything up without leaving AWS resources behind?&lt;/p&gt;

&lt;p&gt;I explored those questions by deploying &lt;strong&gt;vProfile&lt;/strong&gt;, a multi-tier Java application, to &lt;strong&gt;Amazon EKS&lt;/strong&gt; using a GitOps workflow.&lt;/p&gt;

&lt;p&gt;The result was this delivery path:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pull request
    -&amp;gt; Maven build and SonarQube Quality Gate
    -&amp;gt; Merge to main
    -&amp;gt; Build and push image to Amazon ECR
    -&amp;gt; Update the image tag in Git
    -&amp;gt; Argo CD synchronizes Amazon EKS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  The application
&lt;/h2&gt;

&lt;p&gt;vProfile contains four runtime components:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tomcat&lt;/td&gt;
&lt;td&gt;Runs the Java web application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;Stores application data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memcached&lt;/td&gt;
&lt;td&gt;Provides caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RabbitMQ&lt;/td&gt;
&lt;td&gt;Handles messaging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This made the project more useful than deploying a single stateless container. It introduced persistent storage, startup dependencies, internal service discovery, secrets, and external traffic routing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tools and their responsibilities
&lt;/h2&gt;

&lt;p&gt;I deliberately gave each tool a narrow responsibility:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; builds, tests, and packages the application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SonarQube&lt;/strong&gt; provides code analysis and a Quality Gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon ECR&lt;/strong&gt; stores the container images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; provisions the AWS infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helm&lt;/strong&gt; packages the Kubernetes resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argo CD&lt;/strong&gt; reconciles the desired state from Git with Amazon EKS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Load Balancer Controller&lt;/strong&gt; creates the public ALB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon EBS CSI Driver&lt;/strong&gt; provides persistent storage for MySQL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important boundary is between CI and CD. The build pipeline creates an artifact and records the desired version in Git. Argo CD, not GitHub Actions, changes the cluster.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pull requests as the quality gate
&lt;/h2&gt;

&lt;p&gt;When a pull request targets &lt;code&gt;main&lt;/code&gt;, GitHub Actions runs the Maven build, tests, SonarQube analysis, and Quality Gate.&lt;/p&gt;

&lt;p&gt;Only after the pull request is reviewed and merged does the pipeline build the application image and push it to Amazon ECR.&lt;/p&gt;

&lt;p&gt;This gives the workflow two distinct stages:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pull request  -&amp;gt; validate the change
Merge         -&amp;gt; produce a deployable artifact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Secrets such as AWS credentials, SonarQube tokens, and webhooks belong in GitHub Secrets. Non-sensitive settings such as the AWS Region and ECR repository name can remain in GitHub Variables.&lt;/p&gt;

&lt;p&gt;For production, I would authenticate GitHub Actions to AWS with OIDC and short-lived credentials instead of storing long-lived AWS access keys.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I used commit SHA image tags
&lt;/h2&gt;

&lt;p&gt;The pipeline tags every application image with the Git commit SHA:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/vprofileappimg&lt;/span&gt;
  &lt;span class="na"&gt;tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7d064a3cc586d197b2968f5bb085e8d2d617942d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This small decision provides a direct link between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the source revision;&lt;/li&gt;
&lt;li&gt;the image in Amazon ECR;&lt;/li&gt;
&lt;li&gt;the workload running in Amazon EKS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a deployment fails, I can identify the exact image and roll back to a known commit. A &lt;code&gt;latest&lt;/code&gt; tag may still be useful for convenience, but it should not be the deployment's only version identifier.&lt;/p&gt;

&lt;p&gt;For stronger immutability, a production workflow can promote images by digest rather than by tag.&lt;/p&gt;
&lt;h2&gt;
  
  
  Provisioning Amazon EKS with Terraform
&lt;/h2&gt;

&lt;p&gt;Terraform creates the main infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC and subnets;&lt;/li&gt;
&lt;li&gt;Amazon EKS cluster;&lt;/li&gt;
&lt;li&gt;managed node group;&lt;/li&gt;
&lt;li&gt;IAM roles;&lt;/li&gt;
&lt;li&gt;OIDC provider for IAM Roles for Service Accounts;&lt;/li&gt;
&lt;li&gt;Amazon EBS CSI Driver.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workshop applies Terraform manually so each step remains visible. A production workflow should instead run:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pull request -&amp;gt; fmt -&amp;gt; validate -&amp;gt; security checks -&amp;gt; plan
Merge + approval -&amp;gt; apply the reviewed plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;GitHub Actions should authenticate through OIDC, production applies should require approval, and Terraform state should use an encrypted and versioned remote backend with locking.&lt;/p&gt;
&lt;h2&gt;
  
  
  Packaging the workloads with Helm
&lt;/h2&gt;

&lt;p&gt;I converted the original Kubernetes manifests into a Helm chart. Values that change between deployments—image tags, replicas, ports, hostname, and storage size—live in &lt;code&gt;values.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before committing a chart change, I render it locally:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm lint helm/vprofile
helm template vprofile helm/vprofile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This catches missing values and invalid templates before Argo CD tries to synchronize them.&lt;/p&gt;

&lt;p&gt;The application also depends on MySQL and Memcached during startup. Init containers wait for their Kubernetes DNS records before Tomcat starts:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;init container waits for service DNS
                |
                v
       application container starts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is a pragmatic way to reduce startup-order failures. It does not replace readiness probes, retries, or proper dependency handling inside the application.&lt;/p&gt;
&lt;h2&gt;
  
  
  Argo CD closes the loop
&lt;/h2&gt;

&lt;p&gt;Argo CD watches the Helm chart in Git. Its automated synchronization policy uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;prune: true&lt;/code&gt; to remove resources deleted from Git;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selfHeal: true&lt;/code&gt; to correct manual changes in the cluster;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CreateNamespace=true&lt;/code&gt; to create the application namespace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After CI updates &lt;code&gt;app.tag&lt;/code&gt;, Argo CD detects the commit, renders the chart, and starts a rolling update.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code
  -&amp;gt; Quality Gate
  -&amp;gt; Image in Amazon ECR
  -&amp;gt; Desired image tag in Git
  -&amp;gt; Argo CD
  -&amp;gt; Amazon EKS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Git acts as the handoff between CI and CD and provides an audit trail for each deployment.&lt;/p&gt;

&lt;p&gt;In this learning project, CI commits the new image tag to the Helm repository because the process is explicit and easy to observe. In production, I would avoid giving CI permission to bypass branch protection. A pull-request-based update or &lt;strong&gt;Argo CD Image Updater&lt;/strong&gt; would keep the build credentials scoped more narrowly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Validating the complete flow
&lt;/h2&gt;

&lt;p&gt;I tested the workflow by making a small change on a feature branch and following it through every stage:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The pull-request checks passed.&lt;/li&gt;
&lt;li&gt;Merging created an image tagged with the commit SHA.&lt;/li&gt;
&lt;li&gt;The Helm values file received the new tag.&lt;/li&gt;
&lt;li&gt;Argo CD moved from OutOfSync to Synced.&lt;/li&gt;
&lt;li&gt;Kubernetes performed a rolling update.&lt;/li&gt;
&lt;li&gt;The application still connected to MySQL, Memcached, and RabbitMQ.&lt;/li&gt;
&lt;li&gt;The HTTPS endpoint remained available through ALB and ACM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Checking each boundary independently made failures easier to locate. A broken deployment could originate in CI, ECR, Git, Argo CD, Kubernetes, DNS, or the application itself.&lt;/p&gt;
&lt;h2&gt;
  
  
  The cleanup step that is easy to miss
&lt;/h2&gt;

&lt;p&gt;The ALBs and EBS volumes in this project are created by Kubernetes controllers. Terraform does not directly own those resources.&lt;/p&gt;

&lt;p&gt;Deleting the EKS cluster first also deletes the controllers that should release them. That can leave orphaned resources generating charges.&lt;/p&gt;

&lt;p&gt;The safe order is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Delete Ingress resources
    -&amp;gt; delete workloads and PVCs
    -&amp;gt; wait for ALBs and EBS volumes to disappear
    -&amp;gt; remove controller resources
    -&amp;gt; terraform destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Finally, verify the Load Balancers, EBS Volumes, EC2 Instances, and EKS Clusters pages in the correct AWS Region.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I would improve for production
&lt;/h2&gt;

&lt;p&gt;This project is a learning environment, not a production reference architecture. My next improvements would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;private networking and tighter EKS API access;&lt;/li&gt;
&lt;li&gt;GitHub OIDC and least-privilege IAM;&lt;/li&gt;
&lt;li&gt;automated Terraform plan and approved apply;&lt;/li&gt;
&lt;li&gt;separate development, staging, and production accounts;&lt;/li&gt;
&lt;li&gt;external secret management;&lt;/li&gt;
&lt;li&gt;health probes, multiple replicas, autoscaling, and observability;&lt;/li&gt;
&lt;li&gt;managed data services where appropriate;&lt;/li&gt;
&lt;li&gt;tested backups and disaster recovery.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If traffic increased, I would first add observability and load testing to find the real bottleneck. I would then configure resource requests and limits, scale stateless pods with HPA, add node capacity with Karpenter, and consider moving MySQL, Memcached, and RabbitMQ to managed AWS services.&lt;/p&gt;
&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The biggest lesson was not how to connect many tools. It was how to define clear boundaries between them.&lt;/p&gt;

&lt;p&gt;CI validates the code and produces the artifact. Git records the desired state. Argo CD reconciles that state. Terraform manages the underlying infrastructure.&lt;/p&gt;

&lt;p&gt;That separation makes the path from commit to running workload easier to trace, review, and repeat.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/KenzyTran" rel="noopener noreferrer"&gt;
        KenzyTran
      &lt;/a&gt; / &lt;a href="https://github.com/KenzyTran/aws-vprofile-gitops" rel="noopener noreferrer"&gt;
        aws-vprofile-gitops
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      GitOps: deploy the multi-tier vProfile app to AWS EKS - CI with GitHub Actions (SonarQube, Docker, ECR), CD with ArgoCD, infra with Terraform.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;vProfile GitOps on Amazon EKS&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;An end-to-end workshop for deploying the multi-tier &lt;strong&gt;vProfile&lt;/strong&gt; Java application to &lt;strong&gt;Amazon EKS&lt;/strong&gt; with a GitOps delivery model.&lt;/p&gt;

&lt;p&gt;The project uses &lt;strong&gt;GitHub Actions&lt;/strong&gt; for continuous integration, &lt;strong&gt;SonarQube&lt;/strong&gt; for code-quality checks, &lt;strong&gt;Amazon ECR&lt;/strong&gt; for container images, &lt;strong&gt;Terraform&lt;/strong&gt; for AWS infrastructure, &lt;strong&gt;Helm&lt;/strong&gt; for application packaging, and &lt;strong&gt;Argo CD&lt;/strong&gt; for continuous delivery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kenzytran.github.io/aws-vprofile-gitops/en/" rel="nofollow noopener noreferrer"&gt;Start the English workshop&lt;/a&gt; · &lt;a href="https://kenzytran.github.io/aws-vprofile-gitops/vi/" rel="nofollow noopener noreferrer"&gt;Vietnamese workshop&lt;/a&gt; · &lt;a href="https://kenzytran.github.io/aws-vprofile-gitops/en/13-cleanup/" rel="nofollow noopener noreferrer"&gt;Cleanup guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/KenzyTran/aws-vprofile-gitops/docs/images/architecture.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FKenzyTran%2Faws-vprofile-gitops%2FHEAD%2Fdocs%2Fimages%2Farchitecture.png" alt="vProfile GitOps architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What this project demonstrates&lt;/h2&gt;
&lt;/div&gt;


&lt;ul&gt;

&lt;li&gt;A pull-request workflow with Maven builds, tests, SonarQube analysis, and a Quality Gate&lt;/li&gt;

&lt;li&gt;Container image builds and pushes to Amazon ECR&lt;/li&gt;

&lt;li&gt;Immutable application image tags based on Git commit SHAs&lt;/li&gt;

&lt;li&gt;Automated Helm value updates after a successful merge&lt;/li&gt;

&lt;li&gt;Reproducible Amazon EKS infrastructure with Terraform&lt;/li&gt;

&lt;li&gt;IAM Roles for Service Accounts (IRSA) for the Amazon EBS CSI Driver&lt;/li&gt;

&lt;li&gt;Persistent MySQL storage backed by Amazon EBS&lt;/li&gt;

&lt;li&gt;Application exposure through AWS Load Balancer Controller, ALB, and ACM&lt;/li&gt;

&lt;li&gt;Automated reconciliation, pruning, and self-healing with Argo CD&lt;/li&gt;

&lt;li&gt;An…&lt;/li&gt;

&lt;/ul&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/KenzyTran/aws-vprofile-gitops" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;The repository includes the source code, Terraform configuration, Helm chart, architecture diagram, and a complete bilingual workshop.&lt;/p&gt;

&lt;p&gt;How do you handle image promotion in your GitOps workflows: direct Git commits, pull requests, or Argo CD Image Updater?&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>gitops</category>
    </item>
  </channel>
</rss>
