A secure GitOps continuous-delivery project demonstrating how Kubernetes changes move from a developer branch through validation, protected pull requests, Argo CD reconciliation, and into a hardened Kubernetes workload.
This project demonstrates a secure GitOps continuous-delivery workflow using Argo CD, Kubernetes and GitHub Actions.
Kubernetes resources are defined declaratively in Git.
Changes follow a controlled delivery path:
Developer
↓
Feature Branch
↓
Pull Request
↓
CI Validation
↓
Protected Main Branch
↓
Argo CD
↓
Automated Reconciliation
↓
Kubernetes
↓
Healthy + Synced
GitHub Actions validates configuration before merge, while Argo CD continuously compares the desired state stored in Git with the Kubernetes cluster and reconciles differences automatically.
The project is currently demonstrated using a local Docker Desktop Kubernetes cluster.
The same GitOps model can later be adapted for managed Kubernetes environments such as Amazon EKS.
The project combines Git-based change control, CI validation, Argo CD reconciliation, Kubernetes workload security and runtime verification.
The design demonstrates the separation between:
Change authoring
↓
Configuration validation
↓
Merge approval
↓
Desired state
↓
Automated reconciliation
↓
Runtime verification
Git remains the source of truth while Argo CD is responsible for continuously driving the Kubernetes cluster toward that declared state.
flowchart LR
DEV["Developer"]
BRANCH["Feature branch"]
PR["Pull request"]
CI["GitHub Actions<br/>manifest validation"]
MAIN["Protected main"]
ARGO["Argo CD"]
K8S["Kubernetes cluster"]
DEV --> BRANCH
BRANCH --> PR
PR --> CI
CI --> MAIN
MAIN --> ARGO
ARGO --> K8S
subgraph NS["nginx namespace"]
SVC["ClusterIP Service"]
DEP["NGINX Deployment"]
POD1["Pod 1"]
POD2["Pod 2"]
SVC --> DEP
DEP --> POD1
DEP --> POD2
end
K8S --> SVC
GitHub
Stores the declarative desired state and provides pull-request-based change control.
GitHub Actions
Validates YAML quality and Kubernetes schemas before configuration is merged.
Protected main
Represents the approved desired state of the environment.
Argo CD
Continuously monitors Git and reconciles the Kubernetes cluster when the desired state changes.
Kubernetes
Runs the application workload and reports deployment and health status.
A typical change follows this lifecycle:
- A developer creates a feature branch.
- Kubernetes or Argo CD configuration is changed.
- A pull request is opened against
main. - GitHub Actions validates YAML quality and Kubernetes schemas.
- The protected branch allows the change to be merged after the required check passes.
- Argo CD detects the new commit on
main. - Argo CD compares the desired state in Git with the live Kubernetes state.
- Argo CD synchronizes any required changes.
- Kubernetes performs the rollout.
- Argo CD reports the application as
HealthyandSynced.
This demonstrates a core GitOps principle:
Infrastructure changes are introduced through Git rather than through unmanaged manual changes to the cluster.
- Kubernetes
- Argo CD
- Docker Desktop
- GitHub
- GitHub Actions
- GitHub branch protection
- NGINX
- Yamllint
- Kubeconform
- GitOps
platform-engineering-gitops-argocd/
├── .github/
│ └── workflows/
│ └── validate-manifests.yaml
│
├── argocd/
│ ├── application.yaml
│ └── project.yaml
│
├── docs/
│ └── screenshots/
│ ├── gitops-argocd-overview.png
│ ├── argocd-resource-tree.png
│ ├── kubectl-resources.png
│ ├── nginx-application.png
│ └── protected-pr-validation.png
│
├── manifests/
│ ├── deployment.yaml
│ └── service.yaml
│
└── README.md
argocd/application.yaml defines the application managed by Argo CD.
The Application configures:
- Git repository source
mainas the tracked revisionmanifests/as the deployment path- Target Kubernetes cluster
- Target
nginxnamespace - Automated synchronization
- Automatic pruning
- Self-healing
- Namespace creation
- Retry and exponential backoff behaviour
The automated policy allows Argo CD to restore the live cluster when it drifts from the desired state in Git.
argocd/project.yaml defines an Argo CD AppProject.
Rather than allowing unrestricted deployment privileges, the project limits:
- The approved Git repository
- The allowed Kubernetes destination
- The permitted namespace
- The permitted resource types
This provides a stronger security boundary around what the GitOps application can deploy.
manifests/deployment.yaml defines the NGINX workload.
The Deployment contains two replicas and uses a rolling-update strategy.
It also defines:
- Readiness probes
- Liveness probes
- CPU requests
- CPU limits
- Memory requests
- Memory limits
- ReplicaSet revision history
- Minimum ready time
- Deployment progress timeout
The deployment strategy uses:
maxUnavailable: 0
maxSurge: 1
allowing Kubernetes to introduce a replacement Pod before removing an existing ready replica.
manifests/service.yaml provides stable internal access to the NGINX Pods.
The Service:
- Selects Pods using the
app: nginxlabel - Exposes port
80 - Routes traffic to the named container HTTP port
- Uses
ClusterIP
The workload itself listens on port 8080 using the unprivileged NGINX image.
The NGINX workload was intentionally hardened rather than running with default container privileges.
The container:
- Runs as non-root UID
101 - Runs as non-root GID
101 - Uses the official unprivileged NGINX image
- Uses an image pinned by digest
- Uses a read-only root filesystem
- Prevents privilege escalation
- Drops all Linux capabilities
- Uses the
RuntimeDefaultseccomp profile - Does not automatically mount a Kubernetes ServiceAccount token
- Uses memory-backed temporary storage
These controls reduce the permissions available to the workload if the container is compromised.
NGINX Container
│
├── runAsNonRoot
├── UID / GID 101
├── readOnlyRootFilesystem
├── allowPrivilegeEscalation: false
├── capabilities: DROP ALL
├── RuntimeDefault seccomp
├── no automatic API token
└── digest-pinned image
Security is applied as part of the declarative Kubernetes configuration rather than being treated as a separate manual step.
The project also applies controls before Kubernetes configuration reaches the cluster.
These include:
- Pull-request-based changes
- Protected
mainbranch - Required CI validation
- Minimal GitHub Actions permissions
- Pinned GitHub Actions dependencies
- Pinned validation container image
- YAML linting
- Kubernetes schema validation
- Container image digest pinning
This creates two layers of protection:
Before Merge
↓
CI + Pull Request Controls
↓
Approved Desired State
↓
Argo CD Reconciliation
↓
Hardened Runtime Workload
GitHub Actions validates repository changes before they are merged.
Yamllint validates:
- YAML syntax
- Formatting
- Indentation
- Duplicate keys
- Trailing whitespace
- Missing final newlines
- Common YAML quality problems
The workflow checks:
manifests/
argocd/
.github/workflows/
Kubeconform performs strict schema validation across both the Kubernetes workload manifests and the Argo CD custom resources:
manifests/
argocd/
Standard Kubernetes resources are validated against the built-in Kubernetes schemas.
Argo CD Application and AppProject resources are validated against pinned CRD schemas from the CRDs catalog, ensuring the GitOps control-plane configuration is structurally validated alongside the workload it deploys.
The workflow includes:
- Strict Kubernetes schema validation
- Argo CD custom-resource schema validation
- Validation summary output
- Minimal
contents: readpermissions - Pinned GitHub Actions dependencies
- Pinned Kubeconform container image
- Pinned Argo CD CRD schema source
- Job timeout
- Concurrency cancellation
Passing CI confirms that both the Kubernetes workload manifests and the Argo CD configuration satisfy the configured structural validation rules.
Runtime behaviour is verified separately inside the Kubernetes cluster.
Install:
- Docker Desktop with Kubernetes enabled
kubectl- Git
- Argo CD CLI, optional but recommended
Confirm access:
kubectl config current-context
kubectl cluster-infoFor this project, the expected local context is:
docker-desktop
Create the namespace:
kubectl create namespace argocdInstall Argo CD:
kubectl apply \
--namespace argocd \
--filename https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlWait for the Argo CD deployments:
kubectl wait \
--namespace argocd \
--for=condition=Available \
deployment \
--all \
--timeout=300sConfirm the Pods:
kubectl get pods --namespace argocdForward a local port:
kubectl port-forward \
--namespace argocd \
service/argocd-server \
8082:443Open:
https://localhost:8082
The browser may display a certificate warning because the local Argo CD server uses a self-signed certificate.
Retrieve the initial administrator password:
kubectl get secret argocd-initial-admin-secret \
--namespace argocd \
--output jsonpath='{.data.password}' |
base64 --decode
echoSign in with:
username: admin
Apply the Argo CD project first:
kubectl apply --filename argocd/project.yamlThen apply the Application:
kubectl apply --filename argocd/application.yamlArgo CD will create the nginx namespace and synchronize the manifests automatically.
Check the Argo CD Application:
kubectl get application nginx --namespace argocdExpected status:
NAME SYNC STATUS HEALTH STATUS
nginx Synced Healthy
Check Kubernetes resources:
kubectl get all --namespace nginxVerify the rollout:
kubectl rollout status \
deployment/nginx \
--namespace nginxCheck the Pods:
kubectl get pods --namespace nginxSelect a Pod:
POD_NAME=$(kubectl get pods \
--namespace nginx \
--selector app=nginx \
--output jsonpath='{.items[0].metadata.name}')Check the user inside the container:
kubectl exec \
--namespace nginx \
"$POD_NAME" \
-- idExpected output should show UID and GID 101.
Check the Deployment configuration:
kubectl get deployment nginx \
--namespace nginx \
--output jsonpath='{.spec.template.spec.automountServiceAccountToken}{"\n"}'Expected:
false
The application does not require Kubernetes API access, so automatically exposing a ServiceAccount credential is unnecessary.
Forward the Service locally:
kubectl port-forward \
--namespace nginx \
service/nginx-service \
8080:80Open:
http://localhost:8080
Keep the terminal process running while accessing the application.
If port 8080 is already in use:
kubectl port-forward \
--namespace nginx \
service/nginx-service \
8081:80Then open:
http://localhost:8081
The screenshots below provide evidence of the complete GitOps workflow rather than showing configuration files alone.
The Argo CD resource tree shows the deployed application and its Kubernetes resources.
The application is reported as:
Synced
Healthy
This confirms that the live Kubernetes environment matches the desired state stored in Git.
The resource tree also provides visibility into the Deployment, Service and running Pods managed through the GitOps workflow.
Changes are introduced through pull requests rather than applied directly to the protected main branch.
The validation workflow checks YAML quality and Kubernetes manifest schemas before the change is allowed to become the approved desired state.
This example also documents the migration of NGINX to a hardened non-root workload using:
- Read-only filesystem
- Dropped Linux capabilities
- Unprivileged container image
- Digest pinning
- RuntimeDefault seccomp
- Disabled automatic ServiceAccount token mounting
The pull request provides an auditable record of both the engineering change and its validation.
The NGINX workload is accessible through the Kubernetes Service using local port forwarding.
This screenshot demonstrates that the Git-managed Kubernetes configuration produced a functioning application rather than only passing static validation.
The deployed resources can also be verified independently using kubectl.
This provides runtime evidence for:
- Deployment
- Service
- Pods
- Pod readiness
- Replica availability
- Namespace state
Using both Argo CD and kubectl provides visibility from the GitOps control plane and the Kubernetes runtime.
Check:
kubectl get namespace argocd
kubectl get pods --namespace argocdIf the namespace does not exist, install Argo CD before applying the Application and AppProject resources.
Request a hard refresh:
kubectl annotate application nginx \
--namespace argocd \
argocd.argoproj.io/refresh=hard \
--overwriteCheck again:
kubectl get application nginx --namespace argocdInspect the Application:
kubectl get application nginx \
--namespace argocd \
--output yamlVerify:
- Repository URL
- Target revision
- Manifest path
- Destination cluster
- Destination namespace
Stop the previous process with:
Ctrl+C
or choose another port:
kubectl port-forward \
--namespace nginx \
service/nginx-service \
8081:80Run Yamllint locally:
yamllint manifests argocd .github/workflowsRun Kubeconform:
docker run --rm \
--volume "${PWD}:/work" \
ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c \
-strict \
-summary \
/work/manifestsThe approved configuration stored in Git represents the desired state.
Infrastructure configuration should follow the same review practices as application code.
Configuration is checked before reaching the protected branch and subsequently being consumed by Argo CD.
The application workload receives only the permissions required to run.
Argo CD continuously detects drift and drives the environment back toward the declared desired state.
Passing CI verifies configuration structure.
It does not prove that the workload functions correctly.
Runtime verification is therefore performed separately using Kubernetes and Argo CD.
This project demonstrates:
- GitOps continuous delivery
- Declarative Kubernetes configuration
- Argo CD Applications
- Argo CD Projects
- Automated synchronization
- Automatic pruning
- Self-healing
- Protected pull-request workflows
- Kubernetes schema validation in CI
- YAML quality validation
- Secure non-root container execution
- Container image digest pinning
- Read-only container filesystems
- Kubernetes workload security controls
- Health probes
- CPU and memory requests and limits
- Rolling deployment strategies
- Operational verification
- Troubleshooting
- Technical documentation with runtime evidence
Planned improvements include:
- Add Kustomize base and environment overlays
- Add development, staging and production environments
- Add namespace
ResourceQuotaandLimitRangepolicies - Add a Kubernetes
NetworkPolicy - Add a
PodDisruptionBudget - Add topology-spread constraints
- Add Trivy container and configuration scanning
- Add Dependabot or Renovate
- Add Kyverno or Gatekeeper policy enforcement
- Add Prometheus metrics
- Add Grafana dashboards
- Add operational alerts
- Add deployment and incident runbooks
- Add Argo CD
ApplicationSet - Validate Argo CD custom resources in CI
- Provision Amazon EKS using Terraform or OpenTofu
The current project demonstrates a working local GitOps lifecycle:
Feature Branch
↓
Pull Request
↓
CI Validation
↓
Protected Main
↓
Argo CD
↓
Kubernetes
↓
Healthy + Synced
The application has been tested using Docker Desktop Kubernetes.
Cloud deployment to Amazon EKS remains a future extension rather than a current production deployment.
Olawale Azeez
Cloud Engineer | Platform Engineer | DevOps Engineer
Focused on Kubernetes, GitOps, Platform Engineering, AWS, Infrastructure as Code, CI/CD, observability and cloud-native engineering.
GitHub: https://github.com/AZ1600




