# Kubernetes Deployments: ArgoCD and GitHub Actions in Action

I recently developed a distributed voting application using Spring Boot and Kafka. So, I decided to build a CI/CD pipeline for this project and deploy it into a Kubernetes cluster. I containerized the services with Docker and set up a GitHub Actions pipeline to push version-tagged images to DockerHub. ArgoCD then auto-syncs those images into a Kubernetes cluster configured on AWS EC2. This setup ensures that whenever code is committed and merged into the `main` branch, a new version of the app is rolled out seamlessly.

In this post, I’ll walk you through everything step by step—from setting up a simple Kubernetes cluster to configuring a CI/CD pipeline with ArgoCD and GitHub Actions.

# Outline

* Spin up a new K3s cluster on AWS EC2
    
* Kubernetes specification files for the project
    
* Install ArgoCD on the cluster
    
* Configure GitHub Actions
    
* Test the CI/CD pipeline
    

---

# Project Overview: CI/CD Setup

Below is the application we want to deploy. It's a simple distributed voting application orchestrated with Docker containers. You can find the source code here:  
🔗 [https://github.com/devsteppe9/voting\_app](https://github.com/devsteppe9/voting_app)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748655374468/6f0a8d60-ce03-4d75-8cda-3ec2c28139ca.png align="left")

Here’s a visual overview of the directory structure:

```bash
├── .github               # GitHub Actions workflow files
│   └── workflows
│       ├── build-result.yaml
│       ├── build-vote-session.yaml
│       ├── build-vote.yaml
│       └── build-worker.yaml
├── docker-compose.yml    # Local development
├── k8s-specifications    # Kubernetes manifests
├── result                # Node.js web app for real-time results
├── vote                  # Spring Boot/Thymeleaf vote submission app
├── vote-session          # Spring Boot REST API to manage sessions
└── worker                # Spring Boot service to persist votes
```

---

# Spin Up a New K3s Cluster on AWS EC2

If you already have a Kubernetes cluster running, feel free to skip this section.

I launched a `t4g.medium` Ubuntu EC2 instance and saved the `.pem` key for later SSH access. If you're not familiar, [K3s](https://docs.k3s.io/) is a lightweight, production-ready Kubernetes distribution developed by Rancher Labs. If you noticed, I also launched `ARM` based Ubuntu instance because I am using `ARM` based MacBook at home and it was comfortable to push images directly from my laptop when I needed to quickly launch Docker images from my laptop.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748653347923/27be0570-27e7-4442-98f0-9c0a2ac578cd.png align="left")

Make sure to open these ports on your EC2 Security Group:

* `8080`: ArgoCD UI
    
* `22`: SSH access
    
* `6443`: Kubernetes API
    
* `31000–31002`: Application ports
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748653630356/f16d96f8-6314-4dd6-8009-94b1feb3fa0e.png align="left")

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">I have exposed these ports to the world <code>0.0.0.0/0</code> If you have a specific public IP address, it is better to set up <code>8080</code>,<code>22</code>, <code>6443</code> ports accessible only from your specific IP address range.</div>
</div>

To bootstrap the K3s cluster, I used cool tool [`k3sup`](https://github.com/alexellis/k3sup) (said 'ketchup'), built by [Alex Ellis](https://github.com/alexellis). From your laptop:

```bash
curl -sLS https://get.k3sup.dev | sh
sudo install k3sup /usr/local/bin/
k3sup --help
```

Now install K3s to your EC2 instance:

💡 Replace `$IP` and key path with your own. `$HOME/controlplanekeypair.pem` is a private key path on my laptop, I saved in this path while I launched an EC2 instance.

```bash
export IP=54.90.96.48
k3sup install --ip $IP --user ec2-user \
  --ssh-key $HOME/controlplanekeypair.pem
```

It might take couple of minutes and if you don’t see any errors from command output above voila! Your kubernetes cluster ready to run and your `kubeconfig` file is located in your local machine.

Verify:

```bash
export KUBECONFIG=`pwd`/kubeconfig
kubectl config use-context default
kubectl get node -o wide

# -------- Output ------- #
NAME                           STATUS   ROLES                  AGE    VERSION
ip-172-31-85-66.ec2.internal   Ready    control-plane,master   7m4s   v1.32.5+k3s1
```

---

# Kubernetes Specification Files

These are the deployment and service specs I created to deploy the app. ArgoCD watches these files and updates deployments when GitHub Actions push new images to DockerHub:

```bash
├── k8s-specifications
│   ├── kafka-deployment.yaml
│   ├── kafka-service.yaml
│   ├── result-deployment.yaml
│   ├── result-service.yaml
│   ├── vote-db-deployment.yaml
│   ├── vote-db-service.yaml
│   ├── vote-deployment.yaml
│   ├── vote-service.yaml
│   ├── vote-session-deployment.yaml
│   ├── vote-session-service.yaml
│   └── worker-deployment.yaml
```

More details here: [k8s-specifications](https://github.com/devsteppe9/voting_app/tree/main/k8s-specifications)

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748665551573/d2aff0b8-d03b-4977-a632-00fead9998c4.jpeg align="center")

> Photo by Jack Japar **😉**

---

# Install ArgoCD on Kubernetes

```bash
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# -------- Output ------- #
NAME                                                READY   STATUS    RESTARTS    AGE
argocd-application-controller-0                     1/1     Running   0           90s
argocd-applicationset-controller-777d5b5dc7-w8blz   1/1     Running   0           90s
argocd-dex-server-7d8fcd845-lg9hr                   1/1     Running   0           90s
argocd-notifications-controller-655df7c996-q2vp4    1/1     Running   0           90s
argocd-redis-574484f6db-ssf2c                       1/1     Running   0           90s
argocd-repo-server-57449f957c-cdjc5                 1/1     Running   0           90s
argocd-server-7dd4c8cf5f-6x68f                      1/1     Running   0           90s
```

Expose ArgoCD on NodePort:

```bash
cat <<EOF > argocd-server-service.yml
apiVersion: v1
kind: Service
metadata:
  name: argocd-server-nodeport
  labels:
    app.kubernetes.io/name: argocd-server
    app.kubernetes.io/component: server
    app.kubernetes.io/part-of: argocd
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      targetPort: 8080
      nodePort: 31002
      protocol: TCP
  selector:
    app.kubernetes.io/name: argocd-server
EOF

kubectl create -f argocd-server-service.yml -n argocd
```

The service above exposes the ArgoCD GUI on port `31002`

Access `http://YOUR_IP:31002` in your browser:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748658647474/3b42d36e-5236-439a-a228-911f2117203b.png align="left")

Get the admin password:

```bash
kubectl get secret argocd-initial-admin-secret -n argocd \
  -o jsonpath={.data.password} | base64 -d
```

To log in ArgoCD dashboard, usethe password you got from above command and use `admin` as a username.

---

# Create ArgoCD App

1. Go to Applications → New App → `Edit as YAML`
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748659322958/05cbe710-98f8-4880-9211-247e90115b43.png align="center")
    
2. Paste:
    

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: vote-app
spec:
  destination:
    namespace: vote-app
    server: https://kubernetes.default.svc
  source:
    repoURL: https://github.com/devsteppe9/voting_app
    path: k8s-specifications
    targetRevision: main
  project: default
  syncPolicy:
    automated:
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
```

3. Then click Create
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748659429349/b96b526d-d52b-489c-b892-18023bc89814.png align="center")
    
    ![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748659458628/5a2fb228-4a87-44ee-bff4-ecc2c059c675.png align="center")
    

---

# GitHub Actions Setup

I prepared 4 workflow files under the `.github/workflows` directory. Each of them detects code changes under `result`, `vote-session`, `vote` and `worker` subdirectories respectively.

```bash
├── .github               # Github Actions workflow files
│   └── workflows
│       ├── build-result.yaml       # workflow for result app
│       ├── build-vote-session.yaml # workflow for vote-session app
│       ├── build-vote.yaml         # workflow for vote app
│       └── build-worker.yaml       # workflow for worker app
```

The workflow file below is for `vote` service, which is one of the 4 workflows above. The remaining 3 workflows are similar, the only difference is the Docker image name tags and `on.push.paths` field values.

```yaml
name: Integrate vote app

on:
  push:
    branches:
      - main
    paths:
      - 'vote/**'
env:
  DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
  DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

permissions:
  contents: write

jobs:
  build-vote-app:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ env.DOCKERHUB_USERNAME }}
          password: ${{ env.DOCKERHUB_TOKEN }}

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push Docker image
        uses: docker/build-push-action@v6
        with:
          context: ./vote
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ${{ env.DOCKERHUB_USERNAME }}/voting_app-vote:${{ github.sha }}
            ${{ env.DOCKERHUB_USERNAME }}/voting_app-vote:latest
          build-args: |
            KAFKA_BOOTSTRAP_SERVERS=kafka:9092
            SESSION_API_URL=http://vote-session:8080/sessions
      - name: Update Kubernetes deployment
        # Replace image tag in deployment.yaml with new Docker image tagged by commit SHA
        run: |
          sed -i "s|image: ${{ env.DOCKERHUB_USERNAME }}/voting_app-vote:.*|image: ${{ env.DOCKERHUB_USERNAME }}/voting_app-vote:${{ github.sha }}|g" k8s-specifications/vote-deployment.yaml
          echo "Updated image in k8s-specifications/vote-deployment.yaml"
      
      - name: Commit and push changes
        run: |
          git config --local user.name "github-actions[bot]"
          git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add k8s-specifications/vote-deployment.yaml
          git commit -m "Update vote deployment image to ${{ env.DOCKERHUB_USERNAME }}/voting_app-vote:${{ github.sha }}"
          git pull origin main --rebase || false
          git push origin main
```

The workflow builds Docker images for `linux/amd64` and `linux/arm64` architectures, pushes to DockerHub, and updates the corresponding `vote-deployment.yaml` with the new tag.

You need to set up DockerHub secrets on your GitHub Repository. Below is the guideline on how to set up secrets in your GitHub repository: [**Using secrets in GitHub Actions**](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions)

---

# Test the Deployment

As shown in the ArgoCD dashboard, the application has successfully synced changes. I’ve experimented with adding and removing some code on `result` service, then pushed it into `main` branch of the repository. As you see, there are 7 more revisions created for the `result` service, and the latest one is serving, running as a pod.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748660881326/d917a83b-8111-4539-afbe-945689ab0ce2.png align="left")

Test your application:

* Vote App: http://YOUR\_IP:31000/votes/1
    
* Result App: http://YOUR\_IP:31001/results/1
    

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748661063702/b28c1488-11f8-482e-ade4-615a9ac9c5ae.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1748661083421/0479754e-d9e3-4ba7-a454-7544d8e8fca1.png align="center")

---

# Recap

### GitHub Actions

* Builds and pushes Docker images
    
* Update Kubernetes deployment files
    

### ArgoCD

* Monitors `k8s-specifications/` directory
    
* Auto-syncs updated manifests into the K8s cluster
    

In conclusion, deploying a distributed voting application using Kubernetes, ArgoCD, and GitHub Actions provides a robust and automated CI/CD pipeline. Additionally, you can extend your workflow by adding Docker image scanning and Linting stages, which check the syntax of your source code, among other features. But these are not covered in this blog post. By integrating these technologies, developers can efficiently manage application deployments, monitor changes, and ultimately enhance the overall development and deployment process.
