Skip to content

Cristina-MariaG/api-spring-boot

Repository files navigation

Spring Boot REST API — Full DevOps Pipeline on AWS

A DevOps project built around an existing Spring Boot 3.2 / Java 21 REST API. The application itself is not the focus — the goal was to build a complete CI/CD pipeline around it using Jenkins, deploy it automatically to AWS EC2, and provision the entire infrastructure with Terraform and Ansible.

The Spring Boot application was used as a realistic deployment target. The work covered here is the pipeline, the infrastructure, the containerization, and the automation — not the backend code itself.

Java Spring Boot Jenkins Terraform Docker AWS PostgreSQL


Overview

The Spring Boot application (Users and Contacts CRUD API) was taken as-is and used as a realistic deployment target. The objective of this project was to build everything around it:

  • CI/CD with Jenkins — pipeline triggered on every GitHub push: build → transfer → deploy → health check
  • Infrastructure as Code — Terraform provisions all AWS resources from scratch; Ansible configures the server
  • Containerization — Docker Compose running the app and PostgreSQL 16 with health checks and restart policies
  • Secret management — no credentials in code or git history; everything flows through Jenkins credentials
  • One-command provisioning./aws/setup-infra.sh chains Terraform + Ansible and leaves a server ready to receive deployments

Tech Stack

Layer Technology
Language Java 21
Framework Spring Boot 3.2
Database PostgreSQL 16
ORM Spring Data JPA / Hibernate
Security Spring Security Crypto (BCrypt), custom API Key filter
API Docs SpringDoc OpenAPI 2.3 / Swagger UI
Build Maven Wrapper
Containerization Docker, Docker Compose
CI/CD Jenkins (runs in Docker locally)
Infrastructure Terraform, Ansible
Cloud AWS EC2, Elastic IP, S3

Architecture

GitHub Push
     │
     ▼
Jenkins (Docker, localhost:8080)
     │
     ├─ 1. Checkout  ─── clone via GitHub token
     ├─ 2. Build     ─── Maven → store-0.0.1-SNAPSHOT.jar
     ├─ 3. Prepare   ─── generate deploy.sh
     ├─ 4. Transfer  ─── SCP → JAR + Dockerfile + docker-compose.yml 
     └─ 5. Deploy    ─── SSH → docker compose down && up --build
                                        │
                                        ▼
                              AWS EC2 (Ubuntu 22.04)
                                        │
                              ┌─────────┴──────────┐
                              │    Docker Compose   │
                              ├────────────────────-┤
                              │  app  → port 8081   │
                              │  db   → PostgreSQL  │
                              └────────────────────-┘

Terraform provisions:

  • EC2 t2.micro (Ubuntu 22.04 LTS, dynamic Canonical AMI)
  • Elastic IP — fixed public address attached to the instance
  • Security Group — SSH restricted to your IP, port 8081 open
  • S3 bucket — remote Terraform state with native locking (use_lockfile) and AES-256 encryption
  • RSA 4096 key pair — generated by Terraform, stored as a Jenkins credential

Ansible configures (on the EC2 after provisioning):

  • Docker CE + Compose plugin (official Docker repository)
  • Docker auto-start at boot via systemd
  • ubuntu user added to the docker group

CI/CD Pipeline

The Jenkinsfile defines a 5-stage pipeline triggered automatically on every GitHub push via webhook:

Stage What it does
Checkout Clones the repository using a GitHub token credential
Build ./mvnw clean package — runs tests, then produces the JAR
Prepare Deployment Generates deploy.sh (docker compose down && up -d --build)
Transfer Files SCP the JAR, Dockerfile, docker-compose.yml, and .env to EC2
Deploy to Staging SSH-executes deploy.sh, waits 30s, then hits /actuator/health (expects HTTP 200)

Jenkins credentials required:

Credential ID Type Used for
github-token-id Secret text GitHub repository clone
server-ip-id Secret text EC2 IP (auto-updated by setup-infra.sh)
aws-ec2-pem SSH private key SCP and SSH access to EC2
env-file-id Secret file .env file transferred at deploy time

See JENKINS_SETUP.md for initial Jenkins configuration (plugins, credentials, GitHub webhook).

Why Jenkins and not GitHub Actions or GitLab CI?

All three tools solve the same problem — automating build, test, and deployment on code changes. The choice depends on the context.

Jenkins

Advantages Disadvantages
Self-hosted: full control over the execution environment Requires a server to run on (or a local machine)
No build minute limits Initial setup takes time (plugins, credentials, configuration)
Works on any infrastructure, including air-gapped or on-premise networks Maintenance overhead: updates, security patches
Agnostic: works with GitHub, GitLab, Bitbucket, or any Git server Groovy DSL has a learning curve
1800+ plugins, deep integration with enterprise tools (JIRA, SonarQube, Artifactory, LDAP) UI feels dated compared to modern alternatives
Programmatic pipelines with Groovy — better suited for complex conditional logic
Widely adopted in large enterprises — strong career value

GitHub Actions

Advantages Disadvantages
Zero infrastructure to set up or maintain Free tier limited to 2000 min/month on private repos, paid beyond that
Native integration directly in the GitHub repository Tightly coupled to GitHub — harder to migrate
Large marketplace of ready-made actions Less control over the execution environment
Simple YAML syntax Complex pipelines become verbose in YAML
Free for public repositories Runner customization requires self-hosted runners

GitLab CI

Advantages Disadvantages
Fully integrated with GitLab (code, issues, registry, pipeline in one place) Only relevant if the codebase is hosted on GitLab
Clean and readable YAML syntax Same YAML complexity limits as GitHub Actions at scale
Built-in container registry Requires GitLab as the hosting platform
Free shared runners included

Why Jenkins for this project

The goal was to learn how to set up a CI/CD pipeline from scratch — Jenkins is the right tool for that because it requires you to understand each piece explicitly: where it runs, how credentials are managed, how the pipeline is structured, how webhooks work. GitHub Actions or GitLab CI abstract most of that away, which is convenient in production but less instructive when learning. Jenkins is also the most common tool in enterprise environments, making it a valuable skill regardless of the project.


AWS Infrastructure

Requires: Terraform, Ansible, and AWS CLI configured with an account that has EC2, S3, and IAM permissions (aws configure).

Everything is provisioned with a single command from your local machine:

./aws/setup-infra.sh

This chains automatically: Terraform → PEM copy → EC2 IP sync into Jenkins → Ansible inventory → SSH wait → Ansible playbook. The server is ready to receive deployments when the script finishes.

To tear everything down:

./aws/destroy-infra.sh   # prompts for confirmation before destroying

What setup-infra.sh does, step by step:

Step Action
1 terraform apply on backend-setup → S3 bucket
2 terraform apply on main → EC2, Elastic IP, Security Group, key pair
3 Copy PEM to ~/.ssh/springboot-api.pem with chmod 600
4 Retrieve Elastic IP via terraform output
5 Create or update server-ip-id credential in Jenkins via REST API
6 Generate aws/ansible/inventory.ini
7 Wait for SSH to be available on the instance (polls every 10s)
8 Run ansible-playbook install.yml to configure Docker on the server

See aws/README.md for the full infrastructure breakdown.

The Terraform state is stored remotely in S3 (versioning + AES-256 encryption) and locked via S3 native locking to prevent concurrent deployments — see aws/README.md for a detailed explanation of this pattern.


API Endpoints

All endpoints under /api/* require the X-API-KEY header. Swagger UI is publicly accessible at /swagger-ui/index.html.


Getting Started

Run locally with Docker Compose

Requires: Docker Desktop (or Docker Engine), Java 21, Maven.

cp .env.example .env
# Edit .env: fill in DB_NAME, DB_USER, DB_PASSWORD, API_KEY
./mvnw clean package -DskipTests   # build the JAR first — required by the Dockerfile
docker compose up --build

The Dockerfile copies the JAR from target/ — it does not compile the code itself. The ./mvnw clean package step must run before docker compose up --build, otherwise the build will fail with a missing file error.

The API will be available at http://localhost:8081. Swagger UI: http://localhost:8081/swagger-ui/index.html

Run Jenkins locally

Requires: Docker Desktop (or Docker Engine). Jenkins itself runs inside a container — nothing else to install. To receive GitHub webhooks while running locally, expose port 8080 with ngrok (ngrok http 8080).

./jenkins_local/jenkins_start.sh   # starts Jenkins at http://localhost:8080
./jenkins_local/jenkins_stop.sh    # stops Jenkins
./jenkins_local/jenkins_logs.sh    # tails logs

Jenkins data (plugins, credentials, pipeline configuration, build history) is stored in a Docker volume named jenkins-data. Stopping and restarting the container with jenkins_stop.sh / jenkins_start.sh preserves everything — no need to reconfigure Jenkins from scratch between sessions.

Project Structure

.
├── src/                        # Spring Boot application source
├── aws/
│   ├── terraform/              # EC2, EIP, Security Group, key pair, S3 backend
│   ├── ansible/install.yml     # Docker CE installation and configuration on EC2
│   ├── setup-infra.sh          # One-command provisioning (Terraform + Ansible)
│   └── destroy-infra.sh        # One-command teardown
├── jenkins_local/              # Scripts to run Jenkins in Docker locally
├── Jenkinsfile                 # CI/CD pipeline definition
├── Dockerfile                  # App image (eclipse-temurin:21-jre-jammy)
├── docker-compose.yml          # App + PostgreSQL services
├── JENKINS_SETUP.md            # Jenkins initial configuration guide
└── .env.example                # Environment variable template

Security

No secrets are hardcoded or committed to the repository. Each secret lives in the layer that owns it:

Secret How it's handled
App credentials (.env) Jenkins secret file credential (env-file-id), SCP'd to EC2 at deploy time
EC2 PEM key Generated by Terraform, stored as Jenkins SSH credential (aws-ec2-pem), in .gitignore
GitHub token Jenkins credential (github-token-id), injected via withCredentials, never printed in logs
EC2 IP Jenkins credential (server-ip-id), auto-updated by setup-infra.sh after each terraform apply

SSH access uses the PEM key (no password). ssh-keyscan populates known_hosts before each connection — no StrictHostKeyChecking=no. The Security Group restricts SSH (port 22) to your IP only; ports 80 and 443 are not open.


Going Further

This project focuses on the CI/CD pipeline and the infrastructure automation. Several layers could be added to make it production-grade — they were intentionally left out because the goal was to learn Jenkins, not to build a complete production setup.

Reverse proxy, TLS and WAF

The application is currently exposed directly on port 8081 over plain HTTP. In a real setup, you would put a reverse proxy in front of it:

  • BunkerWeb — an nginx-based web application firewall (WAF) that blocks common attacks (SQLi, XSS, bad bots, brute force) out of the box. Drop-in replacement for a standard nginx setup with security built in.
  • Nginx + Fail2ban — a lighter alternative: nginx as a reverse proxy with Fail2ban watching the logs and automatically banning IPs that trigger too many failed requests.
  • Let's Encrypt — free TLS certificates, auto-renewed, easily integrated via Certbot with either of the above.

DNS management

Instead of exposing the raw EC2 IP, you would point a domain at the Elastic IP and manage DNS through a provider like Cloudflare. Beyond DNS, Cloudflare also acts as a CDN and adds another layer of DDoS protection and bot filtering in front of the origin server.

Jenkins on AWS instead of locally

In this project, Jenkins runs locally in Docker on your own machine. This is fine for learning but has obvious limitations: your machine needs to be on and reachable from GitHub for webhooks to work.

The natural next step would be to run Jenkins on its own dedicated EC2 instance, provisioned the same way as the application server (Terraform + Ansible), with BunkerWeb or Nginx in front to protect the Jenkins UI from the internet.

This was deliberately left out for one practical reason: cost. Running a permanent EC2 instance for Jenkins (even a t2.micro) adds to the AWS bill every hour. For a personal learning project, keeping Jenkins local avoids unnecessary spending while still covering everything the pipeline needs to do.

Docker image registry

Currently the Docker image is built directly on the EC2 server at each deployment (docker compose up --build). This means the production server does the build — it pulls the source code, compiles, and constructs the image every time.

The proper approach is to build the image once in Jenkins, push it to a registry, and have the server only pull the already-built image:

Jenkins build JAR
      │
      ▼
Jenkins build Docker image
      │
      ▼
Push image → registry (AWS ECR, Docker Hub, GitHub Container Registry)
      │
      ▼
EC2: docker pull image:v1.2 && docker compose up

This has several advantages: the server no longer needs build tools, every image gets a unique tag (commit SHA, build number), and rollback becomes trivial — just pull a previous tag.

Rollback strategy

There is currently no rollback mechanism. If the health check fails, the deployment stops, but the previous containers are already down. The server is left in a broken state.

With an image registry and tagged images, rollback is straightforward: the pipeline stores the previous image tag, and if the health check fails it automatically re-deploys the last known good version. Without a registry, an alternative is to keep a copy of the previous JAR on the server and re-run docker compose up with it if the deployment fails.

Secrets management with AWS

The application currently receives its secrets through a .env file transferred via SCP and stored on disk on the EC2 instance. Two AWS-native alternatives remove the need for this file entirely:

  • AWS Secrets Manager — secrets (database credentials, API keys) are stored centrally in AWS and retrieved by the application at runtime via the AWS SDK. Nothing is stored on disk. Secrets can be rotated without redeploying.
  • AWS Systems Manager Parameter Store — a lighter alternative for non-sensitive configuration values. Free for standard parameters, with optional encryption via KMS for sensitive ones.

Both approaches also eliminate the need to transfer a .env file through Jenkins, since the EC2 instance retrieves its own secrets directly from AWS using its IAM role.

Staging and production environments

This project deploys to a single environment. In a real setup, you would have at least two: staging and production, each on its own EC2 instance, with the pipeline deploying to staging first and promoting to production only after the health check passes.

push to main
      │
      ▼
Jenkins build + tests
      │
      ▼
Deploy → EC2 staging → health check ✓
      │
      ▼
Deploy → EC2 production → health check ✓

The Terraform structure would mirror this with separate directories per environment (aws/terraform/staging/, aws/terraform/production/), each with their own state, variables, and EC2 instance. Jenkins would hold separate credentials for each (server-ip-staging, server-ip-prod) and separate .env files.

This was left out for the same reason as the dedicated Jenkins server: cost. A second EC2 instance running permanently doubles the infrastructure bill. For a learning project focused on Jenkins, a single environment is sufficient to cover everything the pipeline needs to demonstrate.


Documentation

  • aws/README.md — Terraform resources, Ansible playbook, setup and destroy scripts in detail
  • JENKINS_SETUP.md — Jenkins plugins, credentials setup, GitHub webhook configuration

About

Full DevOps pipeline around a Spring Boot API — Jenkins CI/CD, Terraform, Ansible, AWS EC2, Docker

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors