diff --git a/.gitignore b/.gitignore index 8cd0df3..cb733db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,46 @@ .vscode -.idea \ No newline at end of file +.idea + +# Node.js dependencies (frontend + gateway) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Python dependencies (quoteservice or others) +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ +env/ + +# Terraform +*.tfstate +*.tfstate.* +crash.log +*.tfvars +.terraform/ +.terraform.lock.hcl + +# Kustomize build output (just in case) +kustomization.yaml.backup +*.out + +# Minikube / kubectl +.kube/ + + +# IDE / Editor +.vscode/ +.idea/ +*.swp +*.swo + +# OS-specific +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ diff --git a/README.md b/README.md index 6f87592..388e272 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,269 @@ -# Simple Microservice Example +# Local GitOps on Minikube (Terraform + Argo CD) -A very simple microservice example with NodeJS, Python and Docker +Spin up a local Kubernetes cluster with **Minikube**, install **Argo CD** via Helm, and deploy your app GitOps-style — all with Terraform. -## Run the API gateway +Each component is a **standalone Terraform project** (cluster / argocd / application), and there's an optional **all-in-one stack** that orchestrates all projects for a one-shot deployment. -- Install `docker` and `docker-compose` according to your operating system +## Table of Contents -- Clone the repository and navigate to it +- [Repository Layout](#repository-layout) +- [Prerequisites](#prerequisites) +- [Getting Started](#getting-started) +- [Individual Project Deployment](#individual-project-deployment) +- [Manifest Structure](#manifest-structure) +- [Configuration](#configuration) +- [Accessing Argo CD](#accessing-argo-cd) +- [Cleanup](#cleanup) +- [Troubleshooting](#troubleshooting) +- [Notes](#notes) -- Run `docker-compose up` to start the services +## Repository Layout -- Try `GET http://YOUR_HOST:3000/api/status` to check whether application is running +``` +modules/ +├── cluster-minikube/ # Minikube cluster management (reusable) +├── argocd/ # Argo CD Helm installation (reusable) +└── application/ # Argo CD Project + Application (reusable) -## Build the frontend +projects/ +├── cluster/ # Standalone TF: creates Minikube cluster +├── argocd/ # Standalone TF: installs Argo CD +└── application/ # Standalone TF: registers repo + Argo App -The application uses a frontend written with plain html with jQuery and to style with Bulma. -This is built with webpack. This default application is built assuming you are using the `localhost`. +stacks/ +└── all-in-one/ # Optional: orchestrates all 3 projects +``` -To build this to fit your own **IP Address** please follow the steps before you running the `docker-compose up` +> Each **project** has its own `providers.tf`, `variables.tf`, and `terraform.tfvars`. +> Providers read `~/.kube/config`. Application project sets `config_context = var.cluster_name`. -- Install NodeJs on your system +## Prerequisites -- Go to FrontendApplication directory +- **Terraform** ≥ 1.10.0 +- **Minikube** + **Docker** (or another supported driver) +- **kubectl**, **Helm**, and **Kustomize** +- A Git repository with your Kustomize layout (see [Manifest Structure](#manifest-structure)) +- A GitHub **Personal Access Token (PAT)** with read access to that repository -- Run `npm install` or if you have yarn `yarn` to install packages +> On Windows, use **PowerShell** or **Git Bash**. All commands below are provided as single lines. -- Now you need to set the API Gateway for this frontend application. It can be any host you have. - - Let's say you are hosting this application on `http://example.com` then your `API_GATEWAY` would be this one. - - If you are hosting in some machine with IP `123.324.345.1` then your `API_GATEWAY` would be your IP. +## Getting Started -- To pass this setting to webpack build you need to set an Environment Variable - - Windows : `set API_GATEWAY=http://YOUR_HOST` - - Linux/Max : `API_GATEWAY=http://YOUR_HOST` - * Remember no / at the end of the URL to get your web app work +### 1. Configure Your Application Settings -- Now you can do `npm run build` or `yarn build` +Before deploying, you need to configure your GitHub repository and credentials: -- Check `dist/` folder for newly created index.html and the main.js +1. **Navigate to the application project:** + ```bash + cd projects/application/ + ``` -- Now run the `docker-compose up` on the root folder of project and check `http://YOUR_HOST:8080` to see web app +2. **Copy the example configuration:** + ```bash + cp terraform.tfvars.example terraform.tfvars + ``` -![image](https://user-images.githubusercontent.com/13379595/42726706-82eb0ae6-87b6-11e8-8456-d933b9dfa73b.png) +3. **Edit `terraform.tfvars` and update:** + - `github_repo_url`: Replace with your actual GitHub repository URL + - `github_pat`: Add your GitHub Personal Access Token (PAT) with read access to the repository + + **⚠️ Security Note:** Instead of putting your PAT directly in the file, you can set it as an environment variable: + ```bash + export TF_VAR_github_pat="ghp_XXXXXXXXXXXXXXXX" + ``` + Then leave `github_pat = ""` in the terraform.tfvars file. + +4. **Return to the repository root:** + ```bash + cd ../../ + ``` + +### 2. All-in-One Deployment + +The fastest way to get everything running! This stack orchestrates all three projects in the correct order. + +Run from `stacks/all-in-one/`: + +```bash +terraform init && terraform apply -auto-approve +``` + +The stack provides helpful outputs including Argo CD access instructions once deployment completes. + +## Individual Project Deployment + +If you prefer granular control, deploy each component individually. Run from the repository root - each project reads its **own** `terraform.tfvars`. + +### 1. Create Minikube Cluster +```bash +terraform -chdir=projects/cluster init -upgrade && terraform -chdir=projects/cluster apply -auto-approve +``` + +### 2. Install Argo CD +```bash +terraform -chdir=projects/argocd init -upgrade && terraform -chdir=projects/argocd apply -auto-approve +``` + +### 3. Deploy Application +```bash +terraform -chdir=projects/application init -upgrade && terraform -chdir=projects/application apply -auto-approve +``` + +## Manifest Structure + +Your Git repository should contain Kubernetes manifests organized with Kustomize. Here's the expected structure: + +``` +k8s/ +├── base/ +│ ├── namespace.yaml +│ ├── resourcequota.yaml +│ ├── rbac/ +│ │ ├── role-readonly.yaml +│ │ ├── role-readwrite.yaml +│ │ ├── rolebinding-readonly.yaml +│ │ └── rolebinding-readwrite.yaml +│ ├── apigateway/ +│ │ ├── deployment.yaml +│ │ ├── service.yaml +│ │ └── configmap.yaml +│ ├── quoteservice/ +│ │ ├── deployment.yaml +│ │ ├── service.yaml +│ │ └── configmap.yaml +│ ├── frontend/ +│ │ ├── deployment.yaml +│ │ ├── service.yaml +│ │ └── configmap.yaml +│ └── kustomization.yaml +└── overlays/ + └── dev/ + └── kustomization.yaml +``` + +The `kustomize_path` in your configuration should point to the overlay you want to deploy (e.g., `k8s/overlays/dev`). + +Configure each project by editing its respective `terraform.tfvars` file: + +### projects/cluster/terraform.tfvars +```hcl +cluster_name = "emumba-minikube-cluster" +driver = "docker" +nodes = 1 +cpus = 4 +memory = 8192 +kubernetes_version = "v1.34.0" +cni = "flannel" +extra_flags = ["--addons=ingress,metrics-server", "--preload=false"] +``` + +### projects/argocd/terraform.tfvars +```hcl +namespace = "argocd" +release_name = "argo-cd" +server_service_type = "ClusterIP" # or "NodePort" / "LoadBalancer" +chart_version = "8.5.7" +# extra_values_yaml = [] +``` + +### projects/application/terraform.tfvars +```hcl +cluster_name = "emumba-minikube-cluster" # Kubernetes context to use +argocd_namespace = "argocd" +application_namespace = "emumba-assessment" + +project_name = "emumba-deployment" +application_name = "emumba-assessment-app" + +github_repo_url = "https://github.com/your-org/your-repo.git" +github_pat = "" # Prefer env: TF_VAR_github_pat +kustomize_path = "k8s/overlays/dev" +target_revision = "HEAD" +``` + +## All-in-One Stack + +This stack orchestrates all three projects in the correct order (init → apply). + +Run from `stacks/all-in-one/`: + +```bash +terraform init && terraform apply -auto-approve +``` + +The stack provides helpful outputs including Argo CD access instructions. + +## Accessing Argo CD + +### Get Admin Password + +**Bash/Git Bash:** +```bash +kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo +``` + +**PowerShell:** +```powershell +$p=(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}"); [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($p)) +``` + +### Access the UI + +1. **Port-forward to Argo CD:** + ```bash + kubectl port-forward service/argo-cd-argocd-server -n argocd 8080:443 + ``` + +2. **Open:** https://localhost:8080 + +3. **Login:** + - Username: `admin` + - Password: (from commands above) + +## Cleanup + +Destroy resources in reverse order: + +### Individual Projects +```bash +terraform -chdir=projects/application destroy -auto-approve +terraform -chdir=projects/argocd destroy -auto-approve +terraform -chdir=projects/cluster destroy -auto-approve +``` + +### All-in-One Stack +From `stacks/all-in-one/`: +```bash +terraform destroy -auto-approve +``` + +## Troubleshooting + +### Context not found / Connection refused +- Ensure Minikube is running and `cluster_name` matches your Minikube profile +- Check: `kubectl config get-contexts` and `minikube profile list` + +### Argo CD CRDs missing +- Apply `projects/argocd` before `projects/application` + +### Port 8080 already in use +- Change the local port: `kubectl port-forward ... 9090:443` + +### Windows CRLF issues in shell scripts +- Use PowerShell for one-liners, or set `*.tf text eol=lf` in `.gitattributes` + +### Re-run a single stage +- Just rerun its one-liner (e.g., `terraform -chdir=projects/argocd apply -auto-approve`) + +## Notes + +- **Argo CD Helm values include:** + - `installCRDs = true` + - `server.insecure = true` + - Service type configurable via `server_service_type` + +- **Application project uses `kubernetes_manifest` to create:** + - Argo CD AppProject + - Argo CD Application (pointing at `github_repo_url` + `kustomize_path`) + +- **Security:** Always prefer `TF_VAR_github_pat` environment variable over committing tokens to files or shell history diff --git a/k8s/base/apigateway/configmap.yaml b/k8s/base/apigateway/configmap.yaml new file mode 100644 index 0000000..3aad3f4 --- /dev/null +++ b/k8s/base/apigateway/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: apigateway-config + namespace: emumba-assessment +data: + QUOTES_API: "http://quoteservice:5000" diff --git a/k8s/base/apigateway/deployment.yaml b/k8s/base/apigateway/deployment.yaml new file mode 100644 index 0000000..303f149 --- /dev/null +++ b/k8s/base/apigateway/deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: apigateway + namespace: emumba-assessment + labels: &albl + app.kubernetes.io/name: apigateway + app.kubernetes.io/part-of: emumba-assessment-k8s-iac +spec: + replicas: 2 + selector: { matchLabels: *albl } + template: + metadata: { labels: *albl } + spec: + containers: + - name: apigateway + image: annasali2/emumba-apigateway:latest + ports: [{ containerPort: 3000 }] + envFrom: [{ configMapRef: { name: apigateway-config } }] + readinessProbe: + httpGet: { path: /api/status, port: 3000 } + initialDelaySeconds: 5 + periodSeconds: 5 + livenessProbe: + httpGet: { path: /api/status, port: 3000 } + initialDelaySeconds: 10 + periodSeconds: 10 + resources: + requests: { cpu: "100m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: { matchLabels: *albl } diff --git a/k8s/base/apigateway/hpa.yaml b/k8s/base/apigateway/hpa.yaml new file mode 100644 index 0000000..9b78d46 --- /dev/null +++ b/k8s/base/apigateway/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: apigateway + namespace: emumba-assessment +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: apigateway + minReplicas: 2 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 diff --git a/k8s/base/apigateway/service.yaml b/k8s/base/apigateway/service.yaml new file mode 100644 index 0000000..0798f25 --- /dev/null +++ b/k8s/base/apigateway/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: apigateway + namespace: emumba-assessment +spec: + selector: + app.kubernetes.io/name: apigateway + ports: + - name: http + port: 3000 + targetPort: 3000 + protocol: TCP + type: ClusterIP diff --git a/k8s/base/frontend/configmap.yaml b/k8s/base/frontend/configmap.yaml new file mode 100644 index 0000000..752d8c5 --- /dev/null +++ b/k8s/base/frontend/configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontend-config + namespace: emumba-assessment +data: + API_GATEWAY: "http://apigateway:3000" \ No newline at end of file diff --git a/k8s/base/frontend/deployment.yaml b/k8s/base/frontend/deployment.yaml new file mode 100644 index 0000000..bbb02e0 --- /dev/null +++ b/k8s/base/frontend/deployment.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + namespace: emumba-assessment + labels: &lbl + app.kubernetes.io/name: frontend + app.kubernetes.io/part-of: emumba-assessment-k8s-iac +spec: + replicas: 2 + selector: { matchLabels: *lbl } + template: + metadata: { labels: *lbl } + spec: + containers: + - name: frontend + image: annasali2/emumba-frontend:latest + ports: [{ containerPort: 80 }] + envFrom: [{ configMapRef: { name: frontend-config } }] + readinessProbe: + httpGet: { path: /, port: 80 } + initialDelaySeconds: 5 + livenessProbe: + httpGet: { path: /, port: 80 } + initialDelaySeconds: 10 + resources: + requests: { cpu: "50m", memory: "64Mi" } + limits: { cpu: "250m", memory: "256Mi" } + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: { matchLabels: *lbl } diff --git a/k8s/base/frontend/hpa.yaml b/k8s/base/frontend/hpa.yaml new file mode 100644 index 0000000..4741f3b --- /dev/null +++ b/k8s/base/frontend/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: frontend + namespace: emumba-assessment +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: frontend + minReplicas: 2 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 diff --git a/k8s/base/frontend/service.yaml b/k8s/base/frontend/service.yaml new file mode 100644 index 0000000..064ecbd --- /dev/null +++ b/k8s/base/frontend/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend + namespace: emumba-assessment +spec: + selector: + app.kubernetes.io/name: frontend + ports: + - name: http + port: 80 + targetPort: 80 + protocol: TCP + type: ClusterIP diff --git a/k8s/base/kustomization.yaml b/k8s/base/kustomization.yaml new file mode 100644 index 0000000..ab89a5f --- /dev/null +++ b/k8s/base/kustomization.yaml @@ -0,0 +1,22 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: emumba-assessment +resources: + - namespace.yaml + - resourcequota.yaml + - rbac/role-readonly.yaml + - rbac/role-readwrite.yaml + - rbac/rolebinding-readonly.yaml + - rbac/rolebinding-readwrite.yaml + - apigateway/deployment.yaml + - apigateway/service.yaml + - apigateway/configmap.yaml + - apigateway/hpa.yaml + - quoteservice/deployment.yaml + - quoteservice/service.yaml + - quoteservice/configmap.yaml + - quoteservice/hpa.yaml + - frontend/deployment.yaml + - frontend/service.yaml + - frontend/configmap.yaml + - frontend/hpa.yaml diff --git a/k8s/base/namespace.yaml b/k8s/base/namespace.yaml new file mode 100644 index 0000000..385ad87 --- /dev/null +++ b/k8s/base/namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: emumba-assessment + labels: + app.kubernetes.io/part-of: emumba-assessment-k8s-iac diff --git a/k8s/base/quoteservice/configmap.yaml b/k8s/base/quoteservice/configmap.yaml new file mode 100644 index 0000000..864c271 --- /dev/null +++ b/k8s/base/quoteservice/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: quoteservice-config + namespace: emumba-assessment +data: {} diff --git a/k8s/base/quoteservice/deployment.yaml b/k8s/base/quoteservice/deployment.yaml new file mode 100644 index 0000000..c174c85 --- /dev/null +++ b/k8s/base/quoteservice/deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: quoteservice + namespace: emumba-assessment + labels: &qlbl + app.kubernetes.io/name: quoteservice + app.kubernetes.io/part-of: emumba-assessment-k8s-iac +spec: + replicas: 2 + selector: { matchLabels: *qlbl } + template: + metadata: { labels: *qlbl } + spec: + containers: + - name: quoteservice + image: annasali2/emumba-quoteservice:latest + ports: [{ containerPort: 5000 }] + envFrom: [{ configMapRef: { name: quoteservice-config } }] + resources: + requests: { cpu: "100m", memory: "128Mi" } + limits: { cpu: "500m", memory: "512Mi" } + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: { matchLabels: *qlbl } diff --git a/k8s/base/quoteservice/hpa.yaml b/k8s/base/quoteservice/hpa.yaml new file mode 100644 index 0000000..326a6b6 --- /dev/null +++ b/k8s/base/quoteservice/hpa.yaml @@ -0,0 +1,19 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: quoteservice + namespace: emumba-assessment +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: quoteservice + minReplicas: 2 + maxReplicas: 5 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 60 diff --git a/k8s/base/quoteservice/service.yaml b/k8s/base/quoteservice/service.yaml new file mode 100644 index 0000000..984028c --- /dev/null +++ b/k8s/base/quoteservice/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: quoteservice + namespace: emumba-assessment +spec: + selector: + app.kubernetes.io/name: quoteservice + ports: + - name: http + port: 5000 + targetPort: 5000 + protocol: TCP + type: ClusterIP diff --git a/k8s/base/rbac/role-readonly.yaml b/k8s/base/rbac/role-readonly.yaml new file mode 100644 index 0000000..8634153 --- /dev/null +++ b/k8s/base/rbac/role-readonly.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: readonly + namespace: emumba-assessment +rules: +- apiGroups: [""] + resources: ["pods","services","configmaps","endpoints"] + verbs: ["get","list","watch"] diff --git a/k8s/base/rbac/role-readwrite.yaml b/k8s/base/rbac/role-readwrite.yaml new file mode 100644 index 0000000..0e9744a --- /dev/null +++ b/k8s/base/rbac/role-readwrite.yaml @@ -0,0 +1,9 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: readwrite + namespace: emumba-assessment +rules: +- apiGroups: [""] + resources: ["pods","services","configmaps","endpoints"] + verbs: ["get","list","watch","create","update","patch","delete"] diff --git a/k8s/base/rbac/rolebinding-readonly.yaml b/k8s/base/rbac/rolebinding-readonly.yaml new file mode 100644 index 0000000..6e08b42 --- /dev/null +++ b/k8s/base/rbac/rolebinding-readonly.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: readonly-binding + namespace: emumba-assessment +subjects: +- kind: ServiceAccount + name: default + namespace: emumba-assessment +roleRef: + kind: Role + name: readonly + apiGroup: rbac.authorization.k8s.io diff --git a/k8s/base/rbac/rolebinding-readwrite.yaml b/k8s/base/rbac/rolebinding-readwrite.yaml new file mode 100644 index 0000000..8f26034 --- /dev/null +++ b/k8s/base/rbac/rolebinding-readwrite.yaml @@ -0,0 +1,13 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: readwrite-binding + namespace: emumba-assessment +subjects: +- kind: ServiceAccount + name: default + namespace: emumba-assessment +roleRef: + kind: Role + name: readwrite + apiGroup: rbac.authorization.k8s.io diff --git a/k8s/base/resourcequota.yaml b/k8s/base/resourcequota.yaml new file mode 100644 index 0000000..9e93b2a --- /dev/null +++ b/k8s/base/resourcequota.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ResourceQuota +metadata: + name: rq-standard + namespace: emumba-assessment +spec: + hard: + requests.cpu: "2" + requests.memory: 4Gi + limits.cpu: "4" + limits.memory: 8Gi + pods: "30" diff --git a/k8s/overlays/dev/kustomization.yaml b/k8s/overlays/dev/kustomization.yaml new file mode 100644 index 0000000..1e35dff --- /dev/null +++ b/k8s/overlays/dev/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ../../base +images: + - name: ghcr.io/your/repo/apigateway + newTag: dev + - name: ghcr.io/your/repo/quoteservice + newTag: dev + - name: ghcr.io/your/repo/frontend + newTag: dev +namespace: emumba-assessment \ No newline at end of file diff --git a/modules/application/main.tf b/modules/application/main.tf new file mode 100644 index 0000000..112a547 --- /dev/null +++ b/modules/application/main.tf @@ -0,0 +1,92 @@ +# App runtime namespace +resource "kubernetes_namespace" "app" { + metadata { + name = var.application_namespace + labels = { + "app.kubernetes.io/name" = var.application_name + "app.kubernetes.io/part-of" = var.cluster_name + "app.kubernetes.io/managed-by" = "terraform" + } + } +} + + +# Register the repo in Argo CD (Secret in argocd namespace) +resource "kubernetes_secret" "argocd_repo_github_https" { + metadata { + name = var.repo_secret_name + namespace = var.argocd_namespace + labels = { + "argocd.argoproj.io/secret-type" = "repository" + "app.kubernetes.io/part-of" = var.cluster_name + "app.kubernetes.io/managed-by" = "terraform" + } + } + type = "Opaque" + + # stringData lets us provide plaintext; provider encodes to data + data = { + type = "git" + url = var.github_repo_url + username = var.repo_username + password = var.github_pat + } +} + +# AppProject (scopes repos/destinations) +resource "kubernetes_manifest" "argocd_project" { + manifest = { + apiVersion = "argoproj.io/v1alpha1" + kind = "AppProject" + metadata = { + name = var.project_name + namespace = var.argocd_namespace + labels = { + "app.kubernetes.io/part-of" = var.cluster_name + "app.kubernetes.io/managed-by" = "terraform" + } + } + spec = { + description = "Project for ${var.cluster_name}" + sourceRepos = [var.github_repo_url] + destinations = [{ + namespace = var.application_namespace + server = "https://kubernetes.default.svc" + }] + clusterResourceWhitelist = [{ group = "*", kind = "*" }] + namespaceResourceWhitelist = [{ group = "*", kind = "*" }] + } + } +} + +# Argo CD Application +resource "kubernetes_manifest" "argocd_application" { + manifest = { + apiVersion = "argoproj.io/v1alpha1" + kind = "Application" + metadata = { + name = var.application_name + namespace = var.argocd_namespace + labels = { + "app.kubernetes.io/part-of" = var.cluster_name + "app.kubernetes.io/managed-by" = "terraform" + } + } + spec = { + project = var.project_name + source = { + repoURL = var.github_repo_url + targetRevision = var.target_revision + path = var.kustomize_path + } + destination = { + server = "https://kubernetes.default.svc" + namespace = var.application_namespace + } + syncPolicy = { + automated = { prune = true, selfHeal = true } + syncOptions = ["CreateNamespace=true"] + } + } + } +} diff --git a/modules/application/outputs.tf b/modules/application/outputs.tf new file mode 100644 index 0000000..5073528 --- /dev/null +++ b/modules/application/outputs.tf @@ -0,0 +1,3 @@ +output "application_namespace" { value = var.application_namespace } +output "application_name" { value = var.application_name } +output "project_name" { value = var.project_name } diff --git a/modules/application/variables.tf b/modules/application/variables.tf new file mode 100644 index 0000000..4aabef8 --- /dev/null +++ b/modules/application/variables.tf @@ -0,0 +1,73 @@ +variable "cluster_name" { + description = "Used in labels and for context-aware manifests (passed from project)" + type = string +} + +variable "argocd_namespace" { + description = "Namespace where Argo CD is installed" + type = string + default = "argocd" +} + +variable "application_namespace" { + description = "Namespace where the app will run" + type = string + default = "emumba-assessment" +} + +variable "project_name" { + description = "Argo CD AppProject name" + type = string + default = "emumba-deployment" +} + +variable "application_name" { + description = "Argo CD Application name" + type = string + default = "emumba-assessment-app" +} + +variable "github_repo_url" { + description = "Git repository URL for the app (https)" + type = string +} + +variable "github_pat" { + description = "GitHub personal access token (read access)" + type = string + sensitive = true +} + +variable "repo_username" { + description = "Repository username for basic auth" + type = string + default = "git" +} + +variable "repo_secret_name" { + description = "K8s Secret name registered in Argo CD" + type = string + default = "repo-github-emumba-https" +} + +variable "kustomize_path" { + description = "Path inside the repo to Kustomize overlay" + type = string + default = "k8s/overlays/dev" +} + +variable "target_revision" { + description = "Git revision (branch, tag, or commit SHA)" + type = string + default = "HEAD" +} + +terraform { + required_version = ">= 1.10.0" + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.38.0" + } + } +} diff --git a/modules/argocd/main.tf b/modules/argocd/main.tf new file mode 100644 index 0000000..6b3d29d --- /dev/null +++ b/modules/argocd/main.tf @@ -0,0 +1,41 @@ +resource "kubernetes_namespace" "argocd" { + metadata { + name = var.namespace + labels = { + "app.kubernetes.io/name" = "argocd" + "app.kubernetes.io/part-of" = "emumba-minikube-cluster" + "app.kubernetes.io/managed-by" = "terraform" + } + } +} + +locals { + base_values = yamlencode({ + installCRDs = true + configs = { + params = { + "server.insecure" = "true" + } + } + server = { + service = { + type = var.server_service_type + } + } + }) + merged_values = concat([local.base_values], var.extra_values_yaml) +} + +resource "helm_release" "argocd" { + name = var.release_name + repository = "https://argoproj.github.io/argo-helm" + chart = "argo-cd" + namespace = kubernetes_namespace.argocd.metadata[0].name + version = var.chart_version + + wait = true + timeout = 600 + + values = local.merged_values +} + diff --git a/modules/argocd/outputs.tf b/modules/argocd/outputs.tf new file mode 100644 index 0000000..0832e2b --- /dev/null +++ b/modules/argocd/outputs.tf @@ -0,0 +1,2 @@ +output "namespace" { value = var.namespace } +output "release_name" { value = helm_release.argocd.name } diff --git a/modules/argocd/variables.tf b/modules/argocd/variables.tf new file mode 100644 index 0000000..74ea92f --- /dev/null +++ b/modules/argocd/variables.tf @@ -0,0 +1,49 @@ +variable "namespace" { + description = "Namespace for Argo CD control plane" + type = string + default = "argocd" +} + +variable "release_name" { + description = "Helm release name" + type = string + default = "argo-cd" +} + +variable "server_service_type" { + description = "Service type for argocd-server (ClusterIP|NodePort|LoadBalancer)" + type = string + default = "ClusterIP" +} + +variable "chart_version" { + description = "argo-helm chart version" + type = string + default = "8.5.7" +} + +variable "extra_values_yaml" { + description = "Optional extra Helm values as YAML strings (each item is a full YAML document)" + type = list(string) + default = [] +} + +terraform { + + required_version = ">= 1.10.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.30.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.13.0" + } + null = { + source = "hashicorp/null" + version = ">= 3.2.4" + } + } +} diff --git a/modules/cluster-minikube/main.tf b/modules/cluster-minikube/main.tf new file mode 100644 index 0000000..9a618d8 --- /dev/null +++ b/modules/cluster-minikube/main.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">= 1.10.0" + required_providers { + null = { + source = "hashicorp/null" + version = ">= 3.2.0" + } + } +} + +resource "null_resource" "minikube" { + triggers = { + profile = var.cluster_name + } + + provisioner "local-exec" { + command = "minikube start --profile=${self.triggers.profile} --driver=${var.driver} --nodes=${var.nodes} --cpus=${var.cpus} --memory=${var.memory} --kubernetes-version=${var.kubernetes_version} --cni=${var.cni}" + } + + provisioner "local-exec" { + when = destroy + command = "minikube delete -p ${self.triggers.profile}" + } +} diff --git a/modules/cluster-minikube/outputs.tf b/modules/cluster-minikube/outputs.tf new file mode 100644 index 0000000..06e11aa --- /dev/null +++ b/modules/cluster-minikube/outputs.tf @@ -0,0 +1,4 @@ +output "profile_name" { + description = "Minikube profile name" + value = var.cluster_name +} diff --git a/modules/cluster-minikube/variables.tf b/modules/cluster-minikube/variables.tf new file mode 100644 index 0000000..1376382 --- /dev/null +++ b/modules/cluster-minikube/variables.tf @@ -0,0 +1,34 @@ +variable "cluster_name" { + type = string + default = "emumba-minikube-cluster" +} + +variable "driver" { + type = string + default = "docker" +} + +variable "nodes" { + type = number + default = 1 +} + +variable "cpus" { + type = number + default = 2 +} + +variable "memory" { + type = string + default = "4g" +} + +variable "kubernetes_version" { + type = string + default = "v1.34.0" +} + +variable "cni" { + type = string + default = "flannel" +} diff --git a/projects/application/main.tf b/projects/application/main.tf new file mode 100644 index 0000000..13577c8 --- /dev/null +++ b/projects/application/main.tf @@ -0,0 +1,22 @@ +module "application" { + source = "../../modules/application" + + cluster_name = var.cluster_name + argocd_namespace = var.argocd_namespace + application_namespace = var.application_namespace + + project_name = var.project_name + application_name = var.application_name + + github_repo_url = var.github_repo_url + github_pat = var.github_pat + repo_username = var.repo_username + repo_secret_name = var.repo_secret_name + + kustomize_path = var.kustomize_path + target_revision = var.target_revision + + providers = { + kubernetes = kubernetes + } +} diff --git a/projects/application/outputs.tf b/projects/application/outputs.tf new file mode 100644 index 0000000..672de66 --- /dev/null +++ b/projects/application/outputs.tf @@ -0,0 +1,3 @@ +output "application_namespace" { value = module.application.application_namespace } +output "application_name" { value = module.application.application_name } +output "project_name" { value = module.application.project_name } diff --git a/projects/application/providers.tf b/projects/application/providers.tf new file mode 100644 index 0000000..06a6aba --- /dev/null +++ b/projects/application/providers.tf @@ -0,0 +1,20 @@ +terraform { + required_version = ">= 1.6.0" + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.38.0" + } + } +} + +variable "cluster_name" { + description = "Minikube cluster name (used as kube context)" + type = string + default = "emumba-minikube-cluster" +} + +provider "kubernetes" { + config_path = pathexpand("~/.kube/config") + config_context = var.cluster_name +} diff --git a/projects/application/terraform.tfvars.example b/projects/application/terraform.tfvars.example new file mode 100644 index 0000000..af2eadc --- /dev/null +++ b/projects/application/terraform.tfvars.example @@ -0,0 +1,21 @@ +# Use the Minikube profile name you started the cluster with: +cluster_name = "emumba-minikube-cluster" + +# Argo CD location +argocd_namespace = "argocd" + +# App destination namespace +application_namespace = "emumba-assessment" + +# Argo CD objects +project_name = "emumba-deployment" +application_name = "emumba-assessment-app" + +# Repo details +github_repo_url = "https://github.com/aliannus2/emumba-assessment-k8s-iac.git" +github_pat = "ghp_Qasdfasdgasdgasfs" +repo_username = "aliannus2" + +# Kustomize path and revision +kustomize_path = "k8s/overlays/dev" +target_revision = "HEAD" diff --git a/projects/application/variabels.tf b/projects/application/variabels.tf new file mode 100644 index 0000000..7cc6b5a --- /dev/null +++ b/projects/application/variabels.tf @@ -0,0 +1,46 @@ +# You already defined cluster_name in providers.tf + +variable "argocd_namespace" { + type = string + default = "argocd" +} +variable "application_namespace" { + type = string + default = "emumba-assessment" +} +variable "project_name" { + type = string + default = "emumba-deployment" +} +variable "application_name" { + type = string + default = "emumba-assessment-app" +} + +variable "github_repo_url" { + type = string + description = "HTTPS repo URL that Argo CD will pull from" +} + +variable "github_pat" { + type = string + sensitive = true + description = "GitHub token with read access" +} + +variable "repo_username" { + type = string + default = "aliannus2" +} +variable "repo_secret_name" { + type = string + default = "repo-github-emumba-https" +} +variable "kustomize_path" { + type = string + default = "k8s/overlays/dev" +} +variable "target_revision" { + type = string + default = "HEAD" +} diff --git a/projects/argocd/main.tf b/projects/argocd/main.tf new file mode 100644 index 0000000..23f3391 --- /dev/null +++ b/projects/argocd/main.tf @@ -0,0 +1,15 @@ +module "argocd" { + source = "../../modules/argocd" + + namespace = var.namespace + release_name = var.release_name + server_service_type = var.server_service_type + chart_version = var.chart_version + extra_values_yaml = var.extra_values_yaml + + providers = { + kubernetes = kubernetes + helm = helm + null = null + } +} diff --git a/projects/argocd/outputs.tf b/projects/argocd/outputs.tf new file mode 100644 index 0000000..fcea2ec --- /dev/null +++ b/projects/argocd/outputs.tf @@ -0,0 +1,2 @@ +output "namespace" { value = module.argocd.namespace } +output "release" { value = module.argocd.release_name } diff --git a/projects/argocd/providers.tf b/projects/argocd/providers.tf new file mode 100644 index 0000000..d17a0b2 --- /dev/null +++ b/projects/argocd/providers.tf @@ -0,0 +1,31 @@ +terraform { + + required_version = ">= 1.10.0" + + required_providers { + kubernetes = { + source = "hashicorp/kubernetes" + version = ">= 2.30.0" + } + helm = { + source = "hashicorp/helm" + version = ">= 2.13.0" + } + null = { + source = "hashicorp/null" + version = ">= 3.2.4" + } + } +} + +provider "kubernetes" { + config_path = pathexpand("~/.kube/config") + config_context = var.cluster_name +} + +provider "helm" { + kubernetes = { + config_path = pathexpand("~/.kube/config") + config_context = var.cluster_name + } +} diff --git a/projects/argocd/terraform.tfvars.example b/projects/argocd/terraform.tfvars.example new file mode 100644 index 0000000..b70627e --- /dev/null +++ b/projects/argocd/terraform.tfvars.example @@ -0,0 +1,3 @@ +# kubeconfig is hardcoded; nothing to set here unless you want overrides +# server_service_type = "NodePort" +# chart_version = "8.5.7" diff --git a/projects/argocd/variables.tf b/projects/argocd/variables.tf new file mode 100644 index 0000000..53da68e --- /dev/null +++ b/projects/argocd/variables.tf @@ -0,0 +1,24 @@ +variable "namespace" { + type = string + default = "argocd" +} +variable "release_name" { + type = string + default = "argo-cd" +} +variable "server_service_type" { + type = string + default = "NodePort" +} +variable "chart_version" { + type = string + default = "8.5.7" +} +variable "extra_values_yaml" { + type = list(string) + default = [] +} +variable "cluster_name" { + type = string + default = "emumba-minikube-cluster" +} diff --git a/projects/cluster/main.tf b/projects/cluster/main.tf new file mode 100644 index 0000000..d2f7160 --- /dev/null +++ b/projects/cluster/main.tf @@ -0,0 +1,11 @@ +module "cluster_minikube" { + source = "../../modules/cluster-minikube" + + cluster_name = var.cluster_name + driver = var.driver + nodes = var.nodes + cpus = var.cpus + memory = var.memory + kubernetes_version = var.kubernetes_version + cni = var.cni +} diff --git a/projects/cluster/outputs.tf b/projects/cluster/outputs.tf new file mode 100644 index 0000000..b40d75b --- /dev/null +++ b/projects/cluster/outputs.tf @@ -0,0 +1 @@ +output "profile_name" { value = module.cluster_minikube.profile_name } diff --git a/projects/cluster/provider.tf b/projects/cluster/provider.tf new file mode 100644 index 0000000..2ec933e --- /dev/null +++ b/projects/cluster/provider.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10.0" + required_providers { + null = { + source = "hashicorp/null" + version = "3.2.4" + } + } +} diff --git a/projects/cluster/terraform.tfvars.example b/projects/cluster/terraform.tfvars.example new file mode 100644 index 0000000..d804dfb --- /dev/null +++ b/projects/cluster/terraform.tfvars.example @@ -0,0 +1,22 @@ +# Override defaults here if you like + +cluster_name = "emumba-minikube-cluster" +driver = "docker" +nodes = 1 +cpus = 4 +memory = 8192 +kubernetes_version = "v1.34.0" +cni = "flannel" + +# e.g., add-ons or preload off +extra_flags = [ + "--addons=ingress,metrics-server", + "--preload=false" +] + +wait_for_nodes = true +delete_on_destroy = true + +# If you prefer a custom kubeconfig path, set it here; +# otherwise Minikube will update ~/.kube/config which is what the next projects use. +kubeconfig_path = pathexpand("~/.kube/config") diff --git a/projects/cluster/variables.tf b/projects/cluster/variables.tf new file mode 100644 index 0000000..045b894 --- /dev/null +++ b/projects/cluster/variables.tf @@ -0,0 +1,28 @@ +variable "cluster_name" { + type = string + default = "emumba-minikube-cluster" +} +variable "driver" { + type = string + default = "docker" +} +variable "nodes" { + type = number + default = 1 +} +variable "cpus" { + type = number + default = 4 +} +variable "memory" { + type = number + default = 8192 +} +variable "kubernetes_version" { + type = string + default = "v1.34.0" +} +variable "cni" { + type = string + default = "flannel" +} diff --git a/stacks/all-in-one/main.tf b/stacks/all-in-one/main.tf new file mode 100644 index 0000000..df2facb --- /dev/null +++ b/stacks/all-in-one/main.tf @@ -0,0 +1,88 @@ +locals { + cluster_dir = "../../projects/cluster" + argocd_dir = "../../projects/argocd" + application_dir = "../../projects/application" +} + +# 1) Cluster INIT +resource "null_resource" "cluster_init" { + triggers = { + dir = local.cluster_dir + bump = "1" + } + provisioner "local-exec" { + command = "terraform -chdir=${self.triggers.dir} init -upgrade" + } +} + +# 1) Cluster APPLY +resource "null_resource" "cluster_apply" { + triggers = { + init_done = null_resource.cluster_init.id + dir = local.cluster_dir + bump = "1" + } + provisioner "local-exec" { + command = "terraform -chdir=${self.triggers.dir} apply -auto-approve" + } + provisioner "local-exec" { + when = destroy + command = "terraform -chdir=${self.triggers.dir} destroy -auto-approve" + } +} + +# 2) Argo CD INIT +resource "null_resource" "argocd_init" { + triggers = { + cluster_done = null_resource.cluster_apply.id + dir = local.argocd_dir + bump = "1" + } + provisioner "local-exec" { + command = "terraform -chdir=${self.triggers.dir} init -upgrade" + } +} + +# 2) Argo CD APPLY +resource "null_resource" "argocd_apply" { + triggers = { + init_done = null_resource.argocd_init.id + dir = local.argocd_dir + bump = "1" + } + provisioner "local-exec" { + command = "terraform -chdir=${self.triggers.dir} apply -auto-approve" + } + provisioner "local-exec" { + when = destroy + command = "terraform -chdir=${self.triggers.dir} destroy -auto-approve" + } +} + +# 3) Application INIT +resource "null_resource" "application_init" { + triggers = { + argocd_done = null_resource.argocd_apply.id + dir = local.application_dir + bump = "1" + } + provisioner "local-exec" { + command = "terraform -chdir=${self.triggers.dir} init -upgrade" + } +} + +# 3) Application APPLY +resource "null_resource" "application_apply" { + triggers = { + init_done = null_resource.application_init.id + dir = local.application_dir + bump = "1" + } + provisioner "local-exec" { + command = "terraform -chdir=${self.triggers.dir} apply -auto-approve" + } + provisioner "local-exec" { + when = destroy + command = "terraform -chdir=${self.triggers.dir} destroy -auto-approve" + } +} diff --git a/stacks/all-in-one/outputs.tf b/stacks/all-in-one/outputs.tf new file mode 100644 index 0000000..4a5f380 --- /dev/null +++ b/stacks/all-in-one/outputs.tf @@ -0,0 +1,24 @@ +output "argocd_username" { + value = "admin" + description = "Argo CD initial username." +} + +output "argocd_admin_password_bash_cmd" { + value = "kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=\"{.data.password}\" | base64 -d; echo" + description = "Run in Bash/zsh/Git Bash to print the Argo CD admin password." +} + +output "argocd_admin_password_powershell_cmd" { + value = "$p=(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath=\"{.data.password}\"); [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($p))" + description = "Run in Windows PowerShell to print the Argo CD admin password." +} + +output "argocd_port_forward_cmd" { + value = "kubectl port-forward service/argo-cd-argocd-server -n argocd 8080:443" + description = "Run this to access Argo CD at https://localhost:8080." +} + +output "argocd_url" { + value = "https://localhost:8080" + description = "Open this in your browser after running the port-forward command." +} \ No newline at end of file diff --git a/stacks/all-in-one/providers.tf b/stacks/all-in-one/providers.tf new file mode 100644 index 0000000..7e01038 --- /dev/null +++ b/stacks/all-in-one/providers.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.10.0" + required_providers { + null = { + source = "hashicorp/null" + version = ">= 3.2.4" + } + } +} diff --git a/stacks/all-in-one/variables.tf b/stacks/all-in-one/variables.tf new file mode 100644 index 0000000..fbc73c8 --- /dev/null +++ b/stacks/all-in-one/variables.tf @@ -0,0 +1,18 @@ +# Minimal inputs — keep everything else defaulted in each project. + +variable "cluster_name" { + description = "Minikube profile / kube context used by Application project" + type = string + default = "emumba-minikube-cluster" +} + +variable "github_repo_url" { + description = "HTTPS repo URL that Argo CD Application will sync" + type = string +} + +variable "github_pat" { + description = "GitHub token with read access" + type = string + sensitive = true +}