diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..0d96032 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml new file mode 100644 index 0000000..7163d78 --- /dev/null +++ b/.github/workflows/cd.yaml @@ -0,0 +1,37 @@ +name: CD Pipeline + +on: + push: + branches: + - master # Production + - stg # Staging + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install ArgoCD CLI + run: | + curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 + chmod +x /usr/local/bin/argocd + + - name: ArgoCD Login + env: + ARGOCD_SERVER: ${{ secrets.ARGOCD_SERVER }} + ARGOCD_USERNAME: admin + ARGOCD_PASSWORD: ${{ secrets.ARGOCD_PASSWORD }} + run: | + argocd login $ARGOCD_SERVER --insecure --username $ARGOCD_USERNAME --password $ARGOCD_PASSWORD + + - name: Deploy Application to ArgoCD + run: | + if [[ "${{ github.ref }}" == "refs/heads/master" ]]; then + argocd app sync p2p-devops-test || \ + argocd app create p2p-devops-test --file https://raw.githubusercontent.com/draju1980/p2p-devops-test/refs/heads/master/argocd/helm/prod-p2p-devops-app.yaml + elif [[ "${{ github.ref }}" == "refs/heads/stg" ]]; then + argocd app sync p2p-devops-test || \ + argocd app create p2p-devops-test --file https://raw.githubusercontent.com/draju1980/p2p-devops-test/refs/heads/stg/argocd/helm/stg-p2p-devops-app.yaml + fi diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..61689cd --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,78 @@ +name: Golang Test, Lint, Format, Build, Publish Docker Image for p2p-devops-test + +on: + push: + branches: + - master + - stg + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test-lint-format: + name: Test, Lint, and Format + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.23.2' + cache: true + + - name: Install dependencies + run: go mod tidy + + - name: Install dependencies + run: go mod download + + - name: Run tests + run: go test ./... + + - name: Lint code + run: | + go install golang.org/x/lint/golint@latest + golint ./... + + - name: Format code + run: gofmt -s -w . + + build-and-publish: + name: Build and Publish Docker Image + runs-on: ubuntu-latest + needs: test-lint-format + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker image + id: meta + uses: docker/metadata-action@v1 + with: + images: ghcr.io/${{ github.repository_owner }}/${{ github.repository }} + + - name: Build and push Docker image + id: push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b93e48a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.23.2 + +WORKDIR /app + +COPY go.mod ./ +RUN go mod tidy +RUN go mod download + +COPY *.go ./ + +RUN go build -o /p2p-devops-test + +EXPOSE 3000 + +CMD [ "/p2p-devops-test" ] \ No newline at end of file diff --git a/Kustomization/.DS_Store b/Kustomization/.DS_Store new file mode 100644 index 0000000..f432113 Binary files /dev/null and b/Kustomization/.DS_Store differ diff --git a/Kustomization/base/deployment.yaml b/Kustomization/base/deployment.yaml new file mode 100644 index 0000000..4671243 --- /dev/null +++ b/Kustomization/base/deployment.yaml @@ -0,0 +1,45 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: p2p-devops-test + namespace: p2p-devops-test +spec: + replicas: 1 + selector: + matchLabels: + app: p2p-devops-test + template: + metadata: + labels: + app: p2p-devops-test + spec: + containers: + - name: p2p-devops-test-pod + image: ghcr.io/draju1980/draju1980/p2p-devops-test:master + ports: + - containerPort: 3000 + resources: + limits: + cpu: "512m" + memory: "512Mi" + requests: + cpu: "256m" + memory: "256Mi" + livenessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + diff --git a/Kustomization/base/kustomization.yaml b/Kustomization/base/kustomization.yaml new file mode 100644 index 0000000..f060a86 --- /dev/null +++ b/Kustomization/base/kustomization.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: p2p-devops-test + +resources: + - namespace.yaml + - service.yaml + - deployment.yaml diff --git a/Kustomization/base/namespace.yaml b/Kustomization/base/namespace.yaml new file mode 100644 index 0000000..7e06c42 --- /dev/null +++ b/Kustomization/base/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: p2p-devops-test diff --git a/Kustomization/base/service.yaml b/Kustomization/base/service.yaml new file mode 100644 index 0000000..e4846f2 --- /dev/null +++ b/Kustomization/base/service.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: p2p-devops-test-svc + namespace: p2p-devops-test +spec: + selector: + app: p2p-devops-test + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + type: LoadBalancer diff --git a/Kustomization/dev/horizontalpodautoscaler.yaml b/Kustomization/dev/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..e92e4ff --- /dev/null +++ b/Kustomization/dev/horizontalpodautoscaler.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: p2p-devops-test-hpa + namespace: p2p-devops-test +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: p2p-devops-test + minReplicas: 1 + maxReplicas: 1 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 75 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 75 diff --git a/Kustomization/dev/kustomization.yaml b/Kustomization/dev/kustomization.yaml new file mode 100644 index 0000000..a11ab1a --- /dev/null +++ b/Kustomization/dev/kustomization.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: p2p-devops-test-dev + +resources: + - ../base # This is the base directory + - horizontalpodautoscaler.yaml + diff --git a/Kustomization/prod/horizontalpodautoscaler.yaml b/Kustomization/prod/horizontalpodautoscaler.yaml new file mode 100644 index 0000000..7237b4d --- /dev/null +++ b/Kustomization/prod/horizontalpodautoscaler.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: p2p-devops-test-hpa + namespace: p2p-devops-test +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: p2p-devops-test + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 75 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 75 diff --git a/Kustomization/prod/kustomization.yaml b/Kustomization/prod/kustomization.yaml new file mode 100644 index 0000000..b0bb73c --- /dev/null +++ b/Kustomization/prod/kustomization.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: p2p-devops-test-prod + +resources: + - ../base # This is the base directory + - horizontalpodautoscaler.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..774a9a2 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +## Solution Summary + +For this technical challenge, I set up a local Minikube Kubernetes cluster, forked the p2p-devops-test repository, and enhanced the Go application by adding health endpoints to support Kubernetes readiness and liveness probes. I initially deployed the application using Kustomize while familiarizing myself with Helm, then created Helm charts for a fault-tolerant, scalable production setup and a minimal staging configuration. I installed ArgoCD on Minikube and configured an ArgoCD application manifest to manage deployments using GitOps with auto-sync enabled. Additionally, I set up a GitFlow-based CD pipeline to deploy across environments through ArgoCD, with options for further automation using GitHub Actions. + + +## Solution Design: + +![image](https://github.com/user-attachments/assets/1f5f238e-c57c-49d5-8962-ca5147a0579b) + + +## Solution Outline: + +Here’s a refined solution outline with a breakdown of each component and its role in achieving a structured, automated CI/CD workflow with GitOps principles, + +### 1. Development Workflow (Local Setup on Minikube) +#### Enhancing and Testing the Application: + +* Containerization: Use Docker to build and containerize the Go application, including essential health endpoints for observability. + +* Local Validation: Run the container on Minikube, leveraging kubectl port-forward to validate Kubernetes readiness and liveness probes. This enables quick checks for the application’s responsiveness and stability. + +#### Local Deployment on Minikube: + +* Initial Testing with Kustomize: Start by deploying with Kustomize for fast configuration testing and validation within the Minikube environment. + +* Transition to Helm: Shift to Helm to manage production-level configurations, using Helm charts for more streamlined adjustments and standardized deployment practices. + +* Iterative Deployment: Test deployment changes iteratively on Minikube to optimize configurations before transitioning to production standards. + + +### 2. GitFlow-Based Continuous Deployment Pipeline (Staging and Production) + +#### CI/CD Pipeline Integration with GitHub Actions: + +##### Automated Workflows: Set up GitHub Actions workflows to automate the build, test, and deployment processes. + +* Docker Build and Push: Build Docker images and push them to the GitHub Container Registry, streamlining container updates. + +* Automated Testing: Execute tests on every pull request, ensuring code quality and stability. + +* Continuous Delivery with ArgoCD: Trigger ArgoCD syncs automatically upon successful merges to the master branch, following GitFlow practices for streamlined deployment to production and staging environments. + +### 3. Automated Deployment with ArgoCD + +#### ArgoCD Application Configuration: + +* Application Manifest: Define an ArgoCD application manifest pointing to the Helm chart repository for automated, GitOps-driven deployments. + +* GitOps Sync: Configure ArgoCD to monitor the master branch for production and stg branch for staging, enabling auto-sync capabilities to automate deployment processes when changes are merged. + +#### Local Minikube ArgoCD Instance: + +* Local Testing with Minikube: Running ArgoCD on Minikube enables efficient, iterative testing of the GitOps deployment model before moving to a cloud-hosted environment. + +* Cloud Environment Preparation: For cloud environments, ensure ARGOCD_SERVER IP and ARGOCD_PASSWORD are updated in repository secrets to support ArgoCD login for continuous deployment. + +### 4. Helm Chart Setup for Environment-Specific Deployments + +#### Production Helm Chart: + +##### Fault Tolerance and Scalability: + +* Configure multiple replicas to ensure fault tolerance. +* Apply Kubernetes readiness and liveness probes to support automated rolling updates and proactive health checks. + +##### Resource Management: Optimize resources to ensure the application’s high availability and responsiveness under production loads. + + +#### Staging Helm Chart: + +##### Resource Efficiency: + +* Limit replicas and resource usage, keeping it minimal while reflecting production configurations to ensure parity. + +##### Environment-Specific Customization: Use Kustomize overlays if additional staging-specific configurations are needed, enhancing testing without altering production specifications. + +### 5. Monitoring and Health Checks + +#### Kubernetes Readiness and Liveness Probes: + +* Automated Health Checks: Implement Kubernetes readiness and liveness probes in the Helm charts, allowing Kubernetes to manage restarts or rolling updates if a pod becomes unhealthy. + +* Enhanced Observability: These probes enable both the Minikube and production clusters to maintain high availability, handling restarts if an issue is detected in real time. + + diff --git a/argocd/.DS_Store b/argocd/.DS_Store new file mode 100644 index 0000000..ba07d83 Binary files /dev/null and b/argocd/.DS_Store differ diff --git a/argocd/helm/prod-p2p-devops-app.yaml b/argocd/helm/prod-p2p-devops-app.yaml new file mode 100644 index 0000000..97250ed --- /dev/null +++ b/argocd/helm/prod-p2p-devops-app.yaml @@ -0,0 +1,27 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: p2p-devops-test +spec: + destination: + name: '' + server: https://kubernetes.default.svc + source: + path: helm + repoURL: git@github.com:draju1980/p2p-devops-test.git + targetRevision: HEAD + helm: + valueFiles: + - values.production.yaml + sources: [] + project: default + syncPolicy: + automated: + prune: false + selfHeal: true + retry: + limit: 2 + backoff: + duration: 5s + maxDuration: 3m0s + factor: 2 \ No newline at end of file diff --git a/argocd/helm/stg-p2p-devops-app.yaml b/argocd/helm/stg-p2p-devops-app.yaml new file mode 100644 index 0000000..98173bb --- /dev/null +++ b/argocd/helm/stg-p2p-devops-app.yaml @@ -0,0 +1,27 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: p2p-devops-test +spec: + destination: + name: '' + server: https://kubernetes.default.svc + source: + path: helm + repoURL: git@github.com:draju1980/p2p-devops-test.git + targetRevision: HEAD + helm: + valueFiles: + - values.staging.yaml + sources: [] + project: default + syncPolicy: + automated: + prune: false + selfHeal: true + retry: + limit: 2 + backoff: + duration: 5s + maxDuration: 3m0s + factor: 2 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3df2e74 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module p2p-devops-test + +go 1.23.2 diff --git a/helm/Chart.yaml b/helm/Chart.yaml new file mode 100644 index 0000000..0cafe80 --- /dev/null +++ b/helm/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: p2p-devops-app +description: A Helm chart for deploying p2p devops test application +version: 0.1.0 \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml new file mode 100644 index 0000000..1e97dae --- /dev/null +++ b/helm/templates/deployment.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: "{{ .Release.Name }}-deployment" + namespace: {{ .Release.Namespace }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ .Release.Name }} + spec: + containers: + - name: "{{ .Release.Name }}" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + ports: + - containerPort: {{ .Values.service.port }} + resources: + limits: + cpu: "512m" + memory: "512Mi" + requests: + cpu: "256m" + memory: "256Mi" + livenessProbe: + httpGet: + path: /health + port: {{ .Values.service.port }} + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 5 + readinessProbe: + httpGet: + path: /health + port: {{ .Values.service.port }} + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml new file mode 100644 index 0000000..d41832d --- /dev/null +++ b/helm/templates/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Release.Name }}-service" + namespace: {{ .Release.Namespace }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: {{ .Values.service.port }} + selector: + app: {{ .Release.Name }} diff --git a/helm/values.production.yaml b/helm/values.production.yaml new file mode 100644 index 0000000..de63b73 --- /dev/null +++ b/helm/values.production.yaml @@ -0,0 +1,16 @@ +replicaCount: 3 # Scale for fault tolerance + +image: + tag: master + +service: + type: LoadBalancer # Use LoadBalancer for external access + +resources: + requests: + cpu: "250m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + diff --git a/helm/values.staging.yaml b/helm/values.staging.yaml new file mode 100644 index 0000000..2795bdb --- /dev/null +++ b/helm/values.staging.yaml @@ -0,0 +1,9 @@ +replicaCount: 1 # Minimal setup + +image: + tag: master + +service: + type: NodePort # Expose the service for testing + +resources: {} diff --git a/helm/values.yaml b/helm/values.yaml new file mode 100644 index 0000000..3fb7500 --- /dev/null +++ b/helm/values.yaml @@ -0,0 +1,12 @@ +replicaCount: 1 + +image: + repository: ghcr.io/draju1980/draju1980/p2p-devops-test + tag: master + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 3000 + +resources: {} diff --git a/main.go b/main.go index 97fe143..c68ee63 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,20 @@ package main import ( - "fmt" - "net/http" + "fmt" + "net/http" ) func main() { - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path) - }) - fmt.Println("Web app running on localhost:3000") - http.ListenAndServe(":3000", nil) + // Application endpoint + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, you've requested: %s\n", r.URL.Path) + }) + // Health check endpoint + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + fmt.Println("Web app running on localhost:3000") + http.ListenAndServe(":3000", nil) }