A declarative, production-patterned infrastructure platform blueprint that provisions a localized Kubernetes GitOps playground using Terraform, ArgoCD, and Elastic Cloud on Kubernetes (ECK) on a single-node Minikube cluster.
This repository demonstrates the platform engineering pattern of separating initial infrastructure bootstrapping (Push-based IaC via Terraform) from continuous application lifecycle management (Pull-based GitOps via ArgoCD).
[ Push-Based Infrastructure Setup ]
β
(1) Run: terraform apply
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β MINIKUBE CLUSTER β
β β
β ββββββββββββββββββββ ββββββββββββββββββ β
β β ArgoCD Namespace β β ECK Operator β β
β ββββββββββ¬ββββββββββ ββββββββββ¬ββββββββ β
βββββββββββββΌββββββββββββββββββββββββΌβββββββββββ
β β
β (2) Continuously β (3) Manages
β Polls & Syncs β Lifecycle
βΌ βΌ
βββββββββββββ βββββββββββββ
β Public β β State β
β Git Repo β β Resources β
βββββββββββββ βββββββββββββ
[ Pull-Based Continuous Reconciliation ]
Local machine configurations target a single-node Minikube instance using the native hashicorp/kubernetes and hashicorp/helm providers. Terraform initializes the core platform requirements, provisions the system namespaces, and injects the baseline orchestrators (argo-cd and eck-operator via official Helm charts) into the cluster, then hands off control entirely.
An ArgoCD root Application controller registers and watches the target repository paths over the public internet. When state modifications are pushed to the tracking branches, ArgoCD triggers an automated reconciliation cycle. It applies the declarations directly to the API server, instructing the ECK Operator to coordinate stateful workload rollouts (such as our resource-managed single-node Elasticsearch cluster).
.
βββ README.md # Comprehensive platform documentation
βββ root-application.yaml # ArgoCD App-of-Apps parent declaration
βββ terraform-bootstrap/
β βββ main.tf # Terraform infrastructure blueprint
βββ apps/
βββ elasticsearch/
βββ cluster.yaml # GitOps target Elasticsearch cluster manifest
This file is maintained inside the terraform-bootstrap/ directory of this repository.
Because ArgoCD is explicitly configured to isolate its tracking path to the apps/ directory, the Terraform infrastructure bootstrapping assets safely coexist within the same repository without interfering with the live GitOps continuous reconciliation loop.
terraform {
required_version = ">= 1.0.0"
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.24"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.12"
}
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
config_context = "minikube"
}
provider "helm" {
kubernetes {
config_path = "~/.kube/config"
config_context = "minikube"
}
}
# 1. Create dedicated namespace for Elastic workloads
resource "kubernetes_namespace" "elastic_system" {
metadata {
name = "elastic-system"
}
}
# 2. Deploy the Elastic Cloud on Kubernetes (ECK) Operator
resource "helm_release" "eck_operator" {
name = "eck-operator"
repository = "[https://helm.elastic.co](https://helm.elastic.co)"
chart = "eck-operator"
namespace = kubernetes_namespace.elastic_system.metadata[0].name
}
# 3. Deploy ArgoCD via official Helm Chart (Standard Profile)
resource "helm_release" "argocd" {
name = "argocd"
repository = "[https://argoproj.github.io/argo-helm](https://argoproj.github.io/argo-helm)"
chart = "argo-cd"
namespace = "argocd"
create_namespace = true
}
Apply this file locally via kubectl to instruct ArgoCD to lock onto your repository layout.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: elasticstack-platform
namespace: argocd
spec:
project: default
source:
repoURL: '[https://github.com/YOUR_USERNAME/eck-gitops-infra.git](https://github.com/YOUR_USERNAME/eck-gitops-infra.git)' # Replace with your public GitHub URL
targetRevision: HEAD
path: 'apps/elasticsearch'
destination:
server: '[https://kubernetes.default.svc](https://kubernetes.default.svc)'
namespace: elastic-system
syncPolicy:
automated:
prune: true
selfHeal: true
Place this configuration file inside your tracking repository. The formatting separates JVM heap configurations from structural container limit allocations to bypass runtime bootstrap errors.
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
name: internal-search
namespace: elastic-system
spec:
version: 9.0.0
nodeSets:
- name: master-data
count: 1
config:
node.roles: ["master", "data", "ingest"]
podTemplate:
spec:
containers:
- name: elasticsearch
env:
- name: ES_JAVA_OPTS
value: "-Xms1536m -Xmx1536m"
resources:
requests:
memory: 2Gi
cpu: "1"
limits:
memory: 2.5Gi
cpu: "1.5"
Elasticsearch utilizes Lucene memory mappings extensively. Set the system limits on your host operating system before spinning up Minikube:
sudo sysctl -w vm.max_map_count=262144
Launch the local cluster with an optimized resource configuration profile tailored to comfortably run the control loop and the data nodes:
minikube start --cpus=3 --memory=5632 --driver=docker
Navigate into your local bootstrap directory, initialize the providers, and execute the execution block:
cd terraform-bootstrap/
terraform init
terraform apply -auto-approve
With the operators active, execute the root bridge configuration to map the platform back to your code definitions:
cd ..
kubectl apply -f root-application.yaml
Fetch the automatically generated cluster administrator authentication details from your cluster state:
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo
Forward the UI service traffic lines directly to your host machine:
kubectl port-forward svc/argocd-server -n argocd 8080:443
Open https://localhost:8080 in your browser, connect using user admin, and watch your architecture render live!
To verify that the continuous delivery engine is actively tracking and protecting your desired state configuration, execute an intentional runtime state modification by manually deleting a structural cluster resource via the command line:
kubectl delete service internal-search-es-http -n elastic-system
- ArgoCD's background application loops flag a drift divergence instantly against the tracking commit.
- The controller blocks out the out-of-band mutation and forcefully regenerates the deleted configuration objects.
- The cluster automatically self-heals back to the immutable configuration state defined within your Git repository.
Here is the updated Markdown snippet to add directly to the bottom of your README.md. It documents the cluster verification steps using both the fast Kubernetes Custom Resource layer and the native Elastic HTTP API.
Once the cluster has successfully reconciled, you can monitor and verify the health of the stateful Elasticsearch nodes using either the Kubernetes abstraction layer or by hitting the native Elastic cluster APIs directly.
The ECK Operator continuously aggregates internal cluster health status directly up to the Custom Resource Definition (CRD) status block. View it instantly with:
kubectl get elasticsearch -n elastic-system
- Expected Output: The
HEALTHcolumn should readgreenoryellow(yellow is standard for single-node sandboxes because there are no secondary nodes to house index replicas), withNODESat1/1.
For comprehensive structural cluster metrics, query the native /_cluster/health API endpoint directly from your host terminal.
Open a port-forwarding channel to expose the cluster's internal secure HTTPS service to your local host (keep this running):
kubectl port-forward svc/internal-search-es-http -n elastic-system 9200:9200
In a separate terminal window, extract the automatically managed elastic user bootstrap password from its Kubernetes secret store:
PASSWORD=$(kubectl get secret internal-search-es-elastic-user -n elastic-system -o jsonpath="{.data.elastic}" | base64 -d)
Run a curl request against your local socket. The -k flag tells curl to bypass verification of the operator's self-signed TLS certificates:
curl -k -u elastic:$PASSWORD "https://localhost:9200/_cluster/health?pretty"
{
"cluster_name" : "internal-search",
"status" : "yellow",
"timed_out" : false,
"number_of_nodes" : 1,
"number_of_data_nodes" : 1,
"active_primary_shards" : 1,
"active_shards" : 1,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 1,
"number_of_pending_tasks" : 0
}
(Note: An internal status of "yellow" is the perfectly optimized target state for a single-node local environment. It simply indicates that primary data shards are active and healthy, but index replication loops are safely unassigned due to the lack of secondary hardware instances).
Depending on whether you want to save your progress for later or completely purge the local sandbox environment from your machine, choose one of the following teardown patterns:
If you want to free up your host machine's CPU and RAM right now but want to keep your Elasticsearch data indices, cluster configurations, and ArgoCD states completely intact for next time:
minikube stop- To Resume Later: Simply execute
minikube start. You do not need to re-run your Terraform or bootstrap manifests. The persistent disk states will mount automatically, and the ECK and ArgoCD control loops will self-heal back to a running green status within a couple of minutes.
If you are completely finished with this sandbox iteration and want to cleanly wipe out every resource, helm release, data volume, and namespace from your local machine, execute this exact structural teardown sequence:
Instruct ArgoCD to stop tracking your tracking repository. This gracefully deletes the root application controller and cascades down to clean up the Elasticsearch workloads and local storage volumes cleanly:
kubectl delete -f root-application.yamlNavigate to your isolated bootstrap directory and allow Terraform to cleanly unprovision the infrastructure orchestrators (ArgoCD and the ECK Operator Helm configurations) and system namespaces:
cd terraform-bootstrap/
terraform destroy -auto-approveFinally, completely destroy the local Minikube virtual machine/container engine profile to wipe out any remaining cache lines or host-path disk allocations:
minikube delete