image: node:21-alpine variables: FQDN_PRODUCTION: "$KUBE_NAMESPACE_PRODUCTION.$KUBE_INGRESS_BASE_DOMAIN" FQDN_STAGING: "$KUBE_NAMESPACE_STAGING.$KUBE_INGRESS_BASE_DOMAIN" IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-$CI_COMMIT_SHORT_SHA" DOCKER_TLS_CERTDIR: '' stages: - build - static-security # Fast scans (1-5 min) - test # Unit test(s) - package # Docker build - container-scan # Scan built image - staging # Deploy to staging - dast # Dynamic scanning - production # Deploy to production include: - component: gitlab.com/components/container-scanning/container-scanning@5.1.0 inputs: stage: container-scan cs_image: $IMAGE_TAG - component: $CI_SERVER_FQDN/components/sast/sast@3.0.1 inputs: stage: static-security run_advanced_sast: true excluded_paths: "node_modules/**,k8s/**,test/**" - component: gitlab.com/components/secret-detection/secret-detection@2.1.0 inputs: stage: static-security - template: Jobs/Dependency-Scanning.v2.gitlab-ci.yml - template: Security/SAST-IaC.latest.gitlab-ci.yml - template: DAST.gitlab-ci.yml dependency-scanning: stage: static-security variables: DS_EXCLUDED_PATHS: "node_modules/**,k8s/**,test/**" iac-sast: stage: static-security dast: stage: dast needs: - deploy-staging variables: DAST_WEBSITE: https://$FQDN_STAGING DAST_FULL_SCAN_ENABLED: "false" DAST_SPIDER_MINS: "3" cache: key: files: - package-lock.json paths: - node_modules/ policy: pull install: stage: build cache: key: files: - package-lock.json paths: - node_modules/ policy: pull-push script: - echo "Running npm ci and caching to ./node_modules" - npm ci - echo "Clean install completed" artifacts: paths: - node_modules/ expire_in: 1 hour unit-test-job: stage: test needs: - install script: - echo "Running unit tests" - npm test - echo "Tests completed" coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)%/' artifacts: when: always reports: junit: junit.xml # Build docker image build-docker-image: stage: package image: docker:28.4.0 services: - docker:28.4.0-dind needs: - unit-test-job before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY script: - echo "Building Docker image $IMAGE_TAG" - docker pull $CI_REGISTRY_IMAGE:latest || true - docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $IMAGE_TAG -t $CI_REGISTRY_IMAGE:latest . - docker push $IMAGE_TAG - | if [ "$CI_COMMIT_BRANCH" == "main" ]; then docker push $CI_REGISTRY_IMAGE:latest fi retry: max: 2 when: - runner_system_failure - stuck_or_timeout_failure timeout: 15 minutes # Deploy template, used in deploy to staging and production .deploy_template: image: alpine/k8s:1.34.1 before_script: - apk add --no-cache gettext script: - echo "Deploying to ${CI_ENVIRONMENT_NAME}" - kubectl config use-context ${KUBE_CONTEXT} # Create namespace - kubectl create namespace ${KUBE_NAMESPACE} --dry-run=client -o yaml | kubectl apply -f - # Create/update registry secret - kubectl create secret docker-registry gitlab-registry-auth -n ${KUBE_NAMESPACE} --docker-server="${CI_REGISTRY}" --docker-username="${CI_DEPLOY_USER}" --docker-password="${CI_DEPLOY_PASSWORD}" --dry-run=client -o yaml | kubectl apply -f - # Deploy manifests - echo "Deploying image ${IMAGE_TAG}" - for file in k8s/*.yaml; do envsubst < "$file" | kubectl apply -f -; done # Wait for rollout to complete - kubectl rollout status deployment/nodejs -n ${KUBE_NAMESPACE} --timeout=5m # Verify deployment - kubectl get pods -n ${KUBE_NAMESPACE} retry: max: 2 when: - runner_system_failure # Deploy to staging deploy-staging: extends: .deploy_template stage: staging environment: name: staging url: https://$FQDN_STAGING on_stop: stop-staging variables: KUBE_NAMESPACE: "$KUBE_NAMESPACE_STAGING" FQDN: "$FQDN_STAGING" needs: - build-docker-image rules: - if: $CI_COMMIT_BRANCH == "master" # Stop staging environment stop-staging: extends: .deploy_template stage: staging environment: name: staging action: stop variables: KUBE_NAMESPACE: "$KUBE_NAMESPACE_STAGING" script: - kubectl delete namespace ${KUBE_NAMESPACE} when: manual rules: - if: $CI_COMMIT_BRANCH == "master" # Deploy to production deploy-production: extends: .deploy_template stage: production environment: name: production url: https://$FQDN_PRODUCTION variables: KUBE_NAMESPACE: "$KUBE_NAMESPACE_PRODUCTION" FQDN: "$FQDN_PRODUCTION" needs: - deploy-staging - dast when: manual rules: - if: $CI_COMMIT_TAG # Stop production environment rollback-production: extends: .deploy_template stage: production environment: name: production variables: KUBE_NAMESPACE: "$KUBE_NAMESPACE_PRODUCTION" IMAGE_TAG: "$ROLLBACK_IMAGE_TAG" script: - echo "Rolling back to ${IMAGE_TAG}" - kubectl config use-context ${KUBE_CONTEXT} - for file in k8s/*.yaml; do envsubst < "$file" | kubectl apply -f -; done - kubectl rollout status deployment/nodejs-app -n ${KUBE_NAMESPACE} --timeout=5m when: manual rules: - if: $CI_COMMIT_TAG