diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 000000000..7144b2981 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,251 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + workflow_dispatch: + +env: + JAVA_VERSION: '21' + JAVA_DISTRIBUTION: 'temurin' + GCP_REGION: 'us-central1' + +jobs: + # Job 1: Build and Test + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.JAVA_DISTRIBUTION }} + cache: 'maven' + + - name: Run tests + run: ./mvnw clean verify -B + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: target/surefire-reports/ + retention-days: 30 + + # Job 2: Security Scan (Filesystem) + security-scan: + name: Security Scan + runs-on: ubuntu-latest + needs: build-and-test + + permissions: + contents: read + security-events: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + # Job 3: Build and Push Docker Image + build-and-push-image: + name: Build and Push Docker Image + runs-on: ubuntu-latest + needs: [build-and-test, security-scan] + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + + permissions: + contents: read + id-token: write + + outputs: + image-tag: ${{ steps.meta.outputs.tags }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Configure Docker for GCR + run: | + gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/petclinic/spring-petclinic + tags: | + type=ref,event=branch + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Job 4: Container Security Scan + scan-image: + name: Scan Docker Image + runs-on: ubuntu-latest + needs: build-and-push-image + + permissions: + contents: read + security-events: write + + steps: + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Configure Docker + run: gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev + + - name: Run Trivy vulnerability scanner on image + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/petclinic/spring-petclinic:${{ github.sha }} + format: 'sarif' + output: 'trivy-image-results.sarif' + severity: 'CRITICAL,HIGH' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-image-results.sarif' + + # Job 5: Deploy to GCP + deploy: + name: Deploy to GCP + runs-on: ubuntu-latest + needs: [build-and-push-image, scan-image] + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + + environment: + name: ${{ github.ref == 'refs/heads/main' && 'production' || 'development' }} + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Set up Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.6.0 + + - name: Determine environment + id: env + run: | + if [ "${{ github.ref }}" == "refs/heads/main" ]; then + echo "environment=prod" >> $GITHUB_OUTPUT + echo "min_instances=1" >> $GITHUB_OUTPUT + else + echo "environment=dev" >> $GITHUB_OUTPUT + echo "min_instances=0" >> $GITHUB_OUTPUT + fi + + - name: Terraform Init + working-directory: ./terraform + run: terraform init + + - name: Terraform Plan + working-directory: ./terraform + run: | + terraform plan \ + -var="project_id=${{ secrets.GCP_PROJECT_ID }}" \ + -var="region=${{ env.GCP_REGION }}" \ + -var="environment=${{ steps.env.outputs.environment }}" \ + -var="image_tag=${{ github.sha }}" \ + -var="min_instances=${{ steps.env.outputs.min_instances }}" \ + -out=tfplan + + - name: Terraform Apply + working-directory: ./terraform + run: terraform apply -auto-approve tfplan + + - name: Get Cloud Run URL + id: deploy + run: | + URL=$(gcloud run services describe petclinic-${{ steps.env.outputs.environment }} \ + --region=${{ env.GCP_REGION }} \ + --format='value(status.url)') + echo "url=$URL" >> $GITHUB_OUTPUT + + - name: Run smoke test + run: | + sleep 30 + curl -f ${{ steps.deploy.outputs.url }}/actuator/health || exit 1 + + # Job 6: Notify + notify: + name: Notify + runs-on: ubuntu-latest + needs: [deploy] + if: always() + + steps: + - name: Send notification + run: | + echo "Deployment ${{ needs.deploy.result }}" + echo "Status: ${{ job.status }}" diff --git a/terraform/.gitignore b/terraform/.gitignore index a3b6fa3db..7a03858bb 100644 --- a/terraform/.gitignore +++ b/terraform/.gitignore @@ -12,4 +12,9 @@ override.tf *.pem *.key .env -.env.local \ No newline at end of file +.env.local + +# IDE +.vscode/ +.idea/ +*.iml \ No newline at end of file