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.