Managing Container Dependencies: Using imgpkg for Java Application Bundles

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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper