8. GitLab CI/CD Pipeline Templates
← Back to Table of Contents | Previous: Terraform Infrastructure | Next: Workflow New Product →
8.1 Pipeline Architecture
The CI/CD pipeline is split into reusable templates that are included in the main .gitlab-ci.yml.
.gitlab-ci.yml (main orchestrator)
│
├── ci/templates/lint.yml
├── ci/templates/test.yml
├── ci/templates/build.yml
├── ci/templates/security-scan.yml
└── ci/templates/deploy.yml
8.2 Main Pipeline File
File: .gitlab-ci.yml (application repo)
stages:
- lint
- test
- build
- security
- deploy-dev
- deploy-staging
- deploy-prod
include:
- local: ci/templates/lint.yml
- local: ci/templates/test.yml
- local: ci/templates/build.yml
- local: ci/templates/security-scan.yml
- local: ci/templates/deploy.yml
variables:
PROJECT_NAME: myapp
AWS_REGION: eu-west-1
NODE_IMAGE: node:20-alpine
8.3 Lint Template
File: ci/templates/lint.yml
.lint-base:
stage: lint
image: ${NODE_IMAGE}
before_script:
- npm ci --prefer-offline
lint:
extends: .lint-base
script:
- npx eslint src/ --max-warnings 0
- npx prettier --check src/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
type-check:
extends: .lint-base
script:
- npx tsc --noEmit
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
8.4 Test Template
File: ci/templates/test.yml
.test-base:
stage: test
image: ${NODE_IMAGE}
before_script:
- npm ci --prefer-offline
coverage: '/All files\s*\|\s*(\d+\.?\d*)\s*\|/'
artifacts:
reports:
junit: test-results/junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
when: always
unit-tests:
extends: .test-base
script:
- npm run test -- --run --reporter=junit --outputFile=test-results/junit.xml --coverage
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
integration-tests:
extends: .test-base
script:
- npm run test:integration -- --run
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
8.5 Build Template
File: ci/templates/build.yml
.build-base:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY}
build-auth:
extends: .build-base
script:
- docker build -t ${ECR_REGISTRY}/${PROJECT_NAME}-auth:${CI_COMMIT_SHORT_SHA} -f docker/auth/Dockerfile .
- docker push ${ECR_REGISTRY}/${PROJECT_NAME}-auth:${CI_COMMIT_SHORT_SHA}
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes:
- src/auth/**/*
- docker/auth/**/*
build-billing:
extends: .build-base
script:
- docker build -t ${ECR_REGISTRY}/${PROJECT_NAME}-billing:${CI_COMMIT_SHORT_SHA} -f docker/billing/Dockerfile .
- docker push ${ECR_REGISTRY}/${PROJECT_NAME}-billing:${CI_COMMIT_SHORT_SHA}
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes:
- src/billing/**/*
- docker/billing/**/*
build-orders:
extends: .build-base
script:
- docker build -t ${ECR_REGISTRY}/${PROJECT_NAME}-orders:${CI_COMMIT_SHORT_SHA} -f docker/orders/Dockerfile .
- docker push ${ECR_REGISTRY}/${PROJECT_NAME}-orders:${CI_COMMIT_SHORT_SHA}
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes:
- src/orders/**/*
- docker/orders/**/*
8.6 Security Scan Template
File: ci/templates/security-scan.yml
security-scan:
stage: security
image: ${NODE_IMAGE}
script:
- npm audit --audit-level=high
- npx snyk test || true
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
allow_failure: false
8.7 Deploy Template
File: ci/templates/deploy.yml
.deploy-base:
image:
name: hashicorp/terraform:1.6
entrypoint: [""]
before_script:
- cd infra/environments/${DEPLOY_ENV}
- terraform init -input=false
deploy-dev:
extends: .deploy-base
stage: deploy-dev
variables:
DEPLOY_ENV: dev
script:
- terraform plan -input=false -out=tfplan
- terraform apply -input=false tfplan
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: dev
deploy-staging:
extends: .deploy-base
stage: deploy-staging
variables:
DEPLOY_ENV: staging
script:
- terraform plan -input=false -out=tfplan
- terraform apply -input=false tfplan
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: staging
when: manual
deploy-prod:
extends: .deploy-base
stage: deploy-prod
variables:
DEPLOY_ENV: prod
script:
- terraform plan -input=false -out=tfplan
- terraform apply -input=false tfplan
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: production
when: manual
8.8 Infrastructure Pipeline
File: .gitlab-ci.yml (infra repo)
stages:
- validate
- plan
- apply
variables:
TF_VERSION: "1.6"
validate:
stage: validate
image:
name: hashicorp/terraform:${TF_VERSION}
entrypoint: [""]
script:
- terraform fmt -check -recursive
- |
for env in environments/*/; do
cd $env
terraform init -backend=false
terraform validate
cd ../..
done
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
plan:
stage: plan
image:
name: hashicorp/terraform:${TF_VERSION}
entrypoint: [""]
script:
- cd environments/${TF_ENV}
- terraform init -input=false
- terraform plan -input=false -out=tfplan
artifacts:
paths:
- environments/${TF_ENV}/tfplan
rules:
- if: $CI_COMMIT_BRANCH == "main"
apply:
stage: apply
image:
name: hashicorp/terraform:${TF_VERSION}
entrypoint: [""]
script:
- cd environments/${TF_ENV}
- terraform init -input=false
- terraform apply -input=false tfplan
dependencies:
- plan
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
8.9 Pipeline Flow Summary
PR opened
↓
lint + type-check (parallel)
↓
unit-tests + integration-tests (parallel)
↓
security-scan
↓
[merge to main]
↓
build docker images (only changed modules)
↓
deploy-dev (automatic)
↓
deploy-staging (manual approval)
↓
deploy-prod (manual approval)
← Back to Table of Contents | Previous: Terraform Infrastructure | Next: Workflow New Product →