DevSecOps CI/CD Project
Project goal: Build a full DevSecOps pipeline that builds, scans, pushes and deploys a Flask app using Docker, stores images in AWS ECR, provisions infrastructure with Terraform, configures servers with Ansible, secures pipeline with Trivy & Ansible Vault, and monitors with Prometheus + Grafana. This README documents everything you used and every step needed to reproduce, maintain and secure the workflow.
Table of contents
- Project overview & architecture
- Folder structure (recommended)
- Prerequisites (local & cloud)
- Terraform — provision EC2 + IAM + security groups (step-by-step)
- Ansible — configure servers & deploy containers (roles & playbooks)
- Jenkins pipeline — build, scan, push, deploy (Jenkinsfile)
- Security: Trivy, SonarQube (optional), Ansible Vault, Jenkins credentials, IAM least privilege
- Monitoring: Prometheus + Grafana + Node Exporter + Jenkins metrics plugin
- Correct workflow (step-by-step) + mermaid diagram for workflow image
- Troubleshooting & common pitfalls
- Useful commands & snippets
- Next improvements & checklist
1 — Project overview & architecture
High-level components:
- GitHub: source repo (your app + infra + ansible)
- Jenkins: CI server (build image, run security scans, push to ECR, trigger Ansible deploy)
- Docker: containerize Flask app
- AWS ECR: Docker registry for images
- Terraform: create EC2 instances, security groups, IAM roles/profiles
- Ansible: install runtime components on EC2 and run deploy tasks
- Trivy: container image vulnerability scanner integrated in pipeline
- Ansible Vault: secrets management for playbooks
- Prometheus + Grafana: monitoring & dashboards (Prometheus scrapes Node Exporter + app metrics, Grafana visualizes)
- Jenkins Prometheus plugin: optional for Jenkins metrics
- Optional: SonarQube for code quality (can be Dockerized or external)
2 — Recommended folder structure
devsecops-pipeline-with-ansible-terraform/ ├── app/ │ ├── Dockerfile │ ├── app.py │ └── requirements.txt ├── ansible/ │ ├── inventory.ini │ ├── playbook.yml │ ├── group_vars/ │ └── roles/ │ ├── flask_app/ │ │ ├── tasks/main.yml │ │ └── templates/ │ ├── jenkins_setup/ │ └── monitoring/ ├── terraform/ │ ├── main.tf │ ├── variables.tf │ ├── provider.tf │ └── outputs.tf ├── Jenkinsfile ├── README.md └── docs/ └── workflow-mermaid.txt 3 — Prerequisites
Local dev machine (Jenkins host or where you run CI):
- Git, Docker, Python3, pip, Ansible, aws-cli v2, Trivy, ngrok (optional)
- Jenkins (installed either on a VM/container or as a system package)
- Jenkins user added to Docker group:
sudo usermod -aG docker jenkins(then restart/relauch session)
AWS:
- AWS account with ECR & EC2 ability
- An IAM user (for CI) with minimal policies for ECR push (AmazonEC2ContainerRegistryPowerUser or fine-grained)
- Key pair (private
.pem) you downloaded and will map to Terraformkey_name.
Security:
- Don’t commit private keys or AWS secrets. Use Jenkins credentials & Ansible Vault.
4 — Terraform: Provision EC2, SG, IAM (step-by-step)
Goals: create three EC2 (flask, jenkins, monitoring) t3.micro (free-tier where available), create security group, IAM role/profile for ECR read/pull.
Basic pieces
Provider.tf
outputs.tf
main.tf
Notes & tips:
- Always use
key_namethat matches an existing EC2 key pair (you create the key in AWS console once and reference its name). - Lock SSH (
22) to your IP via CIDR for production safety. - Avoid user_data for complex provisioning if you use Ansible to configure servers — keep Terraform for infra only.
5 — Ansible: configure servers & deploy containers
Inventory (ansible/inventory.ini) example:
[flask_server] 34.238.165.116 ansible_user=ubuntu [jenkins_server] 18.207.168.154 ansible_user=ubuntu [monitoring_server] 10.0.1.163 ansible_user=ubuntu Playbook (ansible/playbook.yml)
--- - hosts: flask_server become: yes roles: - flask_app - hosts: jenkins_server become: yes roles: - jenkins_setup - hosts: monitoring_server become: yes roles: - monitoring Role flask_app/tasks/main.yml (example):
flask_app_role
jenkins_setup role
- name: Install Docker & AWS CLI v2 apt: name: ['docker.io', 'unzip', 'python3-pip'] update_cache: yes state: present - name: Ensure docker started systemd: name: docker state: started enabled: yes - name: Install AWS CLI v2 (if not present) - script tasks... - name: Log in to ECR shell: | aws ecr get-login-password --region {{ aws_region }} | docker login --username AWS --password-stdin {{ ecr_repo_domain }} environment: { AWS_ACCESS_KEY_ID: "{{ lookup('env','AWS_ACCESS_KEY_ID') }}" } args: { warn: false } register: login_result failed_when: login_result.rc != 0 and ('Unable to locate credentials' in login_result.stderr == False) - name: Pull image docker_image: name: "{{ ecr_repo }}:{{ image_tag }}" source: pull Tips:
- Use
docker_imagemodule where possible. - For ECR login, run with IAM role (preferred) or provide credentials via env/credentials file on host. If the EC2 has the appropriate IAM instance profile (ECR pull policy), you do not need to store AWS keys on the host.
- Use
become: yesfor system-level tasks. - Avoid referencing local PEM path in
inventory.ini— use Jenkinssshagentto provide keys at runtime. Inventory entries should beansible_user=ubuntu.
6 — Jenkinsfile (CI pipeline — build, scan, push, deploy)
Key points:
-
Use Jenkins credentials:
-
aws-credentials(AWS access key + secret) or use IAM role for Jenkins agent. -
ansible-ec2-key(SSH private key)
-
Ensure
trivyis installed or install in pipeline before using it.
Example Jenkinsfile:
pipeline { agent any environment { AWS_REGION = 'us-east-1' ECR_REPO = '772954893641.dkr.ecr.us-east-1.amazonaws.com/flask-app' IMAGE_TAG = "build-${BUILD_NUMBER}" } stages { stage('Checkout') { steps { git url: 'https://github.com/ritesh355/devsecops-pipeline-with-ansible-terraform.git', branch:'main' } } stage('Install Trivy if needed') { steps { sh ''' if ! command -v trivy >/dev/null 2>&1; then curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin fi trivy --version ''' } } stage('Build') { steps { script { sh "docker build -t ${ECR_REPO}:${IMAGE_TAG} ." } } } stage('Scan') { steps { sh "trivy image --exit-code 1 --severity HIGH,CRITICAL ${ECR_REPO}:${IMAGE_TAG} || true" } } stage('Push to ECR') { steps { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'aws-credentials']]) { sh ''' aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${ECR_REPO} docker push ${ECR_REPO}:${IMAGE_TAG} ''' } } } stage('Deploy via Ansible') { steps { sshagent (credentials: ['ansible-ec2-key']) { sh ''' cd ansible ansible-playbook -i inventory.ini playbook.yml --limit flask_server -e "image_tag=${IMAGE_TAG}" ''' } } } } post { always { cleanWs() } } } output of CICD pipeline
Notes:
- Pass variables
image_tagandecr_repoto Ansible using-e. - Use
sshagentJenkins plugin and SSH credential type for the private key; ensure the credential ID matches.
7 — Security: Trivy, SonarQube, Ansible Vault, IAM best practices
Trivy
- Integrate Trivy scan in pipeline. Fail builds for
HIGH/CRITICALif policy requires. - Keep a policy: fail when critical vuln found, allow medium/low with warnings.
SonarQube
- Optional code-quality & SAST. Could be run in Jenkins stage. Use Sonarqube scanner plugin.
Ansible Vault
- Store sensitive variables (e.g., DB passwords) in
group_vars/.../vault.ymlencrypted with ansible-vault.
ansible-playbook -i inventory.ini playbook.yml --ask-vault-pass Use
ansible-vault encrypt_stringoransible-vault create.In Jenkins, store the vault password in credentials and pass with
--vault-password-fileor--vault-idsecurely.
these are the commands which i used
- Run playbook with the encrypted inventory file
ansible-playbook -i inventory.ini playbook.yml --ask-vault-pass - Edit the encrypted inventory later
ansible-vault edit inventory.ini - Decrypt if needed
ansible-vault decrypt inventory.ini Jenkins credentials
- Use Jenkins Credentials store (AWS keys, SSH keys, GitHub token). Never commit secrets.
- Use
sshagentplugin to make SSH keys available to the build agent for the duration of the block.
IAM roles & policies
-
Use least-privilege:
- ECR push/pull:
AmazonEC2ContainerRegistryPowerUseror custom policy limited to the repo.
- ECR push/pull:
- EC2 instance profile for servers that will
docker pullto fetch images should have ECR read access or useAmazonEC2ContainerRegistryReadOnly. - Prefer instance profiles over hardcoded AWS keys on hosts.
8 — Monitoring: Prometheus + Grafana + Node Exporter + Jenkins plugin
-
Add scrape targets:
- Node Exporter on EC2 instances:
<ec2_private_ip>:9100 - Flask app (if instrumented):
<flask_ip>:5000/metrics(useprometheus_flask_exporter) - Jenkins:
metrics_path: '/prometheus'(requires Prometheus plugin)
- Node Exporter on EC2 instances:
Grafana
- Add Prometheus datasource: URL
http://<prometheus_host>:9090 - Import dashboards:
Node Exporter Full (ID 1860)
Docker Monitoring → Dashboard ID: 12227
Prometheus 2.0 Stats → Dashboard ID: 3662
Set up Alerting in Grafana
9 — Correct workflow (step-by-step) + diagram
High-level workflow
- Developer pushes code to GitHub.
- Jenkins job triggers on git push.
- Jenkins pulls code, builds Docker image.
- Jenkins runs Trivy scan.
- If Trivy finds critical issues → fail the build.
- If scan passes → Jenkins logs in to ECR and pushes the image with tag (
build-N). - Jenkins calls Ansible (via
sshagent) passingimage_tag. - Ansible logs into target EC2 (using Ansible role with proper IAM or ECR login) and pulls the pushed image, stops old container and spins up a new one (docker run).
- Prometheus scrapes Node Exporter and application metrics; Grafana visualizes and triggers alerts.
- If scan passes → Jenkins logs in to ECR and pushes the image with tag (
10 — Troubleshooting & common pitfalls
- Ansible SSH fails: make sure Jenkins loads private key via
sshagentand inventory does NOT hardcodeansible_ssh_private_key_filepointing to a path that doesn’t exist on Jenkins agent. Useansible_user=ubuntuin inventory andsshagentto supply key. - ECR login failing: ensure Jenkins has AWS credentials or EC2 instance running Ansible has IAM instance profile for ECR. Also use
aws ecr get-login-password | docker login. - Docker permission denied: ensure Jenkins user in
dockergroup or run docker commands via sudo; prefer adding user to group and restarting session. - Prometheus YAML errors: YAML is whitespace-sensitive. Use 2-space indentation and validate with
docker run --rm -v /path/prom.yml:/etc/prometheus/prometheus.yml prom/prometheus --config.file=/etc/prometheus/prometheus.ymlto see parse errors. - Prometheus target 404: target reachable but wrong path. For Jenkins use
/prometheus(plugin). For Node Exporter use/metricson port 9100. - Trivy not found in Jenkins: install Trivy system-wide or add an
Install Trivystage in Jenkinsfile. - Ansible variable recursion: always pass required variables (
image_tag,ecr_repo) from pipeline into Ansible (-e "image_tag=${IMAGE_TAG} ecr_repo=${ECR_REPO}").
11 — Useful commands & snippets
Docker run Prometheus (host-mounted config):
docker run -d --name prometheus -p 9090:9090 -v /home/ubuntu/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus Start Node Exporter:
docker run -d --name node_exporter -p 9100:9100 prom/node-exporter ECR login & push (shell):
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 772954893641.dkr.ecr.us-east-1.amazonaws.com docker tag local-image:latest 772954893641.dkr.ecr.us-east-1.amazonaws.com/flask-app:build-1 docker push 772954893641.dkr.ecr.us-east-1.amazonaws.com/flask-app:build-1 Ansible run (manual):
ansible-playbook -i ansible/inventory.ini ansible/playbook.yml --limit flask_server -e "image_tag=build-1" Validate Prometheus yaml (debug):
docker run --rm -v /home/ubuntu/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus --config.file=/etc/prometheus/prometheus.yml --log.level=debug Final notes & tips
- Don’t store secrets in repo. Use Jenkins credentials and Ansible Vault.
- Use IAM roles for EC2 instances that need to pull images from ECR — avoids storing keys on servers.
- Test individual steps locally first (Docker build & run, Ansible tasks to install Docker & pull image).
- Keep your prometheus.yml simple and validate YAML syntax when editing.
👨💻 Author
Ritesh Singh
📝 Hashnode























Top comments (0)