Article
As Java applications evolve into distributed microservices with complex container dependencies, managing related container images becomes challenging. imgpkg (from the Carvel tool suite) addresses this by providing a way to bundle multiple container images and files into a single, versioned, and relocatable package.
What is imgpkg?
imgpkg is a Kubernetes-native tool that creates immutable bundles of container images and configuration files. Unlike traditional container registries that store individual images, imgpkg bundles allow you to version and distribute collections of images as a single unit.
Key Benefits for Java Teams:
- Atomic Deployments: Ship all related images (app, database migrations, utilities) together
- Air-Gapped Environments: Easily relocate entire application stacks
- Version Consistency: Ensure all components are tested and deployed together
- Reproducible Deployments: Bundle includes exact image digests, not just tags
- Configuration Management: Include configuration files alongside container images
Core Concepts
- Bundle: A collection of images and files referenced by a single digest
- Image Lock File: Records exact image digests for reproducibility
- Relocation: Copying bundles between registries while preserving integrity
- Bundle Repository: Special OCI registry for storing bundles
Installation and Setup
Install imgpkg
Mac with Homebrew:
brew tap vmware-tanzu/carvel brew install imgpkg
Linux:
wget -O- https://carvel.dev/install.sh | bash # or curl -L https://carvel.dev/install.sh | bash
Docker:
docker run -it --rm -v $(pwd):/workspace -w /workspace ghcr.io/carvel-dev/imgpkg:latest
Authenticate to Registries
# Docker Hub imgpkg auth login --username myuser --password mystuff index.docker.io # GitHub Container Registry export GITHUB_TOKEN=ghp_xxx imgpkg auth login --username myuser --password $GITHUB_TOKEN ghcr.io # Google Container Registry imgpkg auth login -u oauth2accesstoken -p "$(gcloud auth print-access-token)" gcr.io
Java Application Bundle Example
Project Structure
my-java-app/ ├── k8s/ │ ├── deployment.yaml │ ├── service.yaml │ └── config/ │ ├── application.properties │ └── logback-spring.xml ├── migrations/ │ └── db-changelog.xml ├── .imgpkg/ │ └── images.yml └── bundle.yml
1. Create Images Configuration
.imgpkg/images.yml:
--- apiVersion: imgpkg.carvel.dev/v1alpha1 kind: ImagesLock images: - image: index.docker.io/mycompany/my-java-app:1.2.3 - image: index.docker.io/mycompany/db-migrator:2.1.0 - image: index.docker.io/bitnami/postgresql:15.3.0 - image: index.docker.io/redis:7.2.0-alpine - image: index.docker.io/mycompany/metrics-sidecar:0.5.1
2. Create Bundle Configuration
bundle.yml:
--- apiVersion: imgpkg.carvel.dev/v1alpha1 kind: Bundle metadata: name: my-java-app-bundle version: 1.2.3 spec: images: - image: index.docker.io/mycompany/my-java-app@sha256:abc123... - image: index.docker.io/mycompany/db-migrator@sha256:def456... - image: index.docker.io/bitnami/postgresql@sha256:ghi789... - image: index.docker.io/redis@sha256:jkl012... - image: index.docker.io/mycompany/metrics-sidecar@sha256:mno345...
3. Push the Bundle
# Create and push bundle imgpkg push \ --bundle ghcr.io/mycompany/my-java-app-bundle:1.2.3 \ --file . \ --lock-output bundle.lock.yml
Output (bundle.lock.yml):
--- apiVersion: imgpkg.carvel.dev/v1alpha1 kind: BundleLock bundle: image: ghcr.io/mycompany/my-java-app-bundle@sha256:xyz789... tag: 1.2.3
Including Configuration Files
Bundle with Application Configuration
# Directory structure mkdir -p config cat > config/application-prod.properties << EOF spring.datasource.url=jdbc:postgresql://postgresql:5432/myapp spring.redis.host=redis server.port=8080 management.endpoints.web.exposure.include=health,metrics,info EOF cat > config/logback-spring.xml << EOF <configuration> <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="net.logstash.logback.encoder.LogstashEncoder"/> </appender> <root level="INFO"> <appender-ref ref="JSON"/> </root> </configuration> EOF # Push bundle with files imgpkg push \ --bundle ghcr.io/mycompany/my-java-app-bundle:1.2.3 \ --file . \ --file config/ \ --file migrations/
Verify Bundle Contents
# List bundle contents imgpkg describe \ --bundle ghcr.io/mycompany/my-java-app-bundle:1.2.3 # Pull bundle to inspect locally imgpkg pull \ --bundle ghcr.io/mycompany/my-java-app-bundle:1.2.3 \ --output /tmp/my-bundle
Java-Specific Bundle Strategies
1. Multi-Module Maven Project Bundle
For complex Java applications with multiple services:
.imgpkg/images.yml:
--- apiVersion: imgpkg.carvel.dev/v1alpha1 kind: ImagesLock images: - image: index.docker.io/mycompany/user-service:2.1.0 - image: index.docker.io/mycompany/order-service:1.5.2 - image: index.docker.io/mycompany/payment-service:3.0.1 - image: index.docker.io/mycompany/api-gateway:1.8.3 - image: index.docker.io/bitnami/kafka:3.4.0 - image: index.docker.io/postgres:15.3
2. Spring Boot Profile Bundles
Create environment-specific bundles:
bundle-dev.yml:
--- apiVersion: imgpkg.carvel.dev/v1alpha1 kind: Bundle metadata: name: my-java-app-dev-bundle version: 1.2.3-dev spec: images: - image: index.docker.io/mycompany/my-java-app:1.2.3 - image: index.docker.io/bitnami/postgresql:15.3.0
bundle-prod.yml:
--- apiVersion: imgpkg.carvel.dev/v1alpha1 kind: Bundle metadata: name: my-java-app-prod-bundle version: 1.2.3 spec: images: - image: index.docker.io/mycompany/my-java-app:1.2.3 - image: index.docker.io/mycompany/my-java-app-utils:1.0.0 - image: index.docker.io/bitnami/postgresql:15.3.0 - image: index.docker.io/redis:7.2.0-alpine
Relocation for Air-Gapped Environments
Copy Bundle Between Registries
# Copy from public to private registry imgpkg copy \ --bundle ghcr.io/mycompany/my-java-app-bundle:1.2.3 \ --to-registry my-private-registry.company.com # Copy with repository mapping imgpkg copy \ --bundle ghcr.io/mycompany/my-java-app-bundle:1.2.3 \ --to-repo my-private-registry.company.com/java-apps/my-java-app-bundle \ --lock-output relocated-bundle.lock.yml
Air-Gapped Deployment
# In air-gapped environment, pull from private registry imgpkg pull \ --bundle my-private-registry.company.com/java-apps/my-java-app-bundle:1.2.3 \ --output /opt/my-app # Use the pulled files and images kubectl apply -f /opt/my-app/k8s/
Integration with Java CI/CD Pipelines
GitHub Actions Workflow
name: Build and Bundle Java Application
on:
push:
tags: ['v*']
jobs:
bundle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Build with Maven
run: mvn -B package -DskipTests
- name: Build Docker images
run: |
docker build -t ghcr.io/mycompany/my-java-app:${{ github.ref_name }} .
docker build -t ghcr.io/mycompany/db-migrator:${{ github.ref_name }} -f Dockerfile.migrator .
- name: Install imgpkg
run: |
wget -O- https://carvel.dev/install.sh | bash
- name: Login to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | imgpkg auth login --username mycompany --password-stdin ghcr.io
- name: Create and push bundle
run: |
# Create images lock file
cat > .imgpkg/images.yml << EOF
apiVersion: imgpkg.carvel.dev/v1alpha1
kind: ImagesLock
images:
- image: ghcr.io/mycompany/my-java-app:${{ github.ref_name }}
- image: ghcr.io/mycompany/db-migrator:${{ github.ref_name }}
- image: index.docker.io/bitnami/postgresql:15.3.0
EOF
# Push bundle
imgpkg push \
--bundle ghcr.io/mycompany/my-java-app-bundle:${{ github.ref_name }} \
--file . \
--lock-output bundle.lock.yml
Jenkins Pipeline
pipeline {
agent any
environment {
IMGPKG_BUNDLE = "ghcr.io/mycompany/my-java-app-bundle:${env.BUILD_ID}"
}
stages {
stage('Build') {
steps {
sh 'mvn -B clean package'
sh 'docker build -t my-java-app:latest .'
}
}
stage('Create Bundle') {
steps {
sh '''
# Create bundle configuration
mkdir -p .imgpkg
cat > .imgpkg/images.yml << EOF
apiVersion: imgpkg.carvel.dev/v1alpha1
kind: ImagesLock
images:
- image: ghcr.io/mycompany/my-java-app:${BUILD_ID}
- image: index.docker.io/redis:7.2.0-alpine
EOF
# Push bundle
imgpkg push --bundle ${IMGPKG_BUNDLE} --file .
'''
}
}
}
}
Kubernetes Deployment with Bundles
Using kapp-controller
apiVersion: kappctrl.k14s.io/v1alpha1
kind: App
metadata:
name: my-java-app
namespace: production
spec:
serviceAccountName: kapp-service-account
fetch:
- imgpkgBundle:
image: ghcr.io/mycompany/my-java-app-bundle:1.2.3
template:
- ytt: {}
- kbld:
paths:
- "-"
- ".imgpkg/images.yml"
deploy:
- kapp: {}
Manual Deployment with Bundle
# Pull bundle imgpkg pull \ --bundle ghcr.io/mycompany/my-java-app-bundle:1.2.3 \ --output /tmp/app-bundle # Apply Kubernetes manifests kubectl apply -f /tmp/app-bundle/k8s/ # Or use kustomize kubectl apply -k /tmp/app-bundle/k8s/overlays/production/
Best Practices for Java Applications
1. Versioning Strategy
# Use semantic versioning for bundles imgpkg push --bundle ghcr.io/mycompany/my-app-bundle:1.2.3 # Also tag with Git SHA for traceability imgpkg push --bundle ghcr.io/mycompany/my-app-bundle:abc1234
2. Security Scanning
# Scan images before bundling trivy image my-java-app:1.2.3 cosign verify my-java-app:1.2.3 # Sign bundles cosign sign ghcr.io/mycompany/my-java-app-bundle:1.2.3
3. Bundle Organization
# Organize by application and environment ghcr.io/mycompany/apps/user-service/production:1.2.3 ghcr.io/mycompany/apps/user-service/staging:1.2.3 ghcr.io/mycompany/platform/database-migrations:2.1.0
4. Bundle Size Optimization
# Only include necessary images images: - image: my-java-app:1.2.3 # Main application - image: postgres:15.3 # Database - image: redis:7.2-alpine # Cache # Exclude build-time only images
Conclusion
imgpkg provides Java teams with a powerful solution for managing complex container dependencies in microservices architectures. By bundling related images and configuration files together, you ensure:
- Consistency: All components are versioned and deployed together
- Reproducibility: Exact image digests prevent "works on my machine" issues
- Portability: Easy relocation between registries for air-gapped deployments
- Atomicity: Related images are updated as a single unit
For Java applications with multiple services, database migrations, and complex dependencies, imgpkg bridges the gap between traditional package management and container orchestration, providing a robust foundation for modern deployment workflows.