Skip to content

GitLab Deploy Pipeline | Complete CI/CD Deployment Guide

GitLab CI/CD deployment pipelines automate the process of deploying applications to various environments. This guide covers deploying Angular UIs, .NET Core microservices, and databases using Liquibase.

Deploying Angular applications involves building the production bundle and serving it through web servers or cloud platforms.

.gitlab-ci.yml
stages:
- build
- deploy
variables:
NODE_VERSION: "18"
deploy_angular_staging:
stage: deploy
image: node:${NODE_VERSION}
dependencies:
- build_angular
script:
- npm install -g @angular/cli
- echo "Deploying to staging environment"
- aws s3 sync dist/ s3://$STAGING_S3_BUCKET --delete
- aws cloudfront create-invalidation --distribution-id $STAGING_CLOUDFRONT_ID --paths "/*"
environment:
name: staging
url: https://staging.myapp.com
only:
- develop
deploy_angular_production:
stage: deploy
image: node:${NODE_VERSION}
dependencies:
- build_angular
script:
- npm install -g @angular/cli
- echo "Deploying to production environment"
- aws s3 sync dist/ s3://$PRODUCTION_S3_BUCKET --delete
- aws cloudfront create-invalidation --distribution-id $PRODUCTION_CLOUDFRONT_ID --paths "/*"
environment:
name: production
url: https://myapp.com
when: manual
only:
- main
deploy_angular_docker:
stage: deploy
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
# Build Docker image
- docker build -t $CI_REGISTRY_IMAGE/angular-ui:$CI_COMMIT_SHA .
- docker tag $CI_REGISTRY_IMAGE/angular-ui:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE/angular-ui:latest
# Push to registry
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE/angular-ui:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE/angular-ui:latest
# Deploy to Kubernetes
- kubectl set image deployment/angular-ui angular-ui=$CI_REGISTRY_IMAGE/angular-ui:$CI_COMMIT_SHA
- kubectl rollout status deployment/angular-ui
environment:
name: production
url: https://myapp.com

Angular Deployment with Environment-Specific Configurations

Section titled โ€œAngular Deployment with Environment-Specific Configurationsโ€
.deploy_angular_template: &deploy_angular
image: node:18
dependencies:
- build_angular
script:
- echo "Deploying Angular UI to $ENVIRONMENT"
- |
if [ "$ENVIRONMENT" = "staging" ]; then
ng build --configuration=staging
else
ng build --configuration=production
fi
- aws s3 sync dist/ s3://$S3_BUCKET --delete
- aws cloudfront create-invalidation --distribution-id $CLOUDFRONT_ID --paths "/*"
deploy_angular_staging:
<<: *deploy_angular
stage: deploy
variables:
ENVIRONMENT: "staging"
S3_BUCKET: $STAGING_S3_BUCKET
CLOUDFRONT_ID: $STAGING_CLOUDFRONT_ID
environment:
name: staging
url: https://staging.myapp.com
only:
- develop
deploy_angular_production:
<<: *deploy_angular
stage: deploy
variables:
ENVIRONMENT: "production"
S3_BUCKET: $PRODUCTION_S3_BUCKET
CLOUDFRONT_ID: $PRODUCTION_CLOUDFRONT_ID
environment:
name: production
url: https://myapp.com
when: manual
only:
- main
deploy_angular_nginx:
stage: deploy
image: nginx:alpine
dependencies:
- build_angular
script:
- cp -r dist/* /usr/share/nginx/html/
- |
cat > /etc/nginx/conf.d/default.conf << 'EOF'
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend-service:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
EOF
- nginx -g 'daemon off;'
environment:
name: production
url: https://myapp.com

Deploying .NET Core microservices involves containerization, orchestration, and proper service mesh configuration.

deploy_dotnet_service:
stage: deploy
image: mcr.microsoft.com/dotnet/sdk:8.0
dependencies:
- build_dotnet
script:
- cd publish/
- dotnet MyService.dll &
- echo "Service deployed successfully"
environment:
name: production
url: https://api.myapp.com
variables:
DOCKER_REGISTRY: "registry.gitlab.com/mygroup/myproject"
SERVICES: "userservice orderservice paymentservice"
.deploy_microservice_template: &deploy_microservice
stage: deploy
image: docker:20.10.16
services:
- docker:20.10.16-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- |
for service in $SERVICES; do
echo "Deploying $service..."
# Build and tag image
docker build -f src/$service/Dockerfile -t $DOCKER_REGISTRY/$service:$CI_COMMIT_SHA src/$service/
docker tag $DOCKER_REGISTRY/$service:$CI_COMMIT_SHA $DOCKER_REGISTRY/$service:latest
# Push to registry
docker push $DOCKER_REGISTRY/$service:$CI_COMMIT_SHA
docker push $DOCKER_REGISTRY/$service:latest
# Deploy to Kubernetes
kubectl set image deployment/$service $service=$DOCKER_REGISTRY/$service:$CI_COMMIT_SHA
kubectl rollout status deployment/$service
done
deploy_microservices_staging:
<<: *deploy_microservice
environment:
name: staging
url: https://staging-api.myapp.com
only:
- develop
deploy_microservices_production:
<<: *deploy_microservice
environment:
name: production
url: https://api.myapp.com
when: manual
only:
- main
deploy_with_helm:
stage: deploy
image: alpine/helm:3.10.0
dependencies:
- build_dotnet
script:
- helm repo add stable https://charts.helm.sh/stable
- helm repo update
- |
helm upgrade --install my-microservices ./helm-chart \
--set image.tag=$CI_COMMIT_SHA \
--set environment=$ENVIRONMENT \
--set ingress.host=$INGRESS_HOST \
--namespace $NAMESPACE \
--create-namespace
- kubectl get pods -n $NAMESPACE
environment:
name: $ENVIRONMENT
url: https://$INGRESS_HOST
deploy_blue_green:
stage: deploy
image: kubectl:latest
script:
- |
# Check current active color
CURRENT_COLOR=$(kubectl get service myapp-service -o jsonpath='{.spec.selector.color}')
if [ "$CURRENT_COLOR" = "blue" ]; then
NEW_COLOR="green"
else
NEW_COLOR="blue"
fi
echo "Deploying to $NEW_COLOR environment"
# Deploy to inactive color
kubectl set image deployment/myapp-$NEW_COLOR myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
kubectl rollout status deployment/myapp-$NEW_COLOR
# Health check
kubectl run health-check --image=curlimages/curl --rm -it --restart=Never \
-- curl -f http://myapp-$NEW_COLOR-service/health
# Switch traffic
kubectl patch service myapp-service -p '{"spec":{"selector":{"color":"'$NEW_COLOR'"}}}'
echo "Traffic switched to $NEW_COLOR"
environment:
name: production
url: https://api.myapp.com
deploy_canary:
stage: deploy
image: istio/istioctl:1.18.0
script:
- |
# Deploy canary version
kubectl set image deployment/myapp-canary myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
kubectl rollout status deployment/myapp-canary
# Configure traffic split (10% to canary)
cat <<EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp-vs
spec:
http:
- match:
- headers:
canary:
exact: "true"
route:
- destination:
host: myapp-canary-service
- route:
- destination:
host: myapp-service
weight: 90
- destination:
host: myapp-canary-service
weight: 10
EOF
echo "Canary deployment with 10% traffic split completed"
environment:
name: production
url: https://api.myapp.com

Database deployments with Liquibase ensure consistent schema changes across environments.

deploy_database:
stage: deploy
image: liquibase/liquibase:4.23
variables:
DB_URL: "jdbc:postgresql://postgres:5432/myapp"
DB_USERNAME: "dbuser"
DB_PASSWORD: "dbpass"
script:
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD update
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD status
environment:
name: production
.deploy_database_template: &deploy_database
image: liquibase/liquibase:4.23
script:
- echo "Deploying database changes to $ENVIRONMENT"
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD validate
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD update
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD status
deploy_database_staging:
<<: *deploy_database
stage: deploy
variables:
ENVIRONMENT: "staging"
DB_URL: $STAGING_DB_URL
DB_USERNAME: $STAGING_DB_USERNAME
DB_PASSWORD: $STAGING_DB_PASSWORD
environment:
name: staging
only:
- develop
deploy_database_production:
<<: *deploy_database
stage: deploy
variables:
ENVIRONMENT: "production"
DB_URL: $PRODUCTION_DB_URL
DB_USERNAME: $PRODUCTION_DB_USERNAME
DB_PASSWORD: $PRODUCTION_DB_PASSWORD
environment:
name: production
when: manual
only:
- main
deploy_database_with_rollback:
stage: deploy
image: liquibase/liquibase:4.23
variables:
DB_URL: $PRODUCTION_DB_URL
DB_USERNAME: $PRODUCTION_DB_USERNAME
DB_PASSWORD: $PRODUCTION_DB_PASSWORD
script:
# Create rollback script before deployment
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD rollbackSQL $(date +"%Y-%m-%d") > rollback-script.sql
# Validate changes
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD validate
# Generate update SQL for review
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD updateSQL > update-script.sql
# Apply changes
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD update
# Verify deployment
- liquibase --url=$DB_URL --username=$DB_USERNAME --password=$DB_PASSWORD status
artifacts:
paths:
- rollback-script.sql
- update-script.sql
expire_in: 1 week
environment:
name: production
deploy_database_with_health_check:
stage: deploy
image: liquibase/liquibase:4.23
services:
- postgres:13
variables:
POSTGRES_DB: myapp
POSTGRES_USER: dbuser
POSTGRES_PASSWORD: dbpass
DB_URL: "jdbc:postgresql://postgres:5432/myapp"
script:
# Wait for database to be ready
- |
for i in {1..30}; do
if liquibase --url=$DB_URL --username=$POSTGRES_USER --password=$POSTGRES_PASSWORD status; then
echo "Database is ready"
break
fi
echo "Waiting for database... ($i/30)"
sleep 10
done
# Deploy changes
- liquibase --url=$DB_URL --username=$POSTGRES_USER --password=$POSTGRES_PASSWORD update
# Run health checks
- |
cat > health_check.sql << 'EOF'
SELECT
table_name,
column_name,
data_type
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;
EOF
- psql $DB_URL -f health_check.sql
environment:
name: production
deploy_multi_database:
stage: deploy
image: liquibase/liquibase:4.23
parallel:
matrix:
- DATABASE: [userdb, orderdb, inventorydb]
script:
- |
case $DATABASE in
userdb)
DB_URL=$USER_DB_URL
DB_USER=$USER_DB_USERNAME
DB_PASS=$USER_DB_PASSWORD
;;
orderdb)
DB_URL=$ORDER_DB_URL
DB_USER=$ORDER_DB_USERNAME
DB_PASS=$ORDER_DB_PASSWORD
;;
inventorydb)
DB_URL=$INVENTORY_DB_URL
DB_USER=$INVENTORY_DB_USERNAME
DB_PASS=$INVENTORY_DB_PASSWORD
;;
esac
- echo "Deploying $DATABASE database changes"
- liquibase --url=$DB_URL --username=$DB_USER --password=$DB_PASS --changeLogFile=changelog-$DATABASE.xml update
- liquibase --url=$DB_URL --username=$DB_USER --password=$DB_PASS status
environment:
name: production
deploy_with_monitoring:
stage: deploy
script:
- echo "Deploying application with monitoring"
- kubectl apply -f deployment.yaml
- kubectl rollout status deployment/myapp
# Setup monitoring
- |
for i in {1..10}; do
HEALTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://myapp-service/health)
if [ "$HEALTH_STATUS" = "200" ]; then
echo "Health check passed"
break
fi
echo "Health check failed, retrying... ($i/10)"
sleep 30
done
# Monitor metrics for 5 minutes
- |
for i in {1..5}; do
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~'5..'}[5m])" | jq -r '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
echo "Error rate too high: $ERROR_RATE"
kubectl rollout undo deployment/myapp
exit 1
fi
echo "Monitoring... Error rate: $ERROR_RATE"
sleep 60
done
environment:
name: production
url: https://myapp.com
deploy_zero_downtime:
stage: deploy
script:
- |
# Scale up new version
kubectl scale deployment myapp-new --replicas=3
kubectl rollout status deployment myapp-new
# Health check new version
kubectl run health-check --image=curlimages/curl --rm -it --restart=Never \
-- curl -f http://myapp-new-service/health
# Gradually shift traffic
for weight in 25 50 75 100; do
echo "Shifting $weight% traffic to new version"
kubectl patch service myapp-service -p "{\"spec\":{\"selector\":{\"version\":\"new\",\"weight\":\"$weight\"}}}"
sleep 120
# Monitor during traffic shift
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~'5..'}[2m])" | jq -r '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.02" | bc -l) )); then
echo "Error rate increased, rolling back"
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"old"}}}'
exit 1
fi
done
# Scale down old version
kubectl scale deployment myapp-old --replicas=0
environment:
name: production
url: https://myapp.com

This comprehensive deployment guide covers the essential aspects of deploying Angular UIs, .NET Core microservices, and databases using Liquibase in GitLab CI/CD pipelines.