Developer Quickstart
Overview
This guide will get you up and running with AWS Lens development in under 30 minutes. Whether you're working on the frontend, backend, or data pipeline, this quickstart provides everything you need.
Prerequisites
Required Software
| Tool | Version | Purpose | Install Command |
|---|---|---|---|
| Java | 17+ | Backend runtime | brew install openjdk@17 |
| Node.js | 20+ | Frontend runtime | brew install node@20 |
| Gradle | 7.6+ | Build tool | brew install gradle |
| Docker | 24+ | Local services | brew install docker |
| Git | 2.40+ | Version control | brew install git |
| AWS CLI | 2.13+ | AWS access | brew install awscli |
Optional Tools
| Tool | Purpose | Install Command |
|---|---|---|
| IntelliJ IDEA | Java IDE | Download from JetBrains |
| VS Code | Frontend IDE | brew install visual-studio-code |
| Postman | API testing | brew install postman |
| DBeaver | Database client | brew install dbeaver-community |
System Requirements
- OS: macOS, Linux, or Windows (WSL2)
- RAM: 16 GB minimum, 32 GB recommended
- Disk: 50 GB free space
- CPU: 4 cores minimum, 8 cores recommended
Quick Setup (5 Minutes)
Step 1: Clone Repository
# Clone the repository
git clone https://github.com/cloudkeeper/aws-lens.git
cd aws-lens
# Checkout develop branch
git checkout develop
Step 2: Start Local Services
# Start MySQL, Redis, MongoDB using Docker Compose
docker-compose up -d
# Verify services are running
docker-compose ps
# Expected output:
# mysql Up 0.0.0.0:3306->3306/tcp
# redis Up 0.0.0.0:6379->6379/tcp
# mongodb Up 0.0.0.0:27017->27017/tcp
Step 3: Configure Environment
# Copy environment template
cp .env.example .env
# Edit .env file with your settings
# Required: Database credentials, API keys
# Example .env:
DB_HOST=localhost
DB_PORT=3306
DB_NAME=lens_dev
DB_USER=lens
DB_PASSWORD=lens_password
REDIS_HOST=localhost
REDIS_PORT=6379
MONGO_HOST=localhost
MONGO_PORT=27017
# Optional: AWS credentials for local testing
AWS_ACCESS_KEY_ID=your_key
AWS_SECRET_ACCESS_KEY=your_secret
Step 4: Build & Run Backend
# Build the backend
./gradlew clean build
# Run database migrations
./gradlew flywayMigrate
# Start the backend server
./gradlew bootRun
# Server starts on http://localhost:8080
# Health check: http://localhost:8080/actuator/health
Step 5: Build & Run Frontend
# Navigate to frontend directory
cd frontend
# Install dependencies
npm install
# Start development server
npm run dev
# Frontend starts on http://localhost:3000
# Auto-opens in browser
✅ Done! You now have AWS Lens running locally.
Repository Structure
aws-lens/
├── backend/ # Spring Boot backend
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/cloudkeeper/lens/
│ │ │ │ ├── controller/ # REST API controllers
│ │ │ │ ├── service/ # Business logic
│ │ │ │ ├── repository/ # Data access (JPA)
│ │ │ │ ├── model/ # Domain models & entities
│ │ │ │ ├── dto/ # Data transfer objects
│ │ │ │ ├── config/ # Configuration classes
│ │ │ │ ├── security/ # Authentication & authorization
│ │ │ │ └── util/ # Utility classes
│ │ │ └── resources/
│ │ │ ├── application.yml # App configuration
│ │ │ └── db/migration/ # Flyway migrations
│ │ └── test/ # Unit & integration tests
│ ├── build.gradle # Gradle build file
│ └── Dockerfile # Docker image
│
├── frontend/ # React frontend
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── pages/ # Page components
│ │ ├── services/ # API clients
│ │ ├── store/ # Redux store
│ │ ├── utils/ # Utility functions
│ │ └── App.tsx # Main app component
│ ├── package.json # NPM dependencies
│ └── vite.config.ts # Vite configuration
│
├── data-pipeline/ # Spark/Airflow data processing
│ ├── airflow/
│ │ └── dags/ # Airflow DAGs
│ ├── spark/
│ │ └── jobs/ # Spark jobs
│ └── lambda/ # Lambda functions
│
├── infrastructure/ # Terraform IaC
│ ├── modules/ # Reusable Terraform modules
│ └── environments/ # Environment configs
│
├── docker-compose.yml # Local development stack
├── .env.example # Environment template
├── README.md # Project README
└── CONTRIBUTING.md # Contribution guide
Common Development Tasks
Backend Development
Create a New API Endpoint
1. Create Controller:
// src/main/java/com/cloudkeeper/lens/controller/CostController.java
package com.cloudkeeper.lens.controller;
import com.cloudkeeper.lens.dto.CostQueryRequest;
import com.cloudkeeper.lens.dto.CostResponse;
import com.cloudkeeper.lens.service.CostService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("/api/v1/costs")
public class CostController {
@Autowired
private CostService costService;
@GetMapping
public List<CostResponse> getCosts(
@Valid @ModelAttribute CostQueryRequest request
) {
return costService.getCosts(request);
}
@GetMapping("/summary")
public CostResponse getSummary(
@RequestParam String accountId,
@RequestParam String startDate,
@RequestParam String endDate
) {
return costService.getSummary(accountId, startDate, endDate);
}
}
2. Create Service:
// src/main/java/com/cloudkeeper/lens/service/CostService.java
package com.cloudkeeper.lens.service;
import com.cloudkeeper.lens.dto.CostQueryRequest;
import com.cloudkeeper.lens.dto.CostResponse;
import com.cloudkeeper.lens.model.Cost;
import com.cloudkeeper.lens.repository.CostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class CostService {
@Autowired
private CostRepository costRepository;
public List<CostResponse> getCosts(CostQueryRequest request) {
List<Cost> costs = costRepository.findCostsByDateRange(
request.getAccountId(),
request.getStartDate(),
request.getEndDate()
);
return costs.stream()
.map(this::convertToResponse)
.collect(Collectors.toList());
}
private CostResponse convertToResponse(Cost cost) {
return CostResponse.builder()
.date(cost.getDate())
.service(cost.getService())
.amount(cost.getAmount())
.build();
}
}
3. Create Repository:
// src/main/java/com/cloudkeeper/lens/repository/CostRepository.java
package com.cloudkeeper.lens.repository;
import com.cloudkeeper.lens.model.Cost;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
@Repository
public interface CostRepository extends JpaRepository<Cost, Long> {
@Query("SELECT c FROM Cost c WHERE c.accountId = :accountId " +
"AND c.date BETWEEN :startDate AND :endDate")
List<Cost> findCostsByDateRange(
@Param("accountId") String accountId,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate
);
}
4. Test the Endpoint:
# Using curl
curl -X GET "http://localhost:8080/api/v1/costs?accountId=123&startDate=2025-10-01&endDate=2025-10-26"
# Using Postman
# Import OpenAPI spec from http://localhost:8080/v3/api-docs
Run Unit Tests
# Run all tests
./gradlew test
# Run specific test class
./gradlew test --tests CostServiceTest
# Run tests with coverage
./gradlew test jacocoTestReport
# View coverage report
open build/reports/jacoco/test/html/index.html
Database Migrations
# Create new migration
cat > src/main/resources/db/migration/V1.1__add_cost_table.sql << 'EOF'
CREATE TABLE costs (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
account_id VARCHAR(255) NOT NULL,
service VARCHAR(255) NOT NULL,
date DATE NOT NULL,
amount DECIMAL(15,2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_account_date (account_id, date),
INDEX idx_service_date (service, date)
);
EOF
# Run migrations
./gradlew flywayMigrate
# Check migration status
./gradlew flywayInfo
# Rollback last migration (if needed)
./gradlew flywayUndo
Frontend Development
Create a New React Component
// src/components/CostSummary.tsx
import React, { useEffect, useState } from 'react';
import { getCostSummary } from '../services/costService';
import { CostData } from '../types';
interface CostSummaryProps {
accountId: string;
startDate: string;
endDate: string;
}
export const CostSummary: React.FC<CostSummaryProps> = ({
accountId,
startDate,
endDate
}) => {
const [data, setData] = useState<CostData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const result = await getCostSummary(accountId, startDate, endDate);
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [accountId, startDate, endDate]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!data) return null;
return (
<div className="cost-summary">
<h2>Cost Summary</h2>
<div className="metrics">
<div className="metric">
<label>Total Cost</label>
<value>${data.totalCost.toLocaleString()}</value>
</div>
<div className="metric">
<label>Daily Average</label>
<value>${data.dailyAverage.toLocaleString()}</value>
</div>
</div>
</div>
);
};
Create API Service
// src/services/costService.ts
import axios from 'axios';
import { CostData, CostQueryParams } from '../types';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:8080/api/v1';
export const getCostSummary = async (
accountId: string,
startDate: string,
endDate: string
): Promise<CostData> => {
const response = await axios.get(`${API_BASE}/costs/summary`, {
params: { accountId, startDate, endDate }
});
return response.data;
};
export const getCosts = async (params: CostQueryParams): Promise<CostData[]> => {
const response = await axios.get(`${API_BASE}/costs`, { params });
return response.data;
};
Run Frontend Tests
# Run all tests
npm run test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
# Run E2E tests
npm run test:e2e
Debugging
Backend Debugging
IntelliJ IDEA:
-
Create Run Configuration:
- Main class:
com.cloudkeeper.lens.LensApplication - VM options:
-Dspring.profiles.active=dev - Environment variables: Load from
.env
- Main class:
-
Set breakpoints in code
-
Run in Debug mode (Shift+F9)
Command Line:
# Run with remote debugging enabled
./gradlew bootRun --debug-jvm
# Connect debugger on port 5005
Frontend Debugging
Browser DevTools:
// Add debugger statement
const fetchData = async () => {
debugger; // Execution pauses here
const result = await getCostSummary(accountId, startDate, endDate);
console.log('Data:', result);
};
VS Code:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/frontend/src"
}
]
}
Testing
Unit Tests (Backend)
// src/test/java/com/cloudkeeper/lens/service/CostServiceTest.java
package com.cloudkeeper.lens.service;
import com.cloudkeeper.lens.dto.CostQueryRequest;
import com.cloudkeeper.lens.model.Cost;
import com.cloudkeeper.lens.repository.CostRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CostServiceTest {
@Mock
private CostRepository costRepository;
@InjectMocks
private CostService costService;
@Test
void getCosts_shouldReturnCosts() {
// Given
String accountId = "123";
LocalDate start = LocalDate.of(2025, 10, 1);
LocalDate end = LocalDate.of(2025, 10, 26);
List<Cost> mockCosts = Arrays.asList(
new Cost(accountId, "EC2", start, 100.0),
new Cost(accountId, "S3", start, 50.0)
);
when(costRepository.findCostsByDateRange(accountId, start, end))
.thenReturn(mockCosts);
// When
CostQueryRequest request = new CostQueryRequest(accountId, start, end);
List<CostResponse> result = costService.getCosts(request);
// Then
assertEquals(2, result.size());
assertEquals(100.0, result.get(0).getAmount());
}
}
Integration Tests (Backend)
// src/test/java/com/cloudkeeper/lens/controller/CostControllerIntegrationTest.java
package com.cloudkeeper.lens.controller;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class CostControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void getCosts_shouldReturnOk() throws Exception {
mockMvc.perform(get("/api/v1/costs")
.param("accountId", "123")
.param("startDate", "2025-10-01")
.param("endDate", "2025-10-26"))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray());
}
}
Unit Tests (Frontend)
// src/components/__tests__/CostSummary.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { CostSummary } from '../CostSummary';
import * as costService from '../../services/costService';
jest.mock('../../services/costService');
describe('CostSummary', () => {
it('renders cost data', async () => {
// Mock API response
(costService.getCostSummary as jest.Mock).mockResolvedValue({
totalCost: 1000,
dailyAverage: 50
});
render(
<CostSummary
accountId="123"
startDate="2025-10-01"
endDate="2025-10-26"
/>
);
await waitFor(() => {
expect(screen.getByText('$1,000')).toBeInTheDocument();
expect(screen.getByText('$50')).toBeInTheDocument();
});
});
});
Common Issues & Solutions
Issue: Port Already in Use
Problem: Port 8080 is already in use
Solution:
# Find process using port 8080
lsof -i :8080
# Kill the process
kill -9 <PID>
# Or use a different port
./gradlew bootRun --args='--server.port=8081'
Issue: Database Connection Failed
Problem: Cannot connect to MySQL
Solution:
# Check if MySQL is running
docker-compose ps mysql
# Restart MySQL
docker-compose restart mysql
# Check logs
docker-compose logs mysql
# Verify credentials in .env match docker-compose.yml
Issue: Frontend Build Fails
Problem: npm run build fails
Solution:
# Clear node_modules and reinstall
rm -rf node_modules package-lock.json
npm install
# Clear vite cache
rm -rf node_modules/.vite
# Rebuild
npm run build
Next Steps
Learn More
- API Reference - Complete API documentation
- Data Models - Database schemas
- Component Design - Architecture details
Contribute
- CONTRIBUTING.md - See project root for contribution guidelines
- GitHub Issues - Bug reports & feature requests
Get Help
- Slack: #lens-dev channel
- Email: dev@cloudkeeper.com
- Office Hours: Tuesdays 2-3 PM PT
Quick Reference
Useful Commands
# Backend
./gradlew clean build # Clean build
./gradlew bootRun # Run backend
./gradlew test # Run tests
./gradlew flywayMigrate # Database migrations
# Frontend
npm install # Install dependencies
npm run dev # Development server
npm run build # Production build
npm run test # Run tests
# Docker
docker-compose up -d # Start services
docker-compose down # Stop services
docker-compose logs -f # View logs
docker-compose restart # Restart services
# Git
git checkout -b feature/my-feature # Create feature branch
git add . # Stage changes
git commit -m "Add feature" # Commit changes
git push origin feature/my-feature # Push to remote
Environment Variables
| Variable | Default | Description |
|---|---|---|
DB_HOST | localhost | MySQL host |
DB_PORT | 3306 | MySQL port |
DB_NAME | lens_dev | Database name |
REDIS_HOST | localhost | Redis host |
SERVER_PORT | 8080 | Backend port |
REACT_APP_API_BASE | http://localhost:8080/api/v1 | API URL |
🎉 You're all set! Happy coding!
For questions or issues, reach out on Slack (#lens-dev) or email dev@cloudkeeper.com