diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000..733c5fcb --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,89 @@ +name: CD Pipeline + +on: + workflow_run: + workflows: [CI Pipeline] + types: [completed] + branches: [DevOps, main] + workflow_dispatch: + inputs: + environment: + description: 'Deployment environment' + required: true + type: choice + options: + - staging + - production + image-tag: + description: 'Docker image tag to deploy' + required: true + type: string + +permissions: + contents: write + packages: read + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/bankapp + +jobs: + prepare: + name: Prepare Deployment + runs-on: ubuntu-latest + if: >- + github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success' + outputs: + image-tag: ${{ steps.resolve-tag.outputs.tag }} + environment: ${{ steps.resolve-env.outputs.environment }} + steps: + - name: Resolve image tag + id: resolve-tag + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "tag=${{ inputs.image-tag }}" >> "$GITHUB_OUTPUT" + else + echo "tag=${{ github.event.workflow_run.head_sha }}" >> "$GITHUB_OUTPUT" + fi + + - name: Resolve environment + id: resolve-env + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "environment=${{ inputs.environment }}" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.event.workflow_run.head_branch }}" == "main" ]]; then + echo "environment=production" >> "$GITHUB_OUTPUT" + else + echo "environment=staging" >> "$GITHUB_OUTPUT" + fi + + deploy-staging: + name: Deploy to Staging + needs: prepare + if: needs.prepare.outputs.environment == 'staging' + uses: ./.github/workflows/reusable-deploy.yml + with: + environment: staging + image-tag: ${{ needs.prepare.outputs.image-tag }} + image-name: ghcr.io/${{ github.repository_owner }}/bankapp + namespace: bankapp-staging + helm-release-name: bankapp-staging + helm-values-file: helm/bankapp/values-staging.yaml + secrets: + kube-config: ${{ secrets.KUBE_CONFIG_STAGING }} + + deploy-production: + name: Deploy to Production + needs: prepare + if: needs.prepare.outputs.environment == 'production' + uses: ./.github/workflows/reusable-deploy.yml + with: + environment: production + image-tag: ${{ needs.prepare.outputs.image-tag }} + image-name: ghcr.io/${{ github.repository_owner }}/bankapp + namespace: bankapp-production + helm-release-name: bankapp-production + helm-values-file: helm/bankapp/values-production.yaml + secrets: + kube-config: ${{ secrets.KUBE_CONFIG_PRODUCTION }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c33434f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,80 @@ +name: CI Pipeline + +on: + push: + branches: [DevOps, main] + pull_request: + branches: [DevOps, main] + +permissions: + contents: read + packages: write + security-events: write + checks: write + +env: + JAVA_VERSION: '17' + IMAGE_NAME: ${{ github.repository_owner }}/bankapp + +jobs: + build: + name: Build + uses: ./.github/workflows/reusable-build.yml + with: + java-version: '17' + maven-goals: 'clean compile' + + test: + name: Test + needs: build + uses: ./.github/workflows/reusable-test.yml + with: + java-version: '17' + + security-scan: + name: Security Scan + needs: build + uses: ./.github/workflows/reusable-security-scan.yml + with: + java-version: '17' + trivy-severity: 'CRITICAL,HIGH' + fail-on-vulnerabilities: false + secrets: + nvd-api-key: ${{ secrets.NVD_API_KEY }} + + docker-build: + name: Docker Build & Push + needs: [test] + if: always() && needs.test.result == 'success' + uses: ./.github/workflows/reusable-docker.yml + with: + image-name: ${{ github.repository_owner }}/bankapp + image-tag: ${{ github.sha }} + push: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/DevOps' || github.ref == 'refs/heads/main') }} + registry: ghcr.io + trivy-scan: true + trivy-severity: 'CRITICAL,HIGH' + secrets: + registry-username: ${{ github.actor }} + registry-password: ${{ secrets.GITHUB_TOKEN }} + + ci-status: + name: CI Status + runs-on: ubuntu-latest + needs: [build, test, security-scan, docker-build] + if: always() + steps: + - name: Check CI results + run: | + echo "Build: ${{ needs.build.result }}" + echo "Test: ${{ needs.test.result }}" + echo "Security Scan: ${{ needs.security-scan.result }}" + echo "Docker Build: ${{ needs.docker-build.result }}" + + if [[ "${{ needs.build.result }}" == "failure" || \ + "${{ needs.test.result }}" == "failure" ]]; then + echo "CI pipeline failed" + exit 1 + fi + + echo "CI pipeline completed successfully" diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml new file mode 100644 index 00000000..71072a13 --- /dev/null +++ b/.github/workflows/reusable-build.yml @@ -0,0 +1,41 @@ +name: Reusable Build + +on: + workflow_call: + inputs: + java-version: + description: 'Java version to use' + required: false + type: string + default: '17' + maven-goals: + description: 'Maven goals to execute' + required: false + type: string + default: 'clean compile' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ inputs.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: 'temurin' + cache: 'maven' + + - name: Build with Maven + run: | + chmod +x ./mvnw + ./mvnw ${{ inputs.maven-goals }} -DskipTests -B + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: target/ + retention-days: 1 diff --git a/.github/workflows/reusable-deploy.yml b/.github/workflows/reusable-deploy.yml new file mode 100644 index 00000000..35128a85 --- /dev/null +++ b/.github/workflows/reusable-deploy.yml @@ -0,0 +1,96 @@ +name: Reusable Deploy + +on: + workflow_call: + inputs: + environment: + description: 'Deployment environment' + required: true + type: string + image-tag: + description: 'Docker image tag to deploy' + required: true + type: string + image-name: + description: 'Full image name (registry/repo)' + required: true + type: string + namespace: + description: 'Kubernetes namespace' + required: false + type: string + default: 'bankapp-namespace' + helm-release-name: + description: 'Helm release name' + required: false + type: string + default: 'bankapp' + helm-values-file: + description: 'Path to Helm values file' + required: false + type: string + default: 'helm/bankapp/values.yaml' + deployment-name: + description: 'Kubernetes Deployment resource name (as defined in Helm template)' + required: false + type: string + default: 'bankapp' + secrets: + kube-config: + description: 'Kubernetes config for cluster access' + required: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up kubectl + uses: azure/setup-kubectl@v4 + + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: 'latest' + + - name: Configure kubeconfig + run: | + mkdir -p $HOME/.kube + echo "${{ secrets.kube-config }}" | base64 -d > $HOME/.kube/config + chmod 600 $HOME/.kube/config + + - name: Validate Helm chart + run: | + helm lint helm/bankapp/ \ + --set image.app=${{ inputs.image-name }}:${{ inputs.image-tag }} + + - name: Deploy with Helm + run: | + helm upgrade --install ${{ inputs.helm-release-name }} helm/bankapp/ \ + --namespace ${{ inputs.namespace }} \ + --create-namespace \ + --set image.app=${{ inputs.image-name }}:${{ inputs.image-tag }} \ + --values ${{ inputs.helm-values-file }} \ + --wait \ + --timeout 300s + + - name: Verify deployment + run: | + kubectl rollout status deployment/${{ inputs.deployment-name }} \ + --namespace ${{ inputs.namespace }} \ + --timeout=300s + + - name: Display deployment info + if: always() + run: | + echo "=== Deployment Status ===" + kubectl get deployments -n ${{ inputs.namespace }} + echo "" + echo "=== Pods ===" + kubectl get pods -n ${{ inputs.namespace }} + echo "" + echo "=== Services ===" + kubectl get services -n ${{ inputs.namespace }} diff --git a/.github/workflows/reusable-docker.yml b/.github/workflows/reusable-docker.yml new file mode 100644 index 00000000..d9628262 --- /dev/null +++ b/.github/workflows/reusable-docker.yml @@ -0,0 +1,120 @@ +name: Reusable Docker Build & Push + +on: + workflow_call: + inputs: + image-name: + description: 'Docker image name' + required: true + type: string + image-tag: + description: 'Docker image tag' + required: false + type: string + default: 'latest' + push: + description: 'Push the image to registry' + required: false + type: boolean + default: false + registry: + description: 'Container registry to push to' + required: false + type: string + default: 'ghcr.io' + trivy-scan: + description: 'Run Trivy scan on built image' + required: false + type: boolean + default: true + trivy-severity: + description: 'Trivy severity levels for image scan' + required: false + type: string + default: 'CRITICAL,HIGH' + secrets: + registry-username: + description: 'Registry username' + required: false + registry-password: + description: 'Registry password' + required: false + +jobs: + docker: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + security-events: write + outputs: + image-digest: ${{ steps.build-push.outputs.digest }} + image-ref: ${{ steps.meta.outputs.tags }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Lowercase image name + id: lower + run: | + echo "image-name=$(echo '${{ inputs.image-name }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.registry }}/${{ steps.lower.outputs.image-name }} + tags: | + type=raw,value=${{ inputs.image-tag }} + type=sha,prefix= + type=ref,event=branch + type=ref,event=pr + + - name: Login to container registry + if: inputs.push + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ secrets.registry-username }} + password: ${{ secrets.registry-password }} + + - name: Build and push Docker image + id: build-push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ inputs.push }} + load: ${{ !inputs.push }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64 + + - name: Run Trivy image scan + if: inputs.trivy-scan + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ inputs.registry }}/${{ steps.lower.outputs.image-name }}:${{ inputs.image-tag }} + format: 'table' + severity: ${{ inputs.trivy-severity }} + exit-code: '0' + + - name: Run Trivy image scan (SARIF) + if: inputs.trivy-scan + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ inputs.registry }}/${{ steps.lower.outputs.image-name }}:${{ inputs.image-tag }} + format: 'sarif' + output: 'trivy-image-results.sarif' + severity: ${{ inputs.trivy-severity }} + + - name: Upload Trivy image scan results + if: inputs.trivy-scan + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-image-results.sarif' + category: 'trivy-container' diff --git a/.github/workflows/reusable-security-scan.yml b/.github/workflows/reusable-security-scan.yml new file mode 100644 index 00000000..59b66751 --- /dev/null +++ b/.github/workflows/reusable-security-scan.yml @@ -0,0 +1,102 @@ +name: Reusable Security Scan + +on: + workflow_call: + inputs: + java-version: + description: 'Java version to use' + required: false + type: string + default: '17' + trivy-severity: + description: 'Trivy severity levels to scan for' + required: false + type: string + default: 'CRITICAL,HIGH' + fail-on-vulnerabilities: + description: 'Fail the build if vulnerabilities are found' + required: false + type: boolean + default: false + secrets: + nvd-api-key: + description: 'NVD API key for OWASP Dependency-Check (speeds up NVD database updates)' + required: false + +jobs: + trivy-fs-scan: + name: Trivy Filesystem Scan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy filesystem scan + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'table' + severity: ${{ inputs.trivy-severity }} + exit-code: ${{ inputs.fail-on-vulnerabilities && '1' || '0' }} + + - name: Run Trivy filesystem scan (SARIF) + uses: aquasecurity/trivy-action@master + if: always() + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-fs-results.sarif' + severity: ${{ inputs.trivy-severity }} + + - name: Upload Trivy scan results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-fs-results.sarif' + category: 'trivy-filesystem' + + dependency-check: + name: OWASP Dependency Check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ inputs.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: 'temurin' + cache: 'maven' + + - name: Run OWASP Dependency-Check via Maven + env: + NVD_API_KEY: ${{ secrets.nvd-api-key }} + run: | + chmod +x ./mvnw + NVD_OPTS="" + if [ -n "$NVD_API_KEY" ]; then + NVD_OPTS="-DnvdApiKey=$NVD_API_KEY" + fi + ./mvnw org.owasp:dependency-check-maven:check \ + -DfailBuildOnCVSS=11 \ + -Dformat=HTML \ + -DretireJsAnalyzerEnabled=true \ + $NVD_OPTS \ + -B || echo "OWASP Dependency-Check completed with warnings" + + - name: Collect dependency check report + if: always() + run: | + mkdir -p reports + cp -r target/dependency-check-report.* reports/ 2>/dev/null || true + + - name: Upload dependency check report + uses: actions/upload-artifact@v4 + if: always() + with: + name: dependency-check-report + path: reports/ + retention-days: 10 diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml new file mode 100644 index 00000000..e68b22d4 --- /dev/null +++ b/.github/workflows/reusable-test.yml @@ -0,0 +1,66 @@ +name: Reusable Test + +on: + workflow_call: + inputs: + java-version: + description: 'Java version to use' + required: false + type: string + default: '17' + +jobs: + test: + runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: Test@123 + MYSQL_DATABASE: bankappdb + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h localhost" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + --health-start-period=30s + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ inputs.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ inputs.java-version }} + distribution: 'temurin' + cache: 'maven' + + - name: Run tests + env: + SPRING_DATASOURCE_URL: jdbc:mysql://localhost:3306/bankappdb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: Test@123 + run: | + chmod +x ./mvnw + ./mvnw clean test -B + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + target/surefire-reports/ + target/failsafe-reports/ + retention-days: 5 + + - name: Publish test report + if: always() + uses: dorny/test-reporter@v1 + with: + name: Maven Test Results + path: target/surefire-reports/*.xml + reporter: java-junit + fail-on-error: false diff --git a/Dockerfile b/Dockerfile index 079acabe..4a3efcbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ #---------------------------------- # Import docker image with maven installed -FROM maven:3.8.3-openjdk-17 as builder +FROM maven:3.9-eclipse-temurin-17 AS builder # Add maintainer, so that new user will understand who had written this Dockerfile MAINTAINER Madhup Pandey @@ -25,7 +25,7 @@ RUN mvn clean install -DskipTests=true #-------------------------------------- # Import small size java image -FROM openjdk:17-alpine as deployer +FROM eclipse-temurin:17-jre-alpine AS deployer # Copy build from stage 1 (builder) COPY --from=builder /src/target/*.jar /src/target/bankapp.jar diff --git a/helm/bankapp/values-production.yaml b/helm/bankapp/values-production.yaml new file mode 100644 index 00000000..cb137b78 --- /dev/null +++ b/helm/bankapp/values-production.yaml @@ -0,0 +1,42 @@ +namespace: bankapp-production + +configmap: + name: bankapp-config + data: + MYSQL_DATABASE: BankDB + SPRING_DATASOURCE_USERNAME: root + +db_statefulset: + name: mysql + storage: 20Gi + +app_deployment: + name: bankapp + cpu_req: 200m + cpu_limit: 1000m + mem_req: 512Mi + mem_limit: 1Gi + +image: + app: ghcr.io/cog-gtm/bankapp:latest + db: mysql:8.0 + +bankapp_svc: + port: 8080 + targetPort: 8080 + nodePort: 30080 + +mysql_svc: + port: 3306 + targetPort: 3306 + +hpa: + min_replica: 2 + max_replica: 10 + cpu_utilizatoion: 40 + +secret: + name: mysql-secret + data: + MYSQL_ROOT_PASSWORD: Test@123 + SPRING_DATASOURCE_PASSWORD: Test@123 diff --git a/helm/bankapp/values-staging.yaml b/helm/bankapp/values-staging.yaml new file mode 100644 index 00000000..b8b7acf5 --- /dev/null +++ b/helm/bankapp/values-staging.yaml @@ -0,0 +1,42 @@ +namespace: bankapp-staging + +configmap: + name: bankapp-config + data: + MYSQL_DATABASE: BankDB + SPRING_DATASOURCE_USERNAME: root + +db_statefulset: + name: mysql + storage: 5Gi + +app_deployment: + name: bankapp + cpu_req: 80m + cpu_limit: 800m + mem_req: 150Mi + mem_limit: 700Mi + +image: + app: ghcr.io/cog-gtm/bankapp:latest + db: mysql:8.0 + +bankapp_svc: + port: 8080 + targetPort: 8080 + nodePort: 30080 + +mysql_svc: + port: 3306 + targetPort: 3306 + +hpa: + min_replica: 1 + max_replica: 3 + cpu_utilizatoion: 60 + +secret: + name: mysql-secret + data: + MYSQL_ROOT_PASSWORD: Test@123 + SPRING_DATASOURCE_PASSWORD: Test@123