Portfolio CI/CD Pipeline
Overview
A fully automated end-to-end CI/CD system spanning three GitLab repositories - website, infrastructure, and Ansible - wired together via cross-repo triggers. A single git tag drives the entire journey from Hugo build to live production deployment, while infrastructure changes flow through Terraform into Ansible-driven provisioning. Supports two isolated environments: staging and prod.
Period: Jan 2026 – Mar 2026 (gradually updating…)| Role: Solo - DevOps Engineer
Architecture
Tech Stack
- CI/CD: GitLab CI/CD (cross-repo pipeline triggers ·
strategy: depend) - Static Site: Hugo Extended · Docker · Nginx
- Infrastructure as Code: Terraform · DigitalOcean provider · Cloudflare DNS
- Configuration Management: Ansible · Ansible Vault · Galaxy collections
- Environments: staging · prod (DigitalOcean Droplets)
- Reverse Proxy: Traefik (TLS termination)
- Secrets: Ansible Vault (encrypted vars) · HashiCorp Vault · GitLab masked CI/CD variables
- Security & Compliance: Gitleaks (secret scanning) · Checkov (IaC SAST) · Open Policy Agent via Conftest (Rego policies)
Pipeline Flow
Website Repo → Deploy
| Stage | Job | Purpose |
|---|---|---|
security | secret-scan, hadolint-dockerfile, create-sbom, trivy-sbom-scan, trivy-misconfig-scan, semgrep-sast | Shift-left security checks (MR to main only) |
build | hugo-build | Build Hugo site |
package | docker-build | Build + push image tags ${CI_COMMIT_SHORT_SHA} and latest on push to main |
package | tag-image-from-commit | Retag commit image to ${CI_COMMIT_TAG} on tag pipelines |
deploy | deploy-staging | Deploy to staging |
deploy | deploy-prod | Deploy to production |
hugo-buildanddocker-buildrun only onpushtomain(not on MR).- Tag convention:
v1.2.3-stagdeploys to staging, whilev1.2.3(without-stag) deploys to prod.
Note
Deploy jobs run from tag pipelines and trigger the Ansible repo (strategy: depend) with TARGET_ENV, PORTFOLIO_IMAGE, and TRIGGER_SOURCE=website. The website pipeline waits for downstream deploy result and mirrors its status.
Infrastructure Repo → Provision
Direct pushes to main are blocked. Changes are expected to go through Merge Requests, and workflow: rules limits valid pipelines to MR targeting main and push to main.
MR to main
| Stage | Job | Purpose |
|---|---|---|
lint | terraform-fmt, tflint | Format check + static analysis |
security | secret-scan (Gitleaks), sast-scan (Checkov) | Secret detection + IaC SAST |
Push to main (MR get merged)
| Stage | Job | Trigger |
|---|---|---|
plan | plan-staging, plan-prod | file changes scoped per env |
compliance | compliance-staging, compliance-prod | OPA / Conftest · custom Rego policies |
apply | apply-staging, apply-prod | manual (allow_failure: true) |
provision | trigger-provision-staging, trigger-provision-prod | manual cross-repo trigger to Ansible (strategy: depend) |
cleanup | cleanup-staging, cleanup-prod | manual (safety valve - tears down env) |
- Plan jobs are automatically triggered by changes to the
mainbranch, but they only run if the changes affect files relevant to that environment (e.g.staging.tfvarsorprod.tfvars), preventing unnecessary plans for unrelated changes. - Compliance jobs run Conftest against the Terraform plan output, validating it against custom Rego policies that enforce rules like “no destructive actions without explicit approval”, “Droplets must be of certain sizes”, “DNS records must follow specific patterns”, and “Vault configuration must meet security standards”.
- Apply jobs are manual gates that only become available after a successful compliance check, ensuring that no infrastructure change can be applied without first passing the OPA policy validation.
- Provision jobs are manual and depend on successful
apply-*jobs, then trigger Ansible downstream provisioning. - Cleanup jobs are manual gates that allow for tearing down the entire environment if needed, providing a safety valve in case of misconfigurations or failed deployments (considered this project is fairly small)
Web pipeline only
| Stage | Job | Trigger |
|---|---|---|
vault | create-vault, vault-bootstrap, destroy-vault | web pipeline only when VAULT=true (destroy-vault is manual) |
Warning
Vault lifecycle is intentionally isolated from the standard infra flow. It is run from a web-triggered pipeline (VAULT=true) for controlled, operator-driven Vault bootstrap/teardown.
Ansible Repo
MR to main
| Stage | Job | Purpose |
|---|---|---|
| Lint | lint | playbook lint + syntax checks |
| Security | secret-scan, ansible-misconfig-scan | secret and Ansible misconfiguration scanning |
misconfig-scan job uses Checkov’s Ansible scanning capabilities to detect common issues like hardcoded secrets, unsafe permissions, and deprecated modules.
Note
While there is some skipped checks (e.g. Vault health check task that targets localhost before TLS is configured), the majority of the playbook code is covered by these automated gates, ensuring high quality and security standards before any change is merged.
When triggered by upstream pipelines (or web manual)
| Stage | Job | Purpose |
|---|---|---|
| Playbook | provision | infra-triggered pipeline (TARGET_ENV set, PORTFOLIO_IMAGE unset) |
| Playbook | deploy | website-triggered pipeline (TRIGGER_SOURCE=website, PORTFOLIO_IMAGE set) |
| Playbook | vault-bootstrap | triggered by infrastructure Vault pipeline when VAULT=true |
- Lint/security gates run on MR pipelines targeting
main. - Provision job runs the Ansible playbook that bootstraps the entire staging or prod environment from scratch - from Droplet creation to Traefik configuration.
- Deploy job runs the Ansible playbook that deploys the determined Hugo Docker image to the live environment, without touching the underlying infrastructure.
Note
The pipeline separates provisioning and deployment by rule conditions: provision runs only when PORTFOLIO_IMAGE is absent, while deploy requires both TRIGGER_SOURCE=website and PORTFOLIO_IMAGE.
Rollback Strategy
- Application Rollback: Previous Hugo image tags are retained in the container registry. To rollback, manually trigger the deploy job with the desired image tag (e.g.
${CI_COMMIT_SHORT_SHA}orlatestfrom a known good commit).
Key Achievements
- Cross-repo trigger chain:
website→ansibleandinfrastructure→ansiblewithstrategy: depend. The triggering pipeline waits for the downstream to finish and reflects its status. - Shift-left security gates: Website, infrastructure, and ansible repos run security checks on MR pipelines targeting
main, catching issues before merge. - OPA compliance gate: Conftest validates each Terraform plan JSON against custom Rego policies (destructive operations, DNS, Droplet sizing, Firewall rules, Vault) before the apply job is unlocked.
- Environment isolation: Staging and prod
apply,provision, andcleanupare explicit manual gates in infra pipeline. - Vault lifecycle pipeline: A dedicated web pipeline (
VAULT=true) creates Vault infra, triggers ansiblevault-bootstrap, and supports manual destroy. - DRY pipeline YAML: Ansible jobs reuse shared templates (
.ansible_base,.ansible_setup) viaextends. - Secrets management: Ansible Vault encrypts all sensitive vars at rest; SSH private key is stored base64-encoded in GitLab masked variables and decoded at runtime, never written in plaintext.
- Selective infra plans: Plan jobs use
changes:filters scoped to per-env tfvars and backend files, so a prod tfvars edit doesn’t trigger staging plans - and vice versa. - Deterministic artifact promotion: Website release tags are promoted from an already-built commit image (
tag-image-from-commit) instead of rebuilding at release time, reducing drift between tested and deployed artifacts. - Policy-before-change enforcement: Infra jobs persist plan JSON artifacts and gate
applybehind Conftest checks, making policy decisions reproducible and reviewable. - Environment-scoped deployment logic: Ansible execution is guarded by explicit runtime conditions (
TARGET_ENV,TRIGGER_SOURCE,PORTFOLIO_IMAGE) to prevent accidental cross-flow execution. - Ephemeral SSH access model: Ansible setup supports Vault-signed short-lived SSH certificates (with bootstrap fallback), minimizing long-lived key exposure in routine operations.
Pipeline Screenshots


