# Complete Grafana k6 Load Testing Tutorial > **Project Repository**: [https://github.com/Yang92047111/Grafana_k6_Load_Testing_Quick_Start](https://github.com/Yang92047111/Grafana_k6_Load_Testing_Quick_Start) This tutorial provides a comprehensive guide to setting up and using Grafana, Prometheus, and k6 for monitoring and load testing cloud-native applications. ## Table of Contents 1. [Introduction to k6](#Introduction-to-k6) 2. [How k6 Works](#How-k6-Works) 3. [Project Structure](#Project-Structure) 4. [Quick Start Guide](#Quick-Start-Guide) 5. [Setting Up the Environment](#Setting-Up-the-Environment) 6. [Creating a Go Backend Service](#Creating-a-Go-Backend-Service) 7. [Dockerizing the Application](#Dockerizing-the-Application) 8. [Writing k6 Load Tests](#Writing-k6-Load-Tests) 9. [Running k6 Tests](#Running-k6-Tests) 10. [Monitoring and Analysis](#Monitoring-and-Analysis) 11. [CI/CD Pipeline](#CI/CD-Pipeline) 12. [Troubleshooting](#Troubleshooting) ## Introduction to k6 k6 is a developer-centric, free and open-source load testing tool built for making performance testing a productive and enjoyable experience. It's designed to test the reliability and performance of systems under load. ### Key Features: - **JavaScript-based**: Write tests in modern JavaScript (ES6+) - **Developer-friendly**: CLI tool with great developer experience - **Cloud and on-premise**: Run locally or in the cloud - **Rich metrics**: Built-in metrics and custom metrics support - **Integrations**: Works with CI/CD pipelines and monitoring systems ## How k6 Works k6 follows a straightforward architecture for load testing: ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ k6 CLI Tool │ │ Test Scripts │ │ Target API │ │ │ │ (JavaScript) │ │ (Go Service) │ │ ┌─────────────┐ │ │ │ │ │ │ │Load Testing │ │───▶│ ┌──────────────┐ │───▶│ ┌─────────────┐ │ │ │ Engine │ │ │ │HTTP Requests │ │ │ │Endpoints │ │ │ └─────────────┘ │ │ │Checks & Thresholds│ │ │Handlers │ │ │ │ │ │Virtual Users │ │ │ │ │ │ │ ┌─────────────┐ │ │ └──────────────┘ │ │ └─────────────┘ │ │ │ Metrics │ │ │ │ │ │ │ │ Collection │ │ └──────────────────┘ └─────────────────┘ │ └─────────────┘ │ │ │ └─────────────────┘ │ │ │ │ │ ▼ ▼ ▼ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Results │ │ Monitoring │ │ Logs & │ │ & Reports │ │ (Grafana) │ │ Responses │ └─────────────────┘ └──────────────────┘ └─────────────────┘ ``` ### k6 Test Lifecycle: 1. **Init**: Code runs once per VU (Virtual User) to set up test data 2. **Setup**: Runs once before the test starts 3. **VU Code**: Main test function that runs for each iteration 4. **Teardown**: Runs once after the test completes ## Project Structure This tutorial implements a complete cloud-native load testing solution with the following structure: ``` grafana-k6-quick-start/ ├── .dockerignore # Docker build optimization ├── .github/workflows/ # CI/CD pipeline (GitHub Actions) ├── backend/ # Go REST API service │ ├── handlers/ # HTTP request handlers + tests │ ├── middleware/ # HTTP middleware + tests │ ├── models/ # Data models and structures │ ├── main.go # Application entry point │ ├── Dockerfile # Container configuration │ └── go.mod/go.sum # Go module dependencies ├── monitoring/ # Observability stack │ ├── grafana/ # Dashboards and provisioning │ └── prometheus/ # Prometheus configuration ├── tests/ # k6 load testing scripts │ ├── smoke-test.js # Basic functionality validation │ ├── load-test.js # Standard load testing │ ├── stress-test.js # High load stress testing │ ├── spike-test.js # Sudden load spike testing │ └── comprehensive-test.js # Full CRUD testing ├── docker-compose.yml # Local development stack ├── Makefile # Build and deployment commands └── README.md # Project documentation ``` ## Quick Start Guide ### Option A: Local Development (API only) ```bash # Clone the repository (if needed) git clone <repository-url> cd grafana-k6-quick-start # Quick setup - downloads dependencies make dev-setup # Start the API server make run # In another terminal, test the API curl http://localhost:8080/health # Run a quick smoke test make k6-smoke ``` ### Option B: Full Setup with Monitoring ```bash # Complete development environment with Docker make dev-setup-full # Or step by step: make deps tidy # Setup Go dependencies make docker-build # Build Docker image make compose-up # Start monitoring stack # Access services: # API: http://localhost:8080 # Grafana: http://localhost:3000 (admin/admin) # Prometheus: http://localhost:9090 ``` ### Available Make Commands ```bash make help # Show all available commands make test # Run unit tests make build # Build Go application make docker-build # Build Docker image make k6-smoke # Run smoke test make k6-load # Run load test make monitoring-up # Start monitoring stack make dev-teardown # Clean up environment ``` ## Setting Up the Environment ### Prerequisites: - Docker and Docker Compose - Go 1.21+ - k6 ### Installation Commands: ```bash # Install k6 (macOS) brew install k6 # Install k6 (Linux) sudo gpg -k sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list sudo apt-get update sudo apt-get install k6 # Verify installation k6 version ``` ## Creating a Go Backend Service Let's create a simple REST API service in Go that we'll use as our testing target. ### Project Structure: ``` backend/ ├── main.go ├── handlers/ │ └── handlers.go ├── models/ │ └── models.go ├── go.mod └── Dockerfile ``` ### main.go ```go package main import ( "log" "net/http" "os" "time" "github.com/gorilla/mux" "your-project/handlers" ) func main() { r := mux.NewRouter() // Routes r.HandleFunc("/health", handlers.HealthCheck).Methods("GET") r.HandleFunc("/api/users", handlers.GetUsers).Methods("GET") r.HandleFunc("/api/users", handlers.CreateUser).Methods("POST") r.HandleFunc("/api/users/{id}", handlers.GetUser).Methods("GET") r.HandleFunc("/api/users/{id}", handlers.UpdateUser).Methods("PUT") r.HandleFunc("/api/users/{id}", handlers.DeleteUser).Methods("DELETE") // Middleware r.Use(handlers.LoggingMiddleware) r.Use(handlers.CORSMiddleware) port := os.Getenv("PORT") if port == "" { port = "8080" } srv := &http.Server{ Handler: r, Addr: ":" + port, WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } log.Printf("Server starting on port %s", port) log.Fatal(srv.ListenAndServe()) } ``` ### handlers/handlers.go ```go package handlers import ( "encoding/json" "fmt" "log" "net/http" "strconv" "time" "github.com/gorilla/mux" "your-project/models" ) var users = make(map[int]models.User) var userID = 1 func HealthCheck(w http.ResponseWriter, r *http.Request) { response := map[string]interface{}{ "status": "healthy", "timestamp": time.Now().Unix(), "service": "user-api", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func GetUsers(w http.ResponseWriter, r *http.Request) { // Simulate some processing time time.Sleep(50 * time.Millisecond) userList := make([]models.User, 0, len(users)) for _, user := range users { userList = append(userList, user) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(userList) } func CreateUser(w http.ResponseWriter, r *http.Request) { var user models.User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Simulate processing time time.Sleep(100 * time.Millisecond) user.ID = userID user.CreatedAt = time.Now() users[userID] = user userID++ w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(user) } func GetUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } user, exists := users[id] if !exists { http.Error(w, "User not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } func UpdateUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } existingUser, exists := users[id] if !exists { http.Error(w, "User not found", http.StatusNotFound) return } var updatedUser models.User if err := json.NewDecoder(r.Body).Decode(&updatedUser); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Simulate processing time time.Sleep(75 * time.Millisecond) updatedUser.ID = id updatedUser.CreatedAt = existingUser.CreatedAt updatedUser.UpdatedAt = time.Now() users[id] = updatedUser w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(updatedUser) } func DeleteUser(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err != nil { http.Error(w, "Invalid user ID", http.StatusBadRequest) return } if _, exists := users[id]; !exists { http.Error(w, "User not found", http.StatusNotFound) return } delete(users, id) w.WriteHeader(http.StatusNoContent) } // Middleware func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) }) } func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } ``` ### models/models.go ```go package models import "time" type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` Age int `json:"age"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at,omitempty"` } ``` ### go.mod ```go module your-project go 1.21 require github.com/gorilla/mux v1.8.0 ``` ## Dockerizing the Application ### Dockerfile ```dockerfile # Build stage FROM golang:1.21-alpine AS builder WORKDIR /app # Copy go mod files COPY go.mod go.sum ./ RUN go mod download # Copy source code COPY . . # Build the application RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . # Final stage FROM alpine:latest RUN apk --no-cache add ca-certificates WORKDIR /root/ # Copy the binary from builder stage COPY --from=builder /app/main . # Expose port EXPOSE 8080 # Command to run CMD ["./main"] ``` ### docker-compose.yml ```yaml version: '3.8' services: user-api: build: ./backend ports: - "8080:8080" environment: - PORT=8080 healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 restart: unless-stopped ``` ## Writing k6 Load Tests ### Basic Load Test (load-test.js) ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; import { Counter, Rate, Trend } from 'k6/metrics'; // Custom metrics const errorRate = new Rate('error_rate'); const customTrend = new Trend('custom_duration'); const httpReqFailed = new Counter('http_req_failed'); // Test configuration export const options = { stages: [ { duration: '30s', target: 10 }, // Ramp up to 10 users { duration: '1m', target: 10 }, // Stay at 10 users { duration: '30s', target: 50 }, // Ramp up to 50 users { duration: '2m', target: 50 }, // Stay at 50 users { duration: '30s', target: 0 }, // Ramp down to 0 users ], thresholds: { http_req_duration: ['p(95)<500'], // 95% of requests must be below 500ms http_req_failed: ['rate<0.05'], // Error rate must be below 5% error_rate: ['rate<0.1'], // Custom error rate }, }; // Base URL - adjust based on your setup const BASE_URL = 'http://localhost:8080'; // const BASE_URL = 'http://user-api.local'; // For ingress // Test data const users = [ { name: 'John Doe', email: 'john@example.com', age: 30 }, { name: 'Jane Smith', email: 'jane@example.com', age: 25 }, { name: 'Bob Johnson', email: 'bob@example.com', age: 35 }, ]; export function setup() { // This runs once before all VUs start console.log('Starting load test setup...'); // Health check const healthResponse = http.get(`${BASE_URL}/health`); if (healthResponse.status !== 200) { throw new Error('Health check failed'); } return { message: 'Setup complete' }; } export default function(data) { // Main test function - runs for each VU iteration // Health check let response = http.get(`${BASE_URL}/health`); const healthCheckPassed = check(response, { 'health check status is 200': (r) => r.status === 200, 'health check has status field': (r) => JSON.parse(r.body).status === 'healthy', }); if (!healthCheckPassed) { errorRate.add(1); httpReqFailed.add(1); } // Get all users response = http.get(`${BASE_URL}/api/users`); check(response, { 'get users status is 200': (r) => r.status === 200, 'get users response time < 500ms': (r) => r.timings.duration < 500, }); // Create a new user const userData = users[Math.floor(Math.random() * users.length)]; userData.email = `${userData.name.replace(' ', '').toLowerCase()}-${Date.now()}@example.com`; const createUserParams = { headers: { 'Content-Type': 'application/json', }, }; response = http.post(`${BASE_URL}/api/users`, JSON.stringify(userData), createUserParams); const createUserPassed = check(response, { 'create user status is 201': (r) => r.status === 201, 'create user has id': (r) => JSON.parse(r.body).id > 0, 'create user response time < 1000ms': (r) => r.timings.duration < 1000, }); if (createUserPassed && response.status === 201) { const createdUser = JSON.parse(response.body); // Get the created user response = http.get(`${BASE_URL}/api/users/${createdUser.id}`); check(response, { 'get user by id status is 200': (r) => r.status === 200, 'get user by id returns correct user': (r) => JSON.parse(r.body).id === createdUser.id, }); // Update the user const updatedUserData = { ...userData, age: userData.age + 1, }; response = http.put(`${BASE_URL}/api/users/${createdUser.id}`, JSON.stringify(updatedUserData), createUserParams); check(response, { 'update user status is 200': (r) => r.status === 200, 'update user age changed': (r) => JSON.parse(r.body).age === updatedUserData.age, }); // Delete the user response = http.del(`${BASE_URL}/api/users/${createdUser.id}`); check(response, { 'delete user status is 204': (r) => r.status === 204, }); } else { errorRate.add(1); } // Record custom metrics customTrend.add(response.timings.duration); // Sleep between 1-3 seconds sleep(Math.random() * 2 + 1); } export function teardown(data) { // This runs once after all VUs have finished console.log('Load test teardown complete'); } ``` ### Spike Test (spike-test.js) ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '10s', target: 10 }, // Below normal load { duration: '1m', target: 10 }, // Normal load { duration: '10s', target: 100 }, // Around the breaking point { duration: '3m', target: 100 }, // Beyond the breaking point { duration: '10s', target: 10 }, // Scale down. Recovery stage. { duration: '3m', target: 10 }, // Recovery stage. { duration: '10s', target: 0 }, // Scale down to 0 users. ], thresholds: { http_req_duration: ['p(99)<1500'], // 99% of requests must be below 1.5s http_req_failed: ['rate<0.1'], // Error rate must be below 10% }, }; const BASE_URL = 'http://localhost:8080'; export default function() { const response = http.get(`${BASE_URL}/health`); check(response, { 'spike test - status is 200': (r) => r.status === 200, 'spike test - response time OK': (r) => r.timings.duration < 2000, }); sleep(1); } ``` ### Stress Test (stress-test.js) ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { stages: [ { duration: '2m', target: 100 }, // Ramp up to 100 users { duration: '5m', target: 100 }, // Stay at 100 users { duration: '2m', target: 200 }, // Ramp up to 200 users { duration: '5m', target: 200 }, // Stay at 200 users { duration: '2m', target: 300 }, // Ramp up to 300 users { duration: '5m', target: 300 }, // Stay at 300 users { duration: '2m', target: 400 }, // Ramp up to 400 users { duration: '5m', target: 400 }, // Stay at 400 users { duration: '10m', target: 0 }, // Ramp down to 0 users ], thresholds: { http_req_duration: ['p(99)<2000'], // 99% of requests must be below 2s http_req_failed: ['rate<0.1'], // Error rate must be below 10% }, }; const BASE_URL = 'http://localhost:8080'; export default function() { const responses = http.batch([ ['GET', `${BASE_URL}/health`], ['GET', `${BASE_URL}/api/users`], ]); check(responses[0], { 'stress test - health check status is 200': (r) => r.status === 200, }); check(responses[1], { 'stress test - users endpoint status is 200': (r) => r.status === 200, }); sleep(1); } ``` ### Smoke Test (smoke-test.js) ```javascript import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { vus: 1, // 1 user looping for 1 minute duration: '1m', thresholds: { http_req_duration: ['p(99)<1500'], // 99% of requests must be below 1.5s http_req_failed: ['rate<0.01'], // Error rate must be below 1% }, }; const BASE_URL = 'http://localhost:8080'; export default function() { const response = http.get(`${BASE_URL}/health`); check(response, { 'smoke test - status is 200': (r) => r.status === 200, 'smoke test - service is healthy': (r) => JSON.parse(r.body).status === 'healthy', }); sleep(1); } ``` ## Running k6 Tests ### Test Execution Commands: ```bash # Run smoke test (quick validation) k6 run smoke-test.js # Run basic load test k6 run load-test.js # Run stress test k6 run stress-test.js # Run spike test k6 run spike-test.js # Run test with custom VUs and duration k6 run --vus 50 --duration 30s load-test.js # Run test and output results to file k6 run --out json=results.json load-test.js # Run test with custom thresholds k6 run --threshold http_req_duration=p(95)<200 load-test.js ``` ### k6 in Docker: ```bash # Run k6 tests in Docker docker run --rm -i grafana/k6:latest run - <load-test.js # Run with volume mount for test files docker run --rm -v "$PWD:/tests" grafana/k6:latest run /tests/load-test.js # Run with network access to local services docker run --rm --network=host -v "$PWD:/tests" grafana/k6:latest run /tests/load-test.js ``` ### k6 in Kubernetes: #### k6-job.yaml ```yaml apiVersion: batch/v1 kind: Job metadata: name: k6-load-test spec: template: spec: containers: - name: k6 image: grafana/k6:latest command: ["k6", "run", "--vus", "50", "--duration", "5m", "/scripts/load-test.js"] volumeMounts: - name: k6-scripts mountPath: /scripts env: - name: K6_OUT value: "json=/tmp/results.json" volumes: - name: k6-scripts configMap: name: k6-scripts restartPolicy: Never backoffLimit: 1 ``` #### Create ConfigMap for test scripts: ```bash kubectl create configmap k6-scripts --from-file=load-test.js kubectl apply -f k6-job.yaml # Check job status kubectl get jobs kubectl logs job/k6-load-test ``` ## Monitoring and Analysis ### Setting up Grafana and Prometheus with Docker Compose: The monitoring stack is already configured in the `docker-compose.yml` file with Grafana and Prometheus services. ### Start the monitoring stack: ```bash # Start all services including monitoring docker compose up -d # Access the services: # Grafana: http://localhost:3000 (admin/admin) # Prometheus: http://localhost:9090 ``` ### Run k6 with Prometheus output: ```bash # Run k6 with Prometheus remote write output (when using docker-compose) k6 run --out experimental-prometheus-rw=http://localhost:9090/api/v1/write load-test.js # Alternative: Run k6 in Docker with Prometheus output docker compose exec k6 k6 run --out experimental-prometheus-rw=http://prometheus:9090/api/v1/write /tests/load-test.js ``` ### k6 Grafana Dashboard Configuration: 1. **Access Grafana**: http://localhost:3000 (admin/admin) 2. **Add Prometheus Data Source**: - URL: http://localhost:9090 - Access: Server (default) - HTTP Method: GET 3. **Import k6 Dashboard**: - Go to + → Import - Use dashboard ID: 19665 (k6 Prometheus dashboard) - Or create custom dashboard with these queries: #### Key Grafana Queries (PromQL): ```promql # HTTP Request Duration (95th percentile) histogram_quantile(0.95, rate(k6_http_req_duration_bucket[5m])) # HTTP Request Rate rate(k6_http_reqs_total[5m]) # Error Rate (percentage) rate(k6_http_req_failed_total[5m]) / rate(k6_http_reqs_total[5m]) * 100 # Virtual Users k6_vus # Data Received Rate rate(k6_data_received_total[5m]) # Data Sent Rate rate(k6_data_sent_total[5m]) # HTTP Request Duration Average rate(k6_http_req_duration_sum[5m]) / rate(k6_http_req_duration_count[5m]) # Checks Pass Rate rate(k6_checks_passed_total[5m]) / (rate(k6_checks_passed_total[5m]) + rate(k6_checks_failed_total[5m])) * 100 ``` ## Advanced k6 Testing Patterns ### Data-Driven Testing: #### users-data.json ```json [ {"name": "Alice Johnson", "email": "alice@example.com", "age": 28}, {"name": "Bob Smith", "email": "bob@example.com", "age": 34}, {"name": "Charlie Brown", "email": "charlie@example.com", "age": 22}, {"name": "Diana Prince", "email": "diana@example.com", "age": 31}, {"name": "Eve Davis", "email": "eve@example.com", "age": 26} ] ``` #### data-driven-test.js ```javascript import http from 'k6/http'; import { check } from 'k6'; const userData = JSON.parse(open('./users-data.json')); export const options = { vus: 10, duration: '2m', }; const BASE_URL = 'http://localhost:8080'; export default function() { // Pick a random user from the dataset const user = userData[Math.floor(Math.random() * userData.length)]; // Modify email to make it unique user.email = `${user.name.replace(' ', '').toLowerCase()}-${__ITER}@example.com`; const response = http.post(`${BASE_URL}/api/users`, JSON.stringify(user), { headers: { 'Content-Type': 'application/json' }, }); check(response, { 'user created successfully': (r) => r.status === 201, 'user has correct name': (r) => JSON.parse(r.body).name === user.name, }); } ``` ### Session-Based Testing: #### session-test.js ```javascript import http from 'k6/http'; import { check, group, sleep } from 'k6'; export const options = { vus: 20, duration: '5m', }; const BASE_URL = 'http://localhost:8080'; export default function() { // Simulate user session group('User Registration Flow', () => { const userData = { name: `User ${__VU}-${__ITER}`, email: `user-${__VU}-${__ITER}@example.com`, age: Math.floor(Math.random() * 50) + 18, }; // Create user let response = http.post(`${BASE_URL}/api/users`, JSON.stringify(userData), { headers: { 'Content-Type': 'application/json' }, }); const userId = check(response, { 'user created': (r) => r.status === 201, }) ? JSON.parse(response.body).id : null; if (userId) { sleep(1); // Get user profile group('Profile Operations', () => { response = http.get(`${BASE_URL}/api/users/${userId}`); check(response, { 'profile retrieved': (r) => r.status === 200, }); sleep(2); // Update user profile userData.age += 1; response = http.put(`${BASE_URL}/api/users/${userId}`, JSON.stringify(userData), { headers: { 'Content-Type': 'application/json' }, }); check(response, { 'profile updated': (r) => r.status === 200, }); }); sleep(1); // Clean up - delete user response = http.del(`${BASE_URL}/api/users/${userId}`); check(response, { 'user deleted': (r) => r.status === 204, }); } }); sleep(Math.random() * 5 + 1); } ``` ### Performance Testing Best Practices: #### comprehensive-test.js ```javascript import http from 'k6/http'; import { check, group, sleep, fail } from 'k6'; import { Rate, Trend, Counter } from 'k6/metrics'; // Custom metrics const errorRate = new Rate('errors'); const responseTimeTrend = new Trend('response_time'); const customCounter = new Counter('custom_counter'); export const options = { stages: [ { duration: '1m', target: 20 }, { duration: '3m', target: 20 }, { duration: '1m', target: 0 }, ], thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate<0.05'], errors: ['rate<0.1'], response_time: ['p(95)<600'], }, ext: { loadimpact: { name: 'Comprehensive API Test', projectID: 123456, }, }, }; const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080'; export function setup() { // Setup phase - run once console.log(`Testing API at: ${BASE_URL}`); const response = http.get(`${BASE_URL}/health`); if (response.status !== 200) { fail('API is not healthy'); } return { baseUrl: BASE_URL, testData: generateTestData(100), }; } export default function(data) { const testUser = data.testData[Math.floor(Math.random() * data.testData.length)]; group('API Health Check', () => { const response = http.get(`${data.baseUrl}/health`); const success = check(response, { 'health check passes': (r) => r.status === 200, 'has healthy status': (r) => JSON.parse(r.body).status === 'healthy', }); if (!success) { errorRate.add(1); } responseTimeTrend.add(response.timings.duration); }); group('User CRUD Operations', () => { // Create let response = http.post(`${data.baseUrl}/api/users`, JSON.stringify(testUser), { headers: { 'Content-Type': 'application/json' }, }); const createSuccess = check(response, { 'user created successfully': (r) => r.status === 201, 'response has user id': (r) => JSON.parse(r.body).id > 0, }); if (!createSuccess) { errorRate.add(1); return; // Skip remaining operations if creation fails } const userId = JSON.parse(response.body).id; customCounter.add(1); sleep(0.5); // Read response = http.get(`${data.baseUrl}/api/users/${userId}`); check(response, { 'user retrieved successfully': (r) => r.status === 200, 'correct user returned': (r) => JSON.parse(r.body).id === userId, }) || errorRate.add(1); sleep(0.5); // Update testUser.age += 1; response = http.put(`${data.baseUrl}/api/users/${userId}`, JSON.stringify(testUser), { headers: { 'Content-Type': 'application/json' }, }); check(response, { 'user updated successfully': (r) => r.status === 200, 'age was updated': (r) => JSON.parse(r.body).age === testUser.age, }) || errorRate.add(1); sleep(0.5); // Delete response = http.del(`${data.baseUrl}/api/users/${userId}`); check(response, { 'user deleted successfully': (r) => r.status === 204, }) || errorRate.add(1); }); sleep(Math.random() * 2 + 1); } export function teardown(data) { console.log('Test completed'); console.log(`Base URL used: ${data.baseUrl}`); } function generateTestData(count) { const names = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry']; const domains = ['example.com', 'test.com', 'demo.com']; const users = []; for (let i = 0; i < count; i++) { users.push({ name: `${names[i % names.length]} ${i}`, email: `user${i}@${domains[i % domains.length]}`, age: Math.floor(Math.random() * 50) + 18, }); } return users; } ``` ## Continuous Integration Integration ### GitHub Actions Workflow: #### .github/workflows/load-test.yml ```yaml name: Load Testing with k6 on: push: branches: [main] pull_request: branches: [main] schedule: - cron: '0 2 * * *' # Run daily at 2 AM jobs: load-test: runs-on: ubuntu-latest services: api: image: user-api:latest ports: - 8080:8080 env: PORT: 8080 options: >- --health-cmd "wget --quiet --tries=1 --spider http://localhost:8080/health" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - name: Setup Go uses: actions/setup-go@v4 with: go-version: '1.21' - name: Build Docker image run: | cd backend docker build -t user-api:latest . - name: Install k6 run: | sudo gpg -k sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list sudo apt-get update sudo apt-get install k6 - name: Wait for API to be ready run: | timeout 300 bash -c 'until curl -f http://localhost:8080/health; do sleep 5; done' - name: Run smoke test run: k6 run tests/smoke-test.js - name: Run load test run: k6 run tests/load-test.js --out json=results.json - name: Upload test results uses: actions/upload-artifact@v3 if: always() with: name: k6-test-results path: results.json - name: Comment PR with results if: github.event_name == 'pull_request' uses: actions/github-script@v6 with: script: | const fs = require('fs'); try { const results = fs.readFileSync('results.json', 'utf8'); const metrics = JSON.parse(results); // Process and comment results... } catch (error) { console.log('No results file found'); } ``` ## CI/CD Pipeline The project includes a comprehensive GitHub Actions pipeline that automates testing, building, and deployment. Here's what the pipeline includes: ### Pipeline Overview ```yaml # .github/workflows/ci-cd.yml name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: test: # Unit tests with coverage build: # Docker image build and push load-test: # Automated k6 testing helm-test: # Helm chart validation security-scan: # Security vulnerability scanning deploy-staging: # Deploy to staging environment deploy-production: # Deploy to production post-deploy-tests: # Production smoke tests ``` ### Pipeline Features - **Unit Testing**: Go test suite with coverage reporting - **Security Scanning**: Gosec (static analysis) + Trivy (image scanning) - **Load Testing**: Automated k6 tests against built containers - **Helm Validation**: Chart linting and templating verification - **Multi-stage Deployment**: Staging → Production with approvals - **Post-deployment Testing**: Smoke tests in production ### Using the Pipeline The pipeline automatically triggers on: - **Pull Requests**: Runs tests and validation only - **Develop Branch**: Deploys to staging environment - **Main Branch**: Deploys to production environment ## Troubleshooting ### Common Issues and Solutions #### Docker Build Problems ```bash # Test Docker connectivity make docker-test # Clean Docker cache make docker-clean # Test network connectivity make network-test # If Docker issues persist, use local development make dev-setup make run ``` #### API Not Starting ```bash # Check if port is in use lsof -i :8080 # Check API logs make compose-logs # Test API directly go run backend/main.go ``` #### k6 Tests Failing ```bash # Verify API is healthy curl http://localhost:8080/health # Test with verbose output k6 run --http-debug tests/smoke-test.js # Check if base URL is correct export BASE_URL=http://localhost:8080 k6 run tests/smoke-test.js ``` #### Kubernetes Deployment Issues ```bash # Check pod status kubectl get pods kubectl describe pod <pod-name> # Check service connectivity kubectl get svc kubectl port-forward service/user-api 8080:80 # Check Helm deployment helm status user-api helm list ``` #### Monitoring Stack Issues ```bash # Check Prometheus connection kubectl port-forward -n monitoring service/prometheus 9090:9090 curl http://localhost:9090/-/healthy # Verify Grafana datasource # Login to Grafana → Configuration → Data Sources # Check k6 output format k6 run --out experimental-prometheus-rw=http://localhost:9090/api/v1/write tests/smoke-test.js ``` ### Make Commands for Troubleshooting ```bash make help # Show all available commands make docker-test # Test Docker connectivity make network-test # Test network connectivity make test-coverage # Run tests with coverage report make k8s-status # Show Kubernetes resource status make monitoring-up # Restart monitoring stack ``` ## Conclusion and Next Steps This comprehensive tutorial has covered: 1. **k6 Fundamentals**: Understanding how k6 works and its architecture 2. **Environment Setup**: Local development with Docker 3. **Backend Service**: Go-based REST API with proper endpoints 4. **Container Orchestration**: Docker containerization 5. **Load Testing Scenarios**: Various test types (smoke, load, stress, spike) 6. **Monitoring Integration**: Grafana and Prometheus for metrics visualization 7. **Advanced Patterns**: Data-driven and session-based testing 8. **CI/CD Integration**: Automated testing in GitHub Actions ### Next Steps: 1. **Scale Testing**: Experiment with distributed k6 testing across multiple nodes 2. **Advanced Monitoring**: Integrate with Prometheus, Jaeger for distributed tracing 3. **Security Testing**: Add authentication and authorization testing scenarios 4. **Database Integration**: Include database load testing scenarios 5. **Cloud Deployment**: Deploy to cloud providers (AWS EKS, GCP GKE, Azure AKS) 6. **Performance Tuning**: Use k6 results to optimize application performance 7. **Custom Extensions**: Develop k6 extensions for specific testing needs ### Key Takeaways: - **Start Small**: Begin with smoke tests before running intensive load tests - **Monitor Everything**: Always collect metrics and monitor system behavior - **Test Early**: Integrate performance testing into your development workflow - **Realistic Scenarios**: Design tests that mirror real user behavior - **Infrastructure Matters**: Ensure your test environment resembles production - **Continuous Improvement**: Use test results to drive performance optimizations This setup provides a solid foundation for comprehensive load testing with k6, enabling you to ensure your applications can handle production traffic effectively.