Local GitOps Platform with Terraform and ArgoCD
Provision a Kubernetes cluster with Terraform and deploy apps via GitOps.
Introduction
30 Second Summary
Every time you manually set up a server, install software by hand, or deploy code by copying files, you are creating a process that only lives in your memory. If something breaks, you have to remember every step to rebuild it. Platform engineering solves this by turning infrastructure into code and deployments into automated pipelines.
In this project, you will build a complete local platform engineering environment where Terraform creates your Kubernetes cluster, ArgoCD deploys apps automatically from GitHub, and Prometheus plus Grafana show you live cluster health metrics.
What You'll Build
You will run a single command and watch an entire Kubernetes cluster appear, push a file to GitHub and see it deploy itself in seconds, then open a dashboard showing real-time CPU and memory usage flowing from your cluster.
By the end of this project, you'll have:
- A running Kubernetes cluster you can create and destroy with a single terraform apply or terraform destroy command, demonstrating Infrastructure as Code in action.
- A complete GitOps pipeline where pushing a YAML change to GitHub triggers ArgoCD to automatically detect and deploy it to your cluster within seconds.
- A production-grade observability stack with Prometheus and Grafana showing live CPU, memory, and pod metrics from your cluster in pre-built dashboards.
- Secret Mission: Build a custom "Platform Health" Grafana dashboard with hand-picked PromQL queries and multiple visualization types.
Are there any prerequisites?
No prior experience with Kubernetes, Terraform, or GitOps is required. Step 1 walks you through installing every tool you need. You just need VS Code and a GitHub account to get started.
Before We Start
Before We Start...
Before we dive in, let's lock in what you're about to build and why it matters. This project gives you hands-on experience with the same tools and patterns that production platform teams use every day.
Install Your Platform Engineering Toolkit
Platform engineering relies on a chain of CLI tools working together. Terraform provisions infrastructure. kubectl talks to Kubernetes clusters. Helm installs complex applications. Docker runs containers that simulate real nodes.
Without any one of them, the rest of the project cannot proceed. This step gets every tool installed and verified so you can focus on building from Step 2 onward.
In this step, get ready to:
- Install Homebrew, Docker Desktop, Terraform, kubectl, and Helm.
- Verify all four tools respond to version commands and Docker can run containers.
Install Homebrew and Docker Desktop
Every tool in this project installs through Homebrew, the macOS package manager. You need Homebrew first, then everything else installs with one-line commands.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Check if Homebrew is already installed by running:
brew --version
✔️ I see a version number
Homebrew is ready. Move on to the Docker Desktop install below.
ⓧ Command not found
- Install Homebrew by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
What does this command do?
It downloads Homebrew's official install script and runs it. The script is interactive. It shows exactly what it plans to do, then pauses for your confirmation before making any changes.
- Follow the prompts in your terminal. When asked for your password, type it and press Enter (the cursor stays blank while typing, which is normal).
- After installation finishes, the script prints "Next steps" with commands to add Homebrew to your PATH. Run those commands exactly as shown in your terminal.
- Close and reopen your terminal, then run brew --version again. You should see a version number.
Still seeing command not found?
The most common cause is skipping the "Next steps" PATH commands the installer printed. Scroll up in your terminal, find those lines, and run them. Then close and reopen the terminal window.
help me fix my Homebrew PATH setup
- Install Docker Desktop by running:
brew install --cask docker-desktop
Why Docker Desktop?
The Kubernetes cluster you build in this project runs its nodes as Docker containers. Docker Desktop provides the container engine that makes this possible on your local machine.
- Once the install finishes, open Docker Desktop by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Docker, and pressing Enter.
- Docker Desktop opens a first-run setup. Accept the license agreement and allow any helper installs it asks for.
- Wait until you see the Docker Desktop dashboard with a green "Running" indicator. This confirms the Docker engine is active.
- Confirm Docker is working by running:
docker run --rm hello-world
You should see a message starting with "Hello from Docker!" confirming that Docker pulled an image and ran it as a container.
Docker command not working?
Make sure Docker Desktop is running (check for the whale icon in your menu bar or system tray). If you see a permissions error, try closing and reopening your terminal after Docker Desktop finishes starting up.
help me get Docker running on my machine
Install Terraform, kubectl, and Helm
With Homebrew and Docker running, you can install the remaining three CLI tools. Each one handles a different layer of your platform.
- Add the HashiCorp tap and install Terraform by running:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
What is a tap?
A tap is a third-party Homebrew repository. HashiCorp publishes their own tap so you always get the official Terraform build directly from the source.
- Confirm Terraform installed by running:
terraform -version
You should see a version number in the output.
- Install kubectl by running:
brew install kubectl
- Install Helm by running:
brew install helm
Seeing errors during brew install?
Run brew update first to make sure Homebrew's package index is current, then retry the failing install command. If a formula cannot be found, double-check you typed the name exactly as shown above.
help me fix a failing brew install command
Verify your full toolkit
All four tools should now be installed. Before moving on, confirm every one responds to a version check.
- Before you run the next command, predict: do you expect all four tools to print version output, or will any of them fail?
- Run this combined version check:
docker --version && terraform -version && kubectl version --client && helm version
You should see version output for all four tools printed one after another. If any command fails, the chain stops at that tool.
One of the tools did not print a version?
The && operator stops at the first failure. Identify which tool failed, then scroll up to find its install command and run it again. Make sure Docker Desktop is still running if docker --version failed.
help me figure out which tool is not installed correctly
Your toolkit is locked and loaded. Next up, you will use Terraform to provision an entire Kubernetes cluster from a single command.
Provision a Kubernetes Cluster with Terraform
Your toolkit is installed and ready to go. Now it's time for the real payoff: creating a running Kubernetes cluster from nothing but code.
Setting up infrastructure by clicking through UIs or running one-off commands is unrepeatable. If something breaks, you cannot reliably recreate the same environment. Infrastructure as Code solves this by defining your entire cluster in a file. Run one command and it appears. Run another and it vanishes.
In this step, get ready to:
- Write three Terraform files that define your cluster infrastructure.
- Run terraform apply to provision a 2-node Kubernetes cluster locally.
- Connect to your cluster with kubectl and confirm both nodes are ready.
Create your project directory and provider configuration
Every Terraform project lives in its own directory. You will create a folder on your Desktop and write the first configuration file that tells Terraform which provider to use.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir platform-terraform
cd platform-terraform
- Confirm the folder exists by running:
ls
You should see the platform-terraform folder listed in the output (it will be empty for now).
- Open this folder in VS Code by running:
code .
Command not found for code?
If code . does not work, open VS Code manually. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Shell Command: Install 'code' command in PATH, and select it. Then retry code . in your terminal.
help me get the code command working in my terminal
Now you will create the first of three Terraform files. The providers.tf file tells Terraform which plugin to download for managing your cluster.
- In VS Code, create a new file called providers.tf in the platform-terraform folder.
- Paste the following code into providers.tf:
# Tell Terraform to use the Kind provider for managing clusters
terraform {
required_providers {
kind = {
source = "tehcyx/kind"
version = "0.11.0"
}
}
}
# Initialize the provider with default settings
provider "kind" {}
What does this code do?
- The required_providers block pins the Kind provider to version 0.11.0 from the Terraform Registry. This ensures everyone who runs your code gets the exact same provider.
- The provider "kind" {} block activates the provider. Kind runs Kubernetes nodes as Docker containers, so it needs Docker Desktop running in the background.
- Save providers.tf.
✔️ Awesome, I've got everything!
Great. Make sure you have saved providers.tf before continuing.
ⓧ I'd like to double check the full code
# Tell Terraform to use the Kind provider for managing clusters
terraform {
required_providers {
kind = {
source = "tehcyx/kind"
version = "0.11.0"
}
}
}
# Initialize the provider with default settings
provider "kind" {}
With the provider defined, Terraform knows what to download. Time to test it.
- Switch back to your terminal and run terraform init to download the Kind provider:
terraform init
You should see output showing the provider being downloaded and installed, ending with a success message.
What should I see?
Terraform prints lines like Installing tehcyx/kind v0.11.0... followed by Terraform has been successfully initialized!. It also creates a .terraform directory and a .terraform.lock.hcl lock file in your project folder.
Seeing a provider error?
Make sure Docker Desktop is running. The Kind provider requires Docker to be available. Also confirm you saved providers.tf and that your terminal is inside the platform-terraform directory.
help me fix a terraform init error with the Kind provider
Define your cluster infrastructure
The provider is ready. Now you will write the file that actually defines what your cluster looks like: how many nodes, what ports to expose, and where to store the connection credentials.
- In VS Code, create a new file called main.tf in the platform-terraform folder.
- Paste the following code into main.tf:
# Define a 2-node Kind cluster with port mappings
resource "kind_cluster" "platform" {
name = "platform-cluster"
kubeconfig_path = pathexpand("~/.kube/platform-cluster-config")
wait_for_ready = true
kind_config {
kind = "Cluster"
api_version = "kind.x-k8s.io/v1alpha4"
node {
role = "control-plane"
# Map container ports to host for accessing services later
extra_port_mappings {
container_port = 30080
host_port = 30080
}
extra_port_mappings {
container_port = 30081
host_port = 30081
}
}
node {
role = "worker"
}
}
}
What does this code do?
- name sets the cluster name to platform-cluster. You will use this name to identify it later.
- kubeconfig_path tells Terraform where to save the connection file. This is the key that lets kubectl talk to your cluster.
- The first node block creates the control-plane (the cluster's brain) with two port mappings so you can access services on ports 30080 and 30081 from your laptop.
- The second node block adds a worker node where your application pods will actually run.
- Save main.tf.
✔️ Awesome, I've got everything!
Great. Make sure you have saved main.tf before continuing.
ⓧ I'd like to double check the full code
# Define a 2-node Kind cluster with port mappings
resource "kind_cluster" "platform" {
name = "platform-cluster"
kubeconfig_path = pathexpand("~/.kube/platform-cluster-config")
wait_for_ready = true
kind_config {
kind = "Cluster"
api_version = "kind.x-k8s.io/v1alpha4"
node {
role = "control-plane"
# Map container ports to host for accessing services later
extra_port_mappings {
container_port = 30080
host_port = 30080
}
extra_port_mappings {
container_port = 30081
host_port = 30081
}
}
node {
role = "worker"
}
}
}
One more file to go. The outputs.tf file tells Terraform which values to print after the cluster is created, so you can easily find your cluster name and connection details.
- In VS Code, create a new file called outputs.tf in the platform-terraform folder.
- Paste the following code into outputs.tf:
# Expose cluster details for use in other commands
output "cluster_name" {
value = kind_cluster.platform.name
}
output "endpoint" {
value = kind_cluster.platform.endpoint
}
output "kubeconfig_path" {
value = kind_cluster.platform.kubeconfig_path
}
What does this code do?
Each output block extracts a value from the cluster resource and prints it in your terminal after terraform apply completes. You get the cluster name, its API endpoint, and the path to the kubeconfig file without needing to look them up manually.
- Save outputs.tf.
✔️ Awesome, I've got everything!
Great. Make sure you have saved outputs.tf before continuing.
ⓧ I'd like to double check the full code
# Expose cluster details for use in other commands
output "cluster_name" {
value = kind_cluster.platform.name
}
output "endpoint" {
value = kind_cluster.platform.endpoint
}
output "kubeconfig_path" {
value = kind_cluster.platform.kubeconfig_path
}
Provision the cluster with Terraform
All three files are saved. Now you will preview what Terraform plans to create, then apply it to bring your cluster to life.
- In your terminal, preview the infrastructure plan by running:
terraform plan
You should see output showing Plan: 1 to add, 0 to change, 0 to destroy at the bottom. Terraform is telling you it will create one resource: your Kind cluster.
Why run plan before apply?
The terraform plan command is a dry run. It shows exactly what will be created, changed, or destroyed without actually doing anything. In production, teams review plans before approving changes to avoid surprises.
The plan looks good. Before you run the next command, what do you think will happen when Terraform applies this plan?
- Create the cluster by running:
terraform apply
Terraform will ask for confirmation
Terraform prompts you with Do you want to perform these actions? before making changes. Type yes and press Enter to confirm.
Terraform downloads the Kind node image and creates your cluster. This takes approximately 30-60 seconds.
What should I see?
When it finishes, you will see Apply complete! Resources: 1 added, 0 changed, 0 destroyed. followed by your three outputs: cluster_name, endpoint, and kubeconfig_path.
Your Kubernetes cluster now exists as Docker containers on your machine. One command created the entire thing.
Seeing a Docker error?
Make sure Docker Desktop is running. Look for the Docker whale icon in your menu bar or system tray. If Docker was just started, wait 30 seconds for it to fully initialize before retrying terraform apply.
help me fix a terraform apply error when creating a Kind cluster
Connect to your cluster
Your cluster is running, but kubectl does not know about it yet. You need to tell it where to find the connection credentials that Terraform just generated.
- Set the KUBECONFIG environment variable to point at your cluster's kubeconfig file by running:
export KUBECONFIG=~/.kube/platform-cluster-config
What does this command do?
The KUBECONFIG variable tells kubectl which cluster to talk to. Terraform saved the connection file at ~/.kube/platform-cluster-config (as specified in main.tf). Now every kubectl command you run in this terminal session targets your new cluster.
Before you run the next command, what do you expect to see? How many nodes should your cluster have based on your main.tf?
- Verify your cluster is alive by running:
kubectl get nodes
You should see two nodes listed, both showing Ready status: one control-plane node and one worker node.
What should I see?
The output shows a table with columns NAME, STATUS, ROLES, AGE, and VERSION. Both rows should show Ready under STATUS. If one shows NotReady, wait 30 seconds and run the command again. Nodes need a moment to fully start.
Seeing connection refused?
Confirm you ran the export KUBECONFIG command in the same terminal session. If you opened a new terminal tab, the variable is not set there. Run the export command again in the new tab.
help me connect kubectl to my Kind cluster
You just created a full Kubernetes cluster from three small text files. Next up, you will install ArgoCD to give your cluster the ability to deploy apps automatically from GitHub.
Deploy ArgoCD to Your Cluster
Your cluster is running, but right now the only way to deploy an application is to manually run kubectl apply every time you want something on the cluster. That works for one-off experiments but breaks down completely when a team needs repeatable, auditable deployments.
ArgoCD solves this by continuously watching a Git repository and deploying whatever it finds there. This is the GitOps pattern that production platform teams use. In this step, you'll install ArgoCD using Helm and access its web dashboard.
In this step, get ready to:
- Install ArgoCD into your cluster using Helm.
- Access the ArgoCD web dashboard via port-forwarding.
- Log in with the auto-generated admin credentials.
Add the Argo Helm repository and install ArgoCD
Helm uses repositories to find pre-packaged applications (called charts). You need to add the official Argo repository before you can install anything from it.
- Add the Argo Helm repository and update Helm's local cache by running these commands:
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
What do these commands do?
- helm repo add registers the Argo chart repository so Helm knows where to download ArgoCD from.
- helm repo update refreshes the local index of all registered repositories to get the latest chart versions.
- You should see "argo" has been added to your repositories in the output of the first command.
Seeing a connection error?
Make sure you have an active internet connection. Helm needs to reach https://argoproj.github.io/argo-helm to download the repository index.
If you're behind a corporate proxy, check that your terminal's proxy settings are configured. Help me troubleshoot Helm repository connection issues
- Install ArgoCD into a dedicated argocd namespace by running:
helm install argocd argo/argo-cd --namespace argocd --create-namespace
What does this command do?
- helm install argocd creates a Helm release named argocd.
- argo/argo-cd tells Helm to pull the argo-cd chart from the argo repository you just added.
- --namespace argocd --create-namespace puts all ArgoCD components into their own isolated namespace, creating it if it doesn't exist.
- Look for STATUS: deployed in Helm's output. This confirms ArgoCD's components are being created in your cluster.
Seeing an install error?
If you see a message about the namespace already existing, a previous install attempt may have partially completed. Run helm uninstall argocd --namespace argocd then try the install command again.
Help me fix my ArgoCD Helm install error
Wait for ArgoCD pods and set up port-forwarding
ArgoCD consists of several components (server, repo server, controller, Redis) that each run as a separate pod. It takes a minute or two for all of them to start up.
- Check the status of ArgoCD's pods by running:
kubectl get pods -n argocd
- You should see pods like argocd-server, argocd-repo-server, and argocd-application-controller all showing Running in the STATUS column.
Pods still showing ContainerCreating?
Wait 30 seconds and run the command again. Kubernetes is pulling container images the first time, which takes a moment. If pods stay in Pending for over two minutes, Docker Desktop may be running low on memory.
Once all pods are Running, you can access the ArgoCD web UI. ArgoCD's server runs inside the cluster, so you need port-forwarding to reach it from your browser.
- Start port-forwarding to the ArgoCD server by running:
kubectl port-forward svc/argocd-server -n argocd 8080:443
What does this command do?
This maps your local port 8080 to the ArgoCD server's port 443 inside the cluster. Any traffic you send to localhost:8080 gets tunneled directly to ArgoCD.
This command blocks your terminal while it runs. You will need to open a new terminal tab for the next commands. Press Ctrl+C when you want to stop it later.
- You should see output like Forwarding from 127.0.0.1:8080 confirming the tunnel is active.
- Open a new terminal tab by pressing Cmd+T (macOS) or Ctrl+Shift+T (Windows/Linux).
- In your new terminal, set the KUBECONFIG variable again by running:
export KUBECONFIG=~/.kube/platform-cluster-config
Why set KUBECONFIG again?
Environment variables only exist in the terminal session where you set them. A new tab starts fresh, so kubectl would not know which cluster to talk to without this.
Retrieve the admin password and log in
ArgoCD generates a random admin password during installation and stores it in a Kubernetes secret. You need to decode it before you can log in.
- Retrieve the auto-generated admin password by running this command in your new terminal:
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
What does this command do?
- kubectl get secret reads the argocd-initial-admin-secret from the argocd namespace.
- -o jsonpath="{.data.password}" extracts just the password field from the secret.
- base64 -d decodes the value because Kubernetes stores secrets in base64 encoding.
- Copy the password string that appears in your terminal. There is no newline at the end, so it may run into your next prompt.
Seeing "not found" for the secret?
Make sure you ran the export KUBECONFIG command in this terminal first. Without it, kubectl cannot find your cluster.
If KUBECONFIG is set but the secret still isn't found, ArgoCD may not have finished starting. Run kubectl get pods -n argocd and confirm all pods show Running.
Help me retrieve my ArgoCD admin password
Before you open the browser, what do you expect to see at https://localhost:8080? Think about what ArgoCD is and what state it's in right now.
- Open your browser and navigate to https://localhost:8080.
- Your browser will show a security warning about the self-signed certificate. Click Advanced and then Proceed to localhost (or Accept the Risk and Continue in Firefox).
- Enter admin as the username.
- Paste the password you copied from the terminal.
- Click SIGN IN.
You should see the ArgoCD dashboard with an empty applications list. ArgoCD is installed and ready, but it is not watching any repositories yet.
Browser shows connection refused?
Switch back to the terminal tab running kubectl port-forward and check that it is still active. If it exited, re-run the port-forward command.
Also confirm you are using https:// in the URL, not http://. ArgoCD's server uses TLS on port 443.
Help me access the ArgoCD dashboard
ArgoCD is live and waiting for instructions. Next up, you'll connect it to a GitHub repository and watch it deploy an application automatically.
Deploy an App via GitOps
Your cluster is running and ArgoCD is installed, but right now the only way to deploy applications is by manually running kubectl apply commands. That approach breaks down fast: there is no audit trail, no rollback path, and no way for a team to collaborate on changes safely.
In this step, you will connect ArgoCD to a GitHub repository so that pushing a YAML file automatically deploys it to your cluster. You will also hit your first real platform engineering problem: ArgoCD detects your changes but refuses to deploy them automatically. Solving that gap teaches the difference between drift detection and automated remediation.
In this step, get ready to:
- Create a GitHub repository with Kubernetes manifests for an nginx deployment.
- Connect ArgoCD to your repo and hit the "OutOfSync but won't auto-deploy" obstacle.
- Enable automated sync with self-healing so ArgoCD deploys every push automatically.
Create a GitHub repository with Kubernetes manifests
Your GitOps manifests live in a separate repository from your Terraform code. This mirrors how production teams separate infrastructure provisioning from application deployment.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your GitOps project folder and move into it by running these commands:
mkdir platform-gitops-repo
cd platform-gitops-repo
- Create the k8s directory that will hold your deployment manifests:
mkdir k8s
- Confirm the folder structure exists by running:
ls
You should see a k8s directory listed in the output.
- Create the Deployment manifest by opening a new file at k8s/deployment.yaml in VS Code and pasting this code:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
labels:
app: nginx-demo
spec:
replicas: 2
selector:
matchLabels:
app: nginx-demo
template:
metadata:
labels:
app: nginx-demo
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
What does this manifest do?
This creates a Deployment that runs 2 replicas of the nginx:1.27 container. Kubernetes ensures 2 pods are always running and restarts any that crash.
The selector and labels fields connect the Deployment to its pods. Every pod gets the label app: nginx-demo so the Deployment can track them.
- Save k8s/deployment.yaml.
- Create the Service manifest by opening a new file at k8s/service.yaml and pasting this code:
apiVersion: v1
kind: Service
metadata:
name: nginx-demo
spec:
selector:
app: nginx-demo
ports:
- port: 80
targetPort: 80
nodePort: 30080
type: NodePort
What does this Service do?
A Service exposes your pods to network traffic. The NodePort type maps port 30080 on your host machine to port 80 inside the pods.
Remember the extra_port_mappings you set in main.tf? That mapped container port 30080 to host port 30080. This Service connects to that same port so traffic flows from your browser all the way to nginx.
- Save k8s/service.yaml.
- Initialise a Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Add nginx deployment and service manifests"
You should see output confirming 2 files were committed.
Seeing an error about Git identity?
If Git asks you to set your name and email, run these commands (replacing the placeholders with your info):
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
Then re-run the git commit command. Help me set up my Git identity for this project.
- Open your browser and go to https://github.com/new to create a new repository.
- Enter platform-gitops-repo as the repository name.
- Leave it set to **Public** (ArgoCD needs to read it without authentication for this project).
- Do NOT check any boxes for README, .gitignore, or license.
- Click **Create repository**.
- Push your local repository to GitHub by running these commands (replacing the URL with your GitHub username):
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/platform-gitops-repo.git
git branch -M main
git push -u origin main
You should see output ending with a line like Branch 'main' set up to track remote branch 'main' from 'origin'.
Push rejected or authentication failed?
GitHub requires a personal access token (not your password) for HTTPS pushes. If prompted for a password, generate a token at **Settings > Developer settings > Personal access tokens > Tokens (classic)** with the repo scope, and paste it as the password.
Help me authenticate my Git push to GitHub.
Connect ArgoCD to your repository
Your manifests are on GitHub. Now you need to tell ArgoCD where to find them. You do this by creating an Application resource, which is ArgoCD's way of linking a Git repo to a cluster destination.
- In VS Code, create a new file called argocd-app.yaml in the root of your platform-gitops-repo folder and paste this code:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: nginx-demo
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/[[GITHUB_USERNAME="your-github-username"]]/platform-gitops-repo
targetRevision: main
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: default
What does this Application resource do?
This tells ArgoCD to watch the k8s directory in your GitHub repo's main branch. Whenever ArgoCD detects a difference between what is in Git and what is in the cluster, it flags the app as "OutOfSync".
Notice there is no syncPolicy field. Without it, ArgoCD only detects drift. It will not automatically fix it.
- Save argocd-app.yaml.
- Apply the Application to your cluster by running:
kubectl apply -f argocd-app.yaml
You should see application.argoproj.io/nginx-demo created.
- Switch to the ArgoCD dashboard in your browser at https://localhost:8080.
- You should see the nginx-demo application card showing status **OutOfSync**.
- Click on the nginx-demo card to open it.
- Click the **Sync** button at the top of the page.
- In the sync panel that appears, click **Synchronize** to confirm.
ArgoCD pulls your manifests from GitHub and applies them to the cluster. After a few seconds, the status should change to **Synced** and **Healthy**.
- Confirm the deployment is running by switching to your terminal and running:
kubectl get pods
You should see 2 pods with names starting with nginx-demo- and status Running.
Pods not showing up?
Make sure your KUBECONFIG is still set correctly. Run echo $KUBECONFIG and confirm it points to ~/.kube/platform-cluster-config.
If ArgoCD still shows OutOfSync, check that your GitHub repo URL in argocd-app.yaml matches your actual repository URL exactly (including your username).
Help me debug why ArgoCD is not syncing my app.
Now for the designed obstacle. You will push a change to GitHub and discover that ArgoCD sees it but does nothing about it.
- In VS Code, open k8s/deployment.yaml.
- Find this line:
replicas: 2
- Change 2 to 3, so the line reads:
replicas: 3
- Save the file, then commit and push the change:
git add .
git commit -m "Scale nginx to 3 replicas"
git push
Before you check ArgoCD, make a prediction: do you think ArgoCD will automatically deploy this change, or just detect it?
- Switch to the ArgoCD dashboard and wait about 30 seconds for it to poll your repo.
- Check the application status.
ArgoCD shows **OutOfSync**. It knows Git says 3 replicas but the cluster still has 2. It detected the drift, but it did not fix it.
- Verify the cluster still has 2 pods by running:
kubectl get pods
Still 2 pods. This is the gap between drift detection and automated remediation. ArgoCD can see the mismatch, but without explicit permission it refuses to act.
Why does ArgoCD not auto-deploy by default?
Automatically applying every Git change to a live cluster is dangerous without safeguards. The default manual-sync policy forces a human to review and approve each deployment.
Production teams often start with manual sync and graduate to automated sync once they trust their CI pipeline and have proper access controls in place.
Enable automated sync with self-healing
The fix is a single configuration block: syncPolicy.automated. This tells ArgoCD to automatically apply any difference it detects between Git and the cluster.
- Create a new file called argocd-app-autosync.yaml in the root of your platform-gitops-repo folder and paste this code:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: nginx-demo
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/[[GITHUB_USERNAME="your-github-username"]]/platform-gitops-repo
targetRevision: main
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
What do prune and selfHeal mean?
The prune: true setting means if you delete a manifest from Git, ArgoCD deletes the corresponding resource from the cluster. Without it, removed files leave orphaned resources behind.
The selfHeal: true setting means if someone manually edits a resource in the cluster (bypassing Git), ArgoCD reverts it back to match Git. Git is always the single source of truth.
- Save argocd-app-autosync.yaml.
- Apply the updated Application resource to replace the old one:
kubectl apply -f argocd-app-autosync.yaml
You should see application.argoproj.io/nginx-demo configured. The word "configured" (not "created") confirms it updated the existing Application.
Before you check the result, predict what happens now. ArgoCD already knows the cluster is OutOfSync (2 replicas vs Git's 3). With automated sync now enabled, what do you think it will do?
- Wait about 10 seconds, then check your pods:
kubectl get pods
You should now see 3 pods with names starting with nginx-demo- and status Running. ArgoCD detected the pending OutOfSync state and automatically applied the change.
- Switch to the ArgoCD dashboard to confirm. The application should show **Synced** and **Healthy** with 3 pods visible in the resource tree.
Still seeing only 2 pods?
Give ArgoCD up to 30 seconds to detect the policy change and trigger a sync. If nothing happens, check the ArgoCD dashboard for error messages on the application card.
You can also force a refresh by clicking the **Refresh** button in the ArgoCD UI.
Help me debug why ArgoCD automated sync is not working.
✔️ Awesome, I've got everything!
Great. Double check that you have saved both argocd-app-autosync.yaml and the updated k8s/deployment.yaml with 3 replicas.
ⓧ I'd like to double check the full code
Here is k8s/deployment.yaml in its final state:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
labels:
app: nginx-demo
spec:
replicas: 3
selector:
matchLabels:
app: nginx-demo
template:
metadata:
labels:
app: nginx-demo
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
Here is k8s/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: nginx-demo
spec:
selector:
app: nginx-demo
ports:
- port: 80
targetPort: 80
nodePort: 30080
type: NodePort
Here is argocd-app-autosync.yaml:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: nginx-demo
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-github-username/platform-gitops-repo
targetRevision: main
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
You have a complete GitOps pipeline running locally. Push a change to GitHub, and ArgoCD deploys it to your cluster within seconds. Next up, you will add Prometheus and Grafana so you can see what is happening inside your cluster in real time.
Add Observability with Prometheus and Grafana
Your platform now deploys apps automatically through ArgoCD, but you have no visibility into cluster health.
If a node runs out of memory or pods crash, you would not know. The kube-prometheus-stack gives you production-grade monitoring out of the box: metrics collection, alerting rules, and pre-built dashboards. One Helm chart deploys Prometheus, Grafana, Alertmanager, and dozens of pre-configured dashboards.
In this step, get ready to:
- Install the kube-prometheus-stack Helm chart into a monitoring namespace.
- Access Grafana and log in with the admin credentials.
- Explore pre-built Kubernetes dashboards showing live cluster metrics.
Install the kube-prometheus-stack
The kube-prometheus-stack is a single Helm chart that deploys Prometheus, Grafana, Alertmanager, node-exporter, kube-state-metrics, and dozens of pre-configured dashboards. This is how real platform teams deploy observability.
- Add the Prometheus community Helm repository by running this command:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
- Update your Helm repositories to fetch the latest chart versions:
helm repo update
- Install the kube-prometheus-stack into a dedicated monitoring namespace by running:
helm install monitoring prometheus-community/kube-prometheus-stack -n monitoring --create-namespace
What does this command do?
This installs the chart with a release name of monitoring into a new namespace called monitoring. The --create-namespace flag tells Helm to create the namespace if it does not exist.
This single chart deploys Prometheus (metrics collection), Grafana (visualization), Alertmanager (alert routing), node-exporter (hardware metrics), and kube-state-metrics (Kubernetes object metrics).
The installation takes 1-2 minutes. Helm will print a summary when it finishes.
- Wait for all pods in the monitoring namespace to reach Running state by running:
kubectl get pods -n monitoring
You should see multiple pods listed, all with a STATUS of Running or Completed. This includes pods for Prometheus, Grafana, Alertmanager, node-exporter, and kube-state-metrics.
Pods still in Pending or ContainerCreating?
Give it another minute. The first time these containers are pulled, they may take longer. Re-run kubectl get pods -n monitoring after waiting.
Access Grafana and retrieve the admin password
Grafana is running inside your cluster, but it is not exposed to your browser yet. You need to set up port-forwarding to access it on localhost:3000.
- Start port-forwarding to the Grafana service by running:
kubectl port-forward svc/monitoring-grafana 3000:80 -n monitoring
What does this command do?
This forwards traffic from localhost:3000 on your machine to port 80 on the Grafana service inside the cluster. The command keeps running in the foreground.
- Open a new terminal tab or window. The port-forward command occupies the current terminal.
- Retrieve the Grafana admin password from the Kubernetes secret by running:
kubectl get secret --namespace monitoring monitoring-grafana -o jsonpath="{.data.admin-password}" | base64 --decode ; echo
This command prints the Grafana admin password to your terminal. Copy it.
Why is the password in a Kubernetes secret?
The Helm chart stores the Grafana admin password as a base64-encoded value inside a Kubernetes Secret. This keeps credentials out of plain-text config files. The base64 --decode part converts it back to readable text.
- Open your browser and navigate to http://localhost:3000.
- Enter admin as the username.
- Paste the password you just retrieved into the password field.
- Click Log in.
You should see the Grafana home page with a welcome message and links to explore dashboards.
Can't log in to Grafana?
- Make sure the port-forward command is still running in your other terminal. If you closed it, re-run kubectl port-forward svc/monitoring-grafana 3000:80 -n monitoring.
- Double-check you copied the full password output with no trailing spaces.
- If the password command returned nothing, verify the pods are running with kubectl get pods -n monitoring.
Help me troubleshoot Grafana login issues with kube-prometheus-stack.
Explore the pre-built Kubernetes dashboards
The kube-prometheus-stack comes with dozens of pre-built dashboards that visualize real metrics from your cluster. You will explore two that show compute resource usage.
- In the Grafana sidebar, click the hamburger menu icon (three horizontal lines) in the top left.
- Click Dashboards.
- In the search field, type Kubernetes / Compute Resources / Cluster.
- Click on the Kubernetes / Compute Resources / Cluster dashboard to open it.
You should see panels showing live CPU usage, memory usage, and network bandwidth across your entire cluster. These are real metrics flowing from your Kind nodes through Prometheus into Grafana.
- Return to the dashboards list by clicking Dashboards in the sidebar again.
- Search for Kubernetes / Compute Resources / Namespace (Pods) and open it.
- In the namespace dropdown at the top of the dashboard, select default.
You should see CPU and memory usage broken down by individual pods. Your three nginx-demo pods from earlier should appear here with their resource consumption.
What is Prometheus doing behind the scenes?
Prometheus scrapes metrics from every component in your cluster on a regular interval. It stores those metrics in a time-series database. Grafana queries Prometheus and renders the data as graphs and gauges.
The pre-built dashboards use PromQL (Prometheus Query Language) queries under the hood. In the Secret Mission, you will write your own PromQL queries to build a custom dashboard.
Before you move on, take a moment to predict: what do you think happens to these dashboard graphs if you were to scale your nginx-demo deployment up or down?
Your platform is now fully observable. You have infrastructure provisioned by code, applications deployed by Git, and cluster health visible in real time. Next up, the Secret Mission challenges you to build a custom Grafana dashboard with hand-crafted PromQL queries.
Secret mission
Build a Platform Health Dashboard
Pre-built dashboards show generic metrics. Platform engineers build custom dashboards that answer specific questions about their infrastructure. In this secret mission, you will compose PromQL queries into a hand-crafted Grafana dashboard that tells a story about your cluster's health.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing cloud costs.
Resources you used:
- Kind Kubernetes cluster platform-cluster running as Docker containers.
- ArgoCD, Prometheus, and Grafana running as pods inside the cluster.
- A GitHub repository (platform-gitops-repo) with your GitOps manifests.
- Local project files on your Desktop (Terraform configs, Kubernetes manifests, ArgoCD application files).
- Kubeconfig file at ~/.kube/platform-cluster-config.
Keep everything running
No action needed. Choose this if you are still actively building on your platform or want to experiment further.
- Leave Docker Desktop running. The Kind cluster persists across terminal sessions.
- Port-forwards (ArgoCD on localhost:8080, Grafana on localhost:3000) stop when you close the terminal. Restart them when needed by running the kubectl port-forward commands from earlier steps.
- Your GitHub repository, local project files, and kubeconfig file all remain on your machine.
- Docker Desktop uses system memory while running (typically 2-4 GB for this cluster). Keep this in mind if your machine has limited RAM.
Pause - I'll come back to this later
Shut down Docker Desktop to free up memory. Your cluster data is preserved and resumes when you reopen Docker Desktop.
- Stop any active port-forwards by pressing Ctrl+C in the terminal windows running them.
- Quit Docker Desktop from the system tray or menu bar icon.
- When you are ready to resume, reopen Docker Desktop. The Kind cluster starts automatically once Docker is running again.
- Re-run your kubectl port-forward commands to access ArgoCD and Grafana again.
Delete - I don't want to use this again
Remove all project resources and start fresh. This is permanent and cannot be undone.
- Navigate to the folder on your Desktop containing your .tf files and run this command to destroy the entire cluster:
terraform destroy
- Type yes when Terraform asks for confirmation. This removes the Kind cluster, all pods (ArgoCD, Prometheus, Grafana, nginx-demo), and all data inside it.
- Remove the kubeconfig file that Terraform created:
rm ~/.kube/platform-cluster-config
- Unset the KUBECONFIG environment variable in your current terminal:
unset KUBECONFIG
- Delete your local project files. Run the following from your Desktop:
cd ~/Desktop
rm -rf platform-gitops
- To delete the GitHub repository, go to your platform-gitops-repo repository on GitHub.
- Click Settings in the repository navigation bar.
- Scroll to the bottom of the page to the Danger Zone section.
- Click Delete this repository and follow the confirmation prompts.
What about Homebrew packages?
This project installed Docker Desktop, Terraform, kubectl, and Helm via Homebrew. These are general-purpose tools useful across many projects, so they are not removed here.
If you want to remove them later, you can run brew uninstall <package-name> for each tool (e.g. brew uninstall helm).
That's a Wrap!
That's a Wrap!
Nice work! You just built a complete local platform engineering environment from scratch. Your laptop now runs a fully declarative infrastructure stack that mirrors how production teams manage clusters, deploy applications, and monitor health in 2026.
Here's what you accomplished:
- Provisioned a Kubernetes cluster entirely through Terraform code. One terraform apply creates it, one terraform destroy removes it. Infrastructure as Code in action.
- Built a full GitOps pipeline with ArgoCD. You push a YAML change to GitHub, ArgoCD detects it, and your cluster updates automatically. You experienced the difference between drift detection and automated remediation firsthand.
- Deployed a production-grade observability stack with Prometheus and Grafana. Real-time CPU, memory, and pod metrics flow from your cluster into pre-built dashboards.
- Secret Mission: Built a custom Platform Health Grafana dashboard with hand-crafted PromQL queries for node CPU, pod count, and memory usage.
Ready to quiz yourself?