Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# AI-BankApp-DevOps

A Spring Boot banking application used as a base for learning end-to-end DevOps — from Docker to Kubernetes to GitOps.

## Tech Stack

- **Backend:** Spring Boot 3.4.1, Java 21, Spring Security, JPA/Hibernate
- **Frontend:** Thymeleaf, Bootstrap 5, Glassmorphism UI with dark/light theme
- **Database:** MySQL 8.0
- **AI:** Ollama (self-hosted LLM chatbot, zero cost)
- **DevOps:** Docker, GitHub Actions, Kubernetes, Helm, Terraform, Prometheus, Grafana, ArgoCD

## Branches

| Branch | Description |
|--------|-------------|
| `start` | Modernized app — full backend + frontend (developer handoff) |
| `docker` | Adds Dockerfile, multi-stage build, docker-compose |
| `ai` | Adds AI chatbot powered by Ollama |
| `main` | End-to-end DevOps (WIP) |

See [ROADMAP.md](ROADMAP.md) for the full progression.

## Quick Start

### Run locally (needs Java 21 + MySQL)

```bash
# Create database
mysql -u root -p -e "CREATE DATABASE bankappdb;"

# Run the app
./mvnw spring-boot:run
```

### Run with Docker (recommended)

```bash
# Switch to docker branch
git checkout docker

# Start everything
docker compose up -d --build

# Visit http://localhost:8080
```

### Run with AI Chatbot

```bash
# Switch to ai branch
git checkout ai

# Start everything (includes Ollama)
docker compose up -d --build

# Pull the AI model (one-time)
docker exec bankapp-ollama ollama pull tinyllama

# Visit http://localhost:8080
```

## Features

- User registration & login with BCrypt passwords
- Deposit, withdraw, transfer between accounts
- Transaction history with color-coded entries
- Dark/light theme toggle (persists across sessions)
- AI chatbot that knows your balance and recent transactions
- Prometheus metrics at `/actuator/prometheus`
- Health check at `/actuator/health`
88 changes: 88 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# DevOps Roadmap — BankApp

A step-by-step progression from code to production-grade DevOps.
Each phase builds on the previous one. Check off as you go.

---

## Phase 1: Application (`start` branch)
- [x] Spring Boot backend with MySQL
- [x] Thymeleaf frontend with modern UI
- [x] Spring Security (login, register, CSRF)
- [x] Actuator + Prometheus metrics endpoint
- [x] Externalized config via environment variables

## Phase 2: Docker (`docker` branch)
- [x] Dockerfile (simple)
- [x] Dockerfile.multistage (optimized image)
- [x] docker-compose.yml (app + MySQL)
- [ ] .dockerignore file
- [ ] Push image to Docker Hub

## Phase 3: CI/CD (`cicd` branch)
- [ ] GitHub Actions workflow — build & test on PR
- [ ] Build Docker image in CI
- [ ] Push image to Docker Hub from CI
- [ ] Tag images with git SHA + `latest`

## Phase 4: Kubernetes (`k8s` branch)
- [ ] Deployment manifest (app)
- [ ] Service manifest (ClusterIP)
- [ ] ConfigMap (app config)
- [ ] Secret (DB credentials)
- [ ] MySQL StatefulSet or external DB
- [ ] Ingress with host-based routing
- [ ] Deploy to a local cluster (minikube / kind)

## Phase 5: Helm (`helm` branch)
- [ ] Helm chart for BankApp
- [ ] values.yaml for dev / prod
- [ ] Install via `helm install`

## Phase 6: IaC with Terraform (`terraform` branch)
- [ ] Provision AWS EKS cluster (or equivalent)
- [ ] RDS MySQL instance
- [ ] VPC, subnets, security groups
- [ ] State stored in S3 + DynamoDB lock

## Phase 7: Monitoring (`monitoring` branch)
- [ ] Prometheus scraping `/actuator/prometheus`
- [ ] Grafana dashboard for app metrics
- [ ] Alerting rules (high error rate, pod restarts)

## Phase 8: GitOps (`gitops` branch)
- [ ] ArgoCD installed on cluster
- [ ] App synced from Git repo to K8s
- [ ] Auto-sync on push to main

## Phase 9: Security & Quality (`security` branch)
- [ ] Trivy image scan in CI pipeline
- [ ] SonarQube code quality scan
- [ ] OWASP dependency check
- [ ] Non-root container user

## Phase 10: AI Chatbot (`ai` branch)
- [ ] Ollama container in docker-compose (self-hosted, zero cost)
- [ ] Chat REST API in Spring Boot calling Ollama
- [ ] Floating chat widget on dashboard
- [ ] Context-aware — knows user's balance and transactions
- [ ] Deploy Ollama on K8s with GPU/CPU resource limits

## Phase 11: Production Readiness (`prod` branch)
- [ ] TLS / HTTPS via cert-manager
- [ ] Horizontal Pod Autoscaler (HPA)
- [ ] Resource limits and requests
- [ ] Liveness & readiness probes
- [ ] Multi-environment setup (dev / staging / prod)

---

## The Story for Interviews

> "I took a Spring Boot banking application, integrated a self-hosted AI chatbot
> using Ollama, containerized everything with Docker, built a CI/CD pipeline with
> GitHub Actions, deployed to Kubernetes using Helm charts, provisioned cloud
> infrastructure with Terraform, set up monitoring with Prometheus and Grafana,
> and implemented GitOps with ArgoCD for automated deployments."

Each phase = one branch = one talking point.
Empty file modified mvnw
100644 → 100755
Empty file.
21 changes: 16 additions & 5 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<version>3.4.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
Expand All @@ -27,7 +27,7 @@
<url/>
</scm>
<properties>
<java.version>17</java.version>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
Expand All @@ -46,15 +46,26 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/example/bankapp/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.bankapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/register", "/login", "/css/**", "/js/**", "/actuator/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
86 changes: 86 additions & 0 deletions src/main/java/com/example/bankapp/controller/BankController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.example.bankapp.controller;

import com.example.bankapp.model.Account;
import com.example.bankapp.service.AccountService;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import java.math.BigDecimal;

@Controller
public class BankController {

private final AccountService accountService;

public BankController(AccountService accountService) {
this.accountService = accountService;
}

@GetMapping("/login")
public String loginPage() {
return "login";
}

@GetMapping("/register")
public String registerPage() {
return "register";
}

@PostMapping("/register")
public String register(@RequestParam String username,
@RequestParam String password,
Model model) {
if (accountService.registerAccount(username, password)) {
return "redirect:/login?registered";
}
model.addAttribute("error", true);
return "register";
}

@GetMapping("/dashboard")
public String dashboard(@AuthenticationPrincipal Account account, Model model) {
model.addAttribute("account", account);
return "dashboard";
}

@PostMapping("/deposit")
public String deposit(@AuthenticationPrincipal Account account,
@RequestParam BigDecimal amount,
RedirectAttributes redirectAttributes) {
accountService.deposit(account, amount);
return "redirect:/dashboard";
}

@PostMapping("/withdraw")
public String withdraw(@AuthenticationPrincipal Account account,
@RequestParam BigDecimal amount,
RedirectAttributes redirectAttributes) {
if (!accountService.withdraw(account, amount)) {
redirectAttributes.addFlashAttribute("error", "Insufficient funds.");
}
return "redirect:/dashboard";
}

@PostMapping("/transfer")
public String transfer(@AuthenticationPrincipal Account account,
@RequestParam String toUsername,
@RequestParam BigDecimal amount,
RedirectAttributes redirectAttributes) {
String error = accountService.transferAmount(account, toUsername, amount);
if (error != null) {
redirectAttributes.addFlashAttribute("error", error);
}
return "redirect:/dashboard";
}

@GetMapping("/transactions")
public String transactions(@AuthenticationPrincipal Account account, Model model) {
model.addAttribute("transactions", accountService.getTransactionHistory(account));
return "transactions";
}
}
Loading