Carvel is a suite of reliable, single-purpose, composable tools that help you build, configure, and deploy applications to Kubernetes. For Java developers, Carvel provides a powerful alternative to traditional Helm charts and shell scripts for managing complex application deployments.
What is Carvel?
Carvel (formerly k14s) provides a set of specialized tools:
- ytt: YAML templating tool for patching and overlaying
- kbld: Image building and resolving tool
- kapp: Application deployment tool for Kubernetes
- imgpkg: Bundle and package management
- vendir: Declarative directory population
Carvel vs Traditional Approaches
Traditional: Java App → Docker Build → Helm Charts → kubectl apply Carvel: Java App → ytt Templates → kbld Image Mgmt → kapp Deployment
Hands-On Tutorial: Complete Carvel Setup for Java Microservices
Let's build a complete Java microservices deployment pipeline using Carvel tools.
Step 1: Project Structure Setup
java-carvel-demo/ ├── apps/ │ ├── user-service/ │ ├── order-service/ │ └── api-gateway/ ├── packages/ │ ├── base/ │ ├── overlays/ │ └── bundles/ ├── config/ │ ├── ytt/ │ └── kapp/ ├── build/ │ └── scripts/ ├── k8s/ └── README.md
Step 2: Carvel Tool Installation
install-carvel.sh:
#!/bin/bash # Install Carvel tools echo "Installing Carvel tools..." # Download and install ytt curl -L -o /tmp/ytt https://github.com/vmware-tanzu/carvel-ytt/releases/download/v0.45.4/ytt-linux-amd64 sudo install /tmp/ytt /usr/local/bin/ytt # Download and install kbld curl -L -o /tmp/kbld https://github.com/vmware-tanzu/carvel-kbld/releases/download/v0.36.4/kbld-linux-amd64 sudo install /tmp/kbld /usr/local/bin/kbld # Download and install kapp curl -L -o /tmp/kapp https://github.com/vmware-tanzu/carvel-kapp/releases/download/v0.55.1/kapp-linux-amd64 sudo install /tmp/kapp /usr/local/bin/kapp # Download and install imgpkg curl -L -o /tmp/imgpkg https://github.com/vmware-tanzu/carvel-imgpkg/releases/download/v0.37.1/imgpkg-linux-amd64 sudo install /tmp/imgpkg /usr/local/bin/imgpkg # Download and install vendir curl -L -o /tmp/vendir https://github.com/vmware-tanzu/carvel-vendir/releases/download/v0.34.1/vendir-linux-amd64 sudo install /tmp/vendir /usr/local/bin/vendir echo "Carvel tools installed successfully!" echo "ytt version: $(ytt version)" echo "kbld version: $(kbld version)" echo "kapp version: $(kapp version)"
Step 3: Java Application Setup
apps/user-service/pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>user-service</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<docker.image.prefix>ghcr.io/your-username</docker.image.prefix>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.0</version>
<configuration>
<to>
<image>${docker.image.prefix}/user-service:${project.version}</image>
</to>
</configuration>
</plugin>
</plugins>
</build>
</project>
apps/user-service/src/main/java/com/example/userservice/UserServiceApplication.java:
package com.example.userservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
apps/user-service/src/main/java/com/example/userservice/controller/UserController.java:
package com.example.userservice.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
@Value("${app.version:1.0.0}")
private String appVersion;
@Value("${spring.application.name:user-service}")
private String appName;
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getUser(@PathVariable String id) {
log.info("Fetching user with id: {}", id);
return ResponseEntity.ok(Map.of(
"id", id,
"name", "John Doe",
"email", "[email protected]",
"service", appName,
"version", appVersion
));
}
@GetMapping("/health")
public ResponseEntity<Map<String, String>> health() {
return ResponseEntity.ok(Map.of(
"status", "UP",
"service", appName,
"version", appVersion
));
}
}
Step 4: Base Kubernetes Configuration with ytt
packages/base/user-service.yml:
#@ load("@ytt:data", "data")
#@ load("@ytt:assert", "assert")
#@ def app_name():
#@ return "user-service"
#@ end
#@ def labels():
app.kubernetes.io/name: #@ app_name()
app.kubernetes.io/version: #@ data.values.version
app.kubernetes.io/component: backend
app.kubernetes.io/part-of: java-carvel-demo
#@ end
---
apiVersion: v1
kind: Service
metadata:
name: #@ app_name()
labels: #@ labels()
spec:
selector:
app.kubernetes.io/name: #@ app_name()
ports:
- port: 8080
targetPort: 8080
name: http
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: #@ app_name()
labels: #@ labels()
spec:
replicas: #@ data.values.replicas
selector:
matchLabels:
app.kubernetes.io/name: #@ app_name()
template:
metadata:
labels: #@ labels()
annotations:
build.version: #@ data.values.version
build.timestamp: #@ data.values.build_timestamp
spec:
containers:
- name: #@ app_name()
image: #@ data.values.image
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
env:
- name: SPRING_APPLICATION_NAME
value: #@ app_name()
- name: APP_VERSION
value: #@ data.values.version
- name: JAVA_OPTS
value: #@ data.values.java_opts
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: #@ app_name() + "-hpa"
labels: #@ labels()
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: #@ app_name()
minReplicas: #@ data.values.min_replicas
maxReplicas: #@ data.values.max_replicas
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
packages/base/values.yml:
#@data/values --- # Default values for all environments app_name: "user-service" version: "1.0.0" build_timestamp: "20240101-120000" image: "ghcr.io/your-username/user-service:1.0.0" replicas: 2 min_replicas: 1 max_replicas: 5 java_opts: "-Xmx256m -Xms128m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom" environment: "development" database: url: "jdbc:postgresql://localhost:5432/users" username: "postgres" password: "password"
Step 5: Environment-Specific Overlays
packages/overlays/development/values.yml:
#@data/values --- #@yaml/text-templated-strings environment: "development" replicas: 1 min_replicas: 1 max_replicas: 3 java_opts: "-Xmx256m -Xms128m -Dspring.profiles.active=dev -Djava.security.egd=file:/dev/./urandom" database: url: "jdbc:postgresql://postgresql.development.svc.cluster.local:5432/users" username: "dev_user" password: "dev_password" config_maps: - name: "dev-config" data: LOG_LEVEL: "DEBUG" FEATURE_FLAGS: "new-ui,experimental-features"
packages/overlays/development/config-map.yml:
#@ load("@ytt:overlay", "overlay")
#@overlay/match by=overlay.subset({"kind": "Deployment"})
---
spec:
template:
spec:
containers:
- name: user-service
envFrom:
- configMapRef:
name: dev-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dev-config
data:
#@yaml/text-templated-strings
LOG_LEVEL: "DEBUG"
SPRING_PROFILES_ACTIVE: "development"
DATABASE_URL: #@ data.values.database.url
FEATURE_FLAGS: "new-ui,experimental-features"
packages/overlays/production/values.yml:
#@data/values --- #@yaml/text-templated-strings environment: "production" replicas: 3 min_replicas: 2 max_replicas: 10 java_opts: "-Xmx512m -Xms256m -Dspring.profiles.active=prod -Djava.security.egd=file:/dev/./urandom -XX:+UseContainerSupport" database: url: "jdbc:postgresql://postgresql.production.svc.cluster.local:5432/users" username: "prod_user" password: #@ data.values.database.password config_maps: - name: "prod-config" data: LOG_LEVEL: "INFO" FEATURE_FLAGS: "stable-features-only"
packages/overlays/production/resource-limits.yml:
#@ load("@ytt:overlay", "overlay")
#@overlay/match by=overlay.subset({"kind": "Deployment"})
---
spec:
template:
spec:
containers:
- name: user-service
#@overlay/match missing_ok=True
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1000m"
#@overlay/match missing_ok=True
envFrom:
- configMapRef:
name: prod-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: prod-config
data:
#@yaml/text-templated-strings
LOG_LEVEL: "INFO"
SPRING_PROFILES_ACTIVE: "production"
DATABASE_URL: #@ data.values.database.url
FEATURE_FLAGS: "stable-features-only"
Step 6: kbld Image Management
packages/kbld.yml:
apiVersion: kbld.k14s.io/v1alpha1 kind: Config sources: - image: ghcr.io/your-username/user-service:1.0.0 path: ../../apps/user-service # Build using Jib for Java applications # Prerequisites: # - mvn command available # - jib-maven-plugin configured in pom.xml build: buildMethod: jib jib: project: maven args: - jib:build - -Dimage=ghcr.io/your-username/user-service:1.0.0 - image: ghcr.io/your-username/order-service:1.0.0 path: ../../apps/order-service build: buildMethod: jib jib: project: maven args: - jib:build - -Dimage=ghcr.io/your-username/order-service:1.0.0 overrides: - image: ghcr.io/your-username/user-service:1.0.0 newImage: ghcr.io/your-username/user-service:latest preresolved: true - image: ghcr.io/your-username/order-service:1.0.0 newImage: ghcr.io/your-username/order-service:latest preresolved: true
Step 7: Build Scripts
build/build-images.sh:
#!/bin/bash
set -e
echo "Building Java applications with Carvel..."
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Configuration
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APPS_DIR="$PROJECT_ROOT/apps"
PACKAGES_DIR="$PROJECT_ROOT/packages"
VERSION="${VERSION:-1.0.0}"
ENVIRONMENT="${ENVIRONMENT:-development}"
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
build_java_app() {
local app_name=$1
local app_dir="$APPS_DIR/$app_name"
if [ ! -d "$app_dir" ]; then
log_error "Application directory not found: $app_dir"
return 1
fi
log_info "Building $app_name..."
cd "$app_dir"
# Build using Maven
mvn clean package -DskipTests
# Build Docker image using Jib
mvn jib:build -Dimage="ghcr.io/your-username/$app_name:$VERSION"
log_info "Successfully built $app_name:$VERSION"
}
resolve_images_with_kbld() {
log_info "Resolving images with kbld..."
cd "$PACKAGES_DIR"
# Resolve images and update configuration
kbld -f base/ -f kbld.yml > resolved.yml
log_info "Images resolved and saved to resolved.yml"
}
generate_k8s_config() {
local environment=$1
local output_dir="$PROJECT_ROOT/k8s/$environment"
log_info "Generating Kubernetes configuration for $environment..."
mkdir -p "$output_dir"
# Generate configuration using ytt
ytt \
-f packages/base/ \
-f "packages/overlays/$environment/" \
-f packages/kbld.yml \
--data-value version="$VERSION" \
--data-value-yaml build_timestamp="$(date -u +"%Y%m%d-%H%M%S")" \
> "$output_dir/manifests.yml"
log_info "Kubernetes configuration generated: $output_dir/manifests.yml"
}
deploy_with_kapp() {
local environment=$1
local app_name="java-carvel-demo-$environment"
log_info "Deploying to $environment with kapp..."
kapp deploy \
-a "$app_name" \
-f "k8s/$environment/manifests.yml" \
-c \
--diff-changes \
--yes
}
# Main execution
main() {
log_info "Starting Carvel build process for Java applications"
log_info "Version: $VERSION"
log_info "Environment: $ENVIRONMENT"
# Build Java applications
build_java_app "user-service"
build_java_app "order-service"
build_java_app "api-gateway"
# Resolve images
resolve_images_with_kbld
# Generate Kubernetes configuration
generate_k8s_config "$ENVIRONMENT"
# Deploy if requested
if [ "$DEPLOY" = "true" ]; then
deploy_with_kapp "$ENVIRONMENT"
fi
log_info "Build process completed successfully!"
}
# Handle command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--version)
VERSION="$2"
shift 2
;;
--environment)
ENVIRONMENT="$2"
shift 2
;;
--deploy)
DEPLOY="true"
shift
;;
*)
log_error "Unknown option: $1"
exit 1
;;
esac
done
main
Step 8: Package Bundles with imgpkg
packages/bundles/bundle.yml:
#@ load("@ytt:data", "data")
apiVersion: imgpkg.carvel.dev/v1alpha1
kind: Bundle
metadata:
name: java-carvel-demo-bundle
labels:
app.kubernetes.io/name: java-carvel-demo
app.kubernetes.io/version: #@ data.values.version
spec:
image: ghcr.io/your-username/java-carvel-demo-bundle:#@ data.values.version
resources:
- path: base/
image: ghcr.io/your-username/user-service:#@ data.values.version
- path: base/
image: ghcr.io/your-username/order-service:#@ data.values.version
- path: overlays/development/
- path: overlays/production/
- path: kbld.yml
build/create-bundle.sh:
#!/bin/bash
set -e
echo "Creating application bundle with imgpkg..."
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PACKAGES_DIR="$PROJECT_ROOT/packages"
VERSION="${VERSION:-1.0.0}"
BUNDLE_IMAGE="ghcr.io/your-username/java-carvel-demo-bundle:$VERSION"
# Create bundle
imgpkg push -b "$BUNDLE_IMAGE" -f "$PACKAGES_DIR" --file-exclude ".git/**/*"
echo "Bundle created: $BUNDLE_IMAGE"
# Create bundle lock file
imgpkg copy -b "$BUNDLE_IMAGE" --to-tar ./bundle.tar
echo "Bundle locked and saved to bundle.tar"
Step 9: kapp Application Deployment
config/kapp/app-config.yml:
apiVersion: kapp.k14s.io/v1alpha1
kind: Config
rebaseRules:
- path: [metadata, annotations, kapp.k14s.io/association]
type: copy
sources: [new, existing]
resourceMatchers:
- apiVersionKindMatcher: {apiVersion: v1, kind: Service}
changeGroupBindings:
- name: deployment
resourceMatchers:
- apiVersionKindMatcher: {apiVersion: apps/v1, kind: Deployment}
- name: service
resourceMatchers:
- apiVersionKindMatcher: {apiVersion: v1, kind: Service}
- name: config
resourceMatchers:
- apiVersionKindMatcher: {apiVersion: v1, kind: ConfigMap}
- apiVersionKindMatcher: {apiVersion: v1, kind: Secret}
changeRuleBindings:
- rules: [upsert, delete]
resourceMatchers:
- apiVersionKindMatcher: {apiVersion: v1, kind: Namespace}
- rules: [upsert]
resourceMatchers:
- apiVersionKindMatcher: {apiVersion: v1, kind: Secret}
- apiVersionKindMatcher: {apiVersion: v1, kind: ConfigMap}
- rules: [upsert]
resourceMatchers:
- apiVersionKindMatcher: {apiVersion: apps/v1, kind: Deployment}
- apiVersionKindMatcher: {apiVersion: v1, kind: Service}
deploy/deploy-app.sh:
#!/bin/bash
set -e
echo "Deploying Java application with kapp..."
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENVIRONMENT="${1:-development}"
APP_NAME="java-carvel-demo-$ENVIRONMENT"
MANIFESTS_FILE="$PROJECT_ROOT/k8s/$ENVIRONMENT/manifests.yml"
if [ ! -f "$MANIFESTS_FILE" ]; then
echo "Error: Manifests file not found: $MANIFESTS_FILE"
exit 1
fi
# Deploy using kapp
kapp deploy \
-a "$APP_NAME" \
-f "$MANIFESTS_FILE" \
-c \
--diff-changes \
--apply-ignored \
--logs-all \
--kapp-config "$PROJECT_ROOT/config/kapp/app-config.yml" \
"$@"
echo "Application deployed successfully: $APP_NAME"
# Display app status
kapp inspect -a "$APP_NAME" --tree
Step 10: Multi-Service Configuration
packages/base/api-gateway.yml:
#@ load("@ytt:data", "data")
#@ def app_name():
#@ return "api-gateway"
#@ end
#@ def labels():
app.kubernetes.io/name: #@ app_name()
app.kubernetes.io/version: #@ data.values.version
app.kubernetes.io/component: gateway
app.kubernetes.io/part-of: java-carvel-demo
#@ end
---
apiVersion: v1
kind: Service
metadata:
name: #@ app_name()
labels: #@ labels()
spec:
selector:
app.kubernetes.io/name: #@ app_name()
ports:
- port: 80
targetPort: 8080
name: http
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: #@ app_name()
labels: #@ labels()
spec:
replicas: #@ data.values.replicas
selector:
matchLabels:
app.kubernetes.io/name: #@ app_name()
template:
metadata:
labels: #@ labels()
annotations:
build.version: #@ data.values.version
spec:
containers:
- name: #@ app_name()
image: #@ data.values.api_gateway_image
ports:
- containerPort: 8080
name: http
env:
- name: USER_SERVICE_URL
value: "http://user-service:8080"
- name: ORDER_SERVICE_URL
value: "http://order-service:8080"
- name: SPRING_APPLICATION_NAME
value: #@ app_name()
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
packages/base/ingress.yml:
#@ load("@ytt:data", "data")
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: java-carvel-demo-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host: #@ data.values.ingress_host
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 8080
- path: /orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: api-gateway
port:
number: 8080
Step 11: Advanced ytt Templates
packages/base/lib/java_app.star:
def create_java_deployment(name, image, port=8080, replicas=2, env_vars=None, resources=None):
"""Create a standardized Java application deployment"""
if env_vars is None:
env_vars = {}
if resources is None:
resources = {
"requests": {"memory": "256Mi", "cpu": "100m"},
"limits": {"memory": "512Mi", "cpu": "500m"}
}
return {
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": name,
"labels": {
"app.kubernetes.io/name": name,
"app.kubernetes.io/component": "java-app"
}
},
"spec": {
"replicas": replicas,
"selector": {
"matchLabels": {
"app.kubernetes.io/name": name
}
},
"template": {
"metadata": {
"labels": {
"app.kubernetes.io/name": name
}
},
"spec": {
"containers": [{
"name": name,
"image": image,
"ports": [{"containerPort": port, "name": "http"}],
"env": [{"name": k, "value": v} for k, v in env_vars.items()],
"resources": resources,
"livenessProbe": {
"httpGet": {
"path": "/actuator/health",
"port": port
},
"initialDelaySeconds": 30,
"periodSeconds": 10
},
"readinessProbe": {
"httpGet": {
"path": "/actuator/health",
"port": port
},
"initialDelaySeconds": 5,
"periodSeconds": 5
}
}]
}
}
}
}
def create_service(name, port=8080, target_port=8080):
"""Create a service for a Java application"""
return {
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": name,
"labels": {
"app.kubernetes.io/name": name
}
},
"spec": {
"selector": {
"app.kubernetes.io/name": name
},
"ports": [{
"port": port,
"targetPort": target_port,
"name": "http"
}],
"type": "ClusterIP"
}
}
packages/base/templates/java-app.yml:
#@ load("lib/java_app.star", "create_java_deployment", "create_service")
#@ load("@ytt:data", "data")
#@ for app in data.values.apps:
---
#@ yaml.encode(create_service(app.name, app.port))
---
#@ yaml.encode(create_java_deployment(
#@ app.name,
#@ app.image,
#@ app.port,
#@ app.replicas,
#@ app.env_vars,
#@ app.resources
#@ ))
#@ end
Step 12: CI/CD Integration
.github/workflows/carvel-deploy.yml:
name: Deploy with Carvel
on:
push:
branches: [ main, develop ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ${{ github.repository_owner }}
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build Java applications
run: |
mvn -f apps/user-service/pom.xml clean package -DskipTests
mvn -f apps/order-service/pom.xml clean package -DskipTests
- name: Run tests
run: |
mvn -f apps/user-service/pom.xml test
mvn -f apps/order-service/pom.xml test
deploy-development:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v4
- name: Install Carvel tools
run: |
curl -L https://carvel.dev/install.sh | bash
sudo mv ytt kbld kapp imgpkg /usr/local/bin/
- name: Build and push images
run: |
./build/build-images.sh --version ${{ github.sha }} --environment development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to development
run: |
./deploy/deploy-app.sh development
env:
KUBECONFIG: ${{ secrets.KUBECONFIG_DEV }}
deploy-production:
needs: build-and-test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- name: Install Carvel tools
run: |
curl -L https://carvel.dev/install.sh | bash
sudo mv ytt kbld kapp imgpkg /usr/local/bin/
- name: Build and push images
run: |
./build/build-images.sh --version ${{ github.ref_name }} --environment production --deploy
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to production
run: |
./deploy/deploy-app.sh production
env:
KUBECONFIG: ${{ secrets.KUBECONFIG_PROD }}
Step 13: Monitoring and Observability
packages/overlays/monitoring/service-monitor.yml:
#@ load("@ytt:overlay", "overlay")
#@overlay/match by=overlay.subset({"kind": "Deployment"})
---
spec:
template:
metadata:
#@overlay/match missing_ok=True
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: java-apps-monitor
spec:
selector:
matchLabels:
app.kubernetes.io/part-of: java-carvel-demo
endpoints:
- port: http
path: /actuator/prometheus
interval: 30s
Step 14: Usage Examples
Deploy to Development:
# Build and deploy to development ./build/build-images.sh --version 1.0.0 --environment development --deploy # Or manually ytt -f packages/base/ -f packages/overlays/development/ -v version=1.0.0 | kbld -f - | kapp deploy -a java-app-dev -f - -y
Deploy to Production:
# Build and deploy to production ./build/build-images.sh --version 1.0.0 --environment production --deploy # With custom values ytt -f packages/base/ -f packages/overlays/production/ -v version=1.0.0 -v database.password=$DB_PASS | kbld -f - | kapp deploy -a java-app-prod -f - -y
Check Application Status:
# List applications kapp list # Inspect specific app kapp inspect -a java-app-dev --tree # Check changes kapp app-change list -a java-app-dev # View logs kapp logs -a java-app-dev -f
Best Practices
1. Security
- Use secret management for sensitive data
- Implement image signing and verification
- Apply RBAC configurations
- Use network policies for service communication
2. Configuration Management
- Separate configuration from code
- Use environment-specific overlays
- Implement configuration validation
- Maintain configuration drift detection
3. Deployment Strategies
- Use progressive rollouts with kapp
- Implement health checks and readiness probes
- Configure proper resource limits
- Set up monitoring and alerting
Benefits for Java Applications
- Consistent Deployments: Repeatable, reliable deployment process
- Configuration Management: Powerful templating with ytt
- Image Management: Automated image building and resolution with kbld
- Application Awareness: kapp understands application relationships
- Bundle Distribution: Portable application packages with imgpkg
Conclusion
Carvel tooling provides a modern, composable approach to deploying Java applications to Kubernetes that offers:
- Declarative configuration with ytt templating
- Reliable image management with kbld
- Application-centric deployment with kapp
- Portable packaging with imgpkg
- Comprehensive dependency management with vendir
By adopting Carvel for your Java applications, you can create robust, maintainable deployment pipelines that scale with your application complexity while providing excellent developer experience and operational reliability.