End-to-end DevOps bootcamp project that provisions AWS infrastructure with Terraform, configures services with Ansible, builds the ShipIt Launchpad web application into a Docker image, publishes that image to Amazon ECR, and deploys it to an EC2 web server. Repository documentation is automatically synchronized to a GitHub Pages site using GitHub Actions.
URLs
| Service | URL |
|---|---|
| Application | https://web.artisandevops.com |
| Documentation site | GitHub Pages |
| Source repository | dani21894/devops-bootcamp-project |
| Monitoring | https://monitoring.artisandevops.com |
Repository Layout
The repository is organized as a monorepo. It contains the web application, automation, infrastructure-as-code, and documentation tooling in one place.
| Path | Purpose |
|---|---|
app/ |
ShipIt Launchpad source code, Dockerfile, and .dockerignore |
ansible/ |
Inventory, Ansible configuration, and server deployment playbooks |
terraform/ |
AWS infrastructure-as-code files |
scripts/sync-docs.mjs |
Synchronizes README.md and the root index.html documentation site |
package.json |
Root Node.js tooling for the documentation sync command |
.github/workflows/commit-pipeline.yml |
Synchronizes documentation after documentation changes are pushed |
.github/workflows/static.yml |
Deploys the root index.html to GitHub Pages |
index.html |
GitHub Pages entry point generated from this README |
.
├── .github/
│ └── workflows/
│ ├── commit-pipeline.yml
│ └── static.yml
├── ansible/
├── app/
│ ├── Dockerfile
│ ├── .dockerignore
│ ├── package.json
│ ├── public/
│ ├── scripts/
│ └── src/
├── scripts/
│ └── sync-docs.mjs
├── terraform/
├── .gitignore
├── index.html
├── package.json
└── README.md
Architecture
The deployment pipeline moves the application through source control, containerization, a private container registry, configuration management, and public DNS.
GitHub repository
|
v
Ansible controller (10.0.0.135)
- builds Docker image
- pushes image to Amazon ECR
|
v
Amazon ECR private repository
462525375635.dkr.ecr.ap-southeast-1.amazonaws.com/
devops-bootcamp/final-project-dani21894:v1
|
v
Web EC2 server (10.0.0.5)
- Ansible pulls image from ECR
- Docker runs Nginx container on port 80
|
v
Cloudflare DNS
web.artisandevops.com
|
v
Public ShipIt Launchpad application
Infrastructure nodes
| Node | Private IP | Role |
|---|---|---|
| Ansible controller | 10.0.0.135 |
Builds and publishes the application image; runs Ansible playbooks |
| Web server | 10.0.0.5 |
Pulls the ECR image and serves the application container on port 80 |
| Monitoring server | 10.0.0.136 |
Hosts monitoring services and participates in exporter monitoring |
All infrastructure is deployed in AWS region ap-southeast-1.
Quickstart
Clone the repository:
git clone https://github.com/dani21894/devops-bootcamp-project.git
cd devops-bootcamp-project
Provision or update AWS infrastructure using Terraform:
cd terraform
terraform init
terraform plan
terraform apply
Run Ansible from the controller to deploy the web image:
cd ../ansible
ansible-playbook -i inventory.ini --syntax-check playbook-web.yaml
ansible-playbook -i inventory.ini --limit web playbook-web.yaml
Verify that the web server responds from inside the VPC:
curl -I http://10.0.0.5/
Expected result:
HTTP/1.1 200 OK
Terraform
Terraform defines the AWS infrastructure used by the project. The exact file names can differ, but the Terraform directory should contain the source configuration for networking, security groups, EC2 instances, IAM resources, outputs, and inventory generation where applicable.
Typical Terraform responsibilities
- Create the VPC, subnets, route tables, internet/NAT connectivity, and security groups.
- Create EC2 instances for the controller, web server, and monitoring stack.
- Associate IAM roles so the controller can push to ECR and the web server can pull from ECR.
- Configure inbound rules required by the deployment.
- Output instance addresses or generate inventory inputs for Ansible.
Security requirements
The public web server needs inbound HTTP access:
| Protocol | Port | Source | Reason |
|---|---|---|---|
| TCP | 80 |
0.0.0.0/0 |
Public HTTP access to the Nginx container |
| TCP | 22 |
Restricted / VPC source | Administrative SSH when required |
Do not commit Terraform state, plans, .terraform/, private keys, or secret variable files. They are excluded through .gitignore.
Ansible
Ansible configures the servers after infrastructure is available.
Inventory groups
The deployment inventory uses these groups:
[web]
10.0.0.5
[monitoring]
10.0.0.136
[exporters]
10.0.0.5
A host can belong to more than one group. The web server belongs to both web and exporters because it runs the application and is also a monitoring target.
Web deployment
ansible/playbook-web.yaml deploys the web application. Its main tasks are:
- Ensure Docker is running and enabled.
- Install AWS CLI requirements if needed.
- Authenticate Docker to private Amazon ECR.
- Stop the older Docker Compose Nginx stack if present.
- Remove the previous
webcontainer. - Pull the selected ECR image.
- Run the new
webcontainer with80:80port publishing. - Verify the container serves HTTP 200 locally on the web server.
Run the deployment:
cd ansible
ansible-playbook -i inventory.ini --limit web playbook-web.yaml
Check the running container:
ansible web \
-i inventory.ini \
-b \
-m ansible.builtin.command \
-a "docker ps"
Expected port mapping:
0.0.0.0:80->80/tcp
Application
The application is ShipIt Launchpad, a Vite-based web application using Three.js. The project source is stored under app/.
Application commands
Run these commands from the application directory:
cd app
npm ci
npm run dev
npm test
npm run build
npm run preview
| Command | Purpose |
|---|---|
npm ci |
Install exact dependency versions from package-lock.json |
npm run dev |
Start the Vite development server |
npm test |
Run the application preflight test |
npm run build |
Generate production files in dist/ |
npm run preview |
Serve the built application locally for validation |
Docker image
The application uses a multi-stage Docker build.
- The Node.js build stage installs dependencies using
npm ci. - Vite generates optimized static files with
npm run build. - The Nginx runtime stage copies only
dist/and serves it on port 80.
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Build and validate the image on the Ansible controller:
cd ~/app/ship
sudo docker build -t shipit-launchpad:v1 .
sudo docker run -d \
--name shipit-test \
-p 8080:80 \
shipit-launchpad:v1
curl -I http://127.0.0.1:8080/
Stop the temporary test container after validation:
sudo docker rm -f shipit-test
Amazon ECR
The container image is stored in private Amazon ECR.
Registry: 462525375635.dkr.ecr.ap-southeast-1.amazonaws.com
Repository: devops-bootcamp/final-project-dani21894
Tag: v1
Full image URI:
462525375635.dkr.ecr.ap-southeast-1.amazonaws.com/devops-bootcamp/final-project-dani21894:v1
Set image variables on the controller:
export AWS_REGION=ap-southeast-1
export ECR_REGISTRY=462525375635.dkr.ecr.ap-southeast-1.amazonaws.com
export IMAGE_REPO=devops-bootcamp/final-project-dani21894
export IMAGE_TAG=v1
export IMAGE_URI=${ECR_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG}
Authenticate Docker, tag the image, and push it:
aws ecr get-login-password --region "$AWS_REGION" \
| sudo docker login \
--username AWS \
--password-stdin "$ECR_REGISTRY"
sudo docker tag shipit-launchpad:v1 "$IMAGE_URI"
sudo docker push "$IMAGE_URI"
Verify the pushed ECR image:
aws ecr describe-images \
--region "$AWS_REGION" \
--repository-name "$IMAGE_REPO" \
--query 'imageDetails[].{Tags:imageTags,PushedAt:imagePushedAt,Digest:imageDigest}' \
--output table
IAM permissions
The controller instance role needs ECR push permissions, including:
ecr:GetAuthorizationToken
ecr:BatchCheckLayerAvailability
ecr:InitiateLayerUpload
ecr:UploadLayerPart
ecr:CompleteLayerUpload
ecr:PutImage
The web EC2 instance role needs permission to pull from ECR, such as AmazonEC2ContainerRegistryReadOnly.
Cloudflare DNS
The web application runs on the EC2 web server but is exposed using Cloudflare DNS.
Create an A record in the artisandevops.com Cloudflare zone:
| Field | Value |
|---|---|
| Type | A |
| Name | web |
| Content | Public IPv4 address of the web EC2 instance |
| Proxy status | Proxied (orange cloud) |
| TTL | Auto |
Do not use the private IP 10.0.0.5 in Cloudflare. Cloudflare must point to the public IPv4 address of the web EC2 instance.
Verify the published application:
curl -I http://web.artisandevops.com
Monitoring
The monitoring server is 10.0.0.136. Node Exporter targets only the web server through the exporters Ansible group.
The monitoring access URL is https://monitoring.artisandevops.com/
CI/CD
Two GitHub Actions workflows live under .github/workflows/.
| Workflow | Purpose |
|---|---|
commit-pipeline.yml |
Synchronizes README.md and root index.html and auto-commits generated documentation changes |
static.yml |
Deploys the root static site to GitHub Pages |
Documentation sync
The documentation sync script keeps README.md and index.html aligned:
- When
README.mdchanges,index.htmlis regenerated from it. - Every
##heading in the README becomes a documentation card in the synced section of the web page. - When
index.htmlchanges inside the synced card area,README.mdis regenerated from its cards. - If both documents change,
README.mdtakes precedence. - The generated content is restricted to the range between
<!-- SYNC:START -->and<!-- SYNC:END -->. - Styling, hero content, and footer content outside that range remain unchanged.
Run the sync locally from the repository root:
npm install
npm run sync:docs
npm run sync:docs -- --write
The first command is a dry run. The final command writes the generated result to disk.
GitHub Pages
GitHub Pages is configured with:
Repository Settings -> Pages -> Build and deployment -> Source: GitHub Actions
The Pages workflow uploads the repository root, including the generated index.html, and publishes it at:
https://dani21894.github.io/devops-bootcamp-project/
CI/CD validation
To test the complete documentation pipeline:
nano README.md
npm run sync:docs -- --write
git add README.md index.html
git commit -m "docs: update deployment documentation"
git push origin main
After the push:
Docs Syncruns and synchronizes the documentation files.- If needed,
github-actions[bot]creates a commit containing generated changes. Deploy static site to GitHub Pagespublishes the updatedindex.html.- The GitHub Pages documentation site reflects the latest README content.
Operational Commands
Check web deployment
curl -I http://10.0.0.5/
curl -I http://web.artisandevops.com
Check running web container
ansible web \
-i ansible/inventory.ini \
-b \
-m ansible.builtin.command \
-a "docker ps"
View web container logs
ansible web \
-i ansible/inventory.ini \
-b \
-m ansible.builtin.command \
-a "docker logs web"
Release a new application version
Use a new image tag for each release rather than overwriting a previous deployment tag.
cd ~/app/ship
sudo docker build -t shipit-launchpad:v2 .
export IMAGE_TAG=v2
export IMAGE_URI=${ECR_REGISTRY}/${IMAGE_REPO}:${IMAGE_TAG}
sudo docker tag shipit-launchpad:v2 "$IMAGE_URI"
sudo docker push "$IMAGE_URI"
Update image_tag in ansible/playbook-web.yaml to v2, then run:
cd ~/ansible/ansible
ansible-playbook -i inventory.ini --limit web playbook-web.yaml
Roll back
To roll back to a working release, change image_tag in ansible/playbook-web.yaml to a prior valid ECR tag and rerun the same web deployment playbook.
Security Notes
The following files must never be committed to GitHub:
- AWS credentials and access keys.
- GitHub personal access tokens.
- SSH private keys,
.pem, and.keyfiles. .envfiles containing secrets.- Terraform state files and plans.
- Ansible vault passwords or secret variable files.
The root .gitignore excludes common sensitive and generated files, including node_modules/, dist/, .terraform/, *.tfstate, *.tfvars, .env*, keys, and credentials.
Project Status
[✓] Application built with Vite
[✓] Multi-stage Docker image built with Node.js and Nginx
[✓] Image pushed to private Amazon ECR
[✓] Ansible pulls the ECR image on the web server
[✓] Docker container serves the application on port 80
[✓] HTTP verification returned 200 OK
[✓] Cloudflare record exposes web.artisandevops.com
[✓] Documentation sync workflow is active
[✓] GitHub Pages deployment workflow is active