Introduction
Helmfile is a declarative specification for deploying Helm charts across multiple environments, bringing GitOps practices to Helm deployments. For Java applications running in Kubernetes, Helmfile provides a powerful way to manage environment-specific configurations, dependencies, and release strategies. This guide explores how to structure Helmfile configurations for complex Java microservices deployments.
Article: Streamlining Multi-Environment Java Deployments with Helmfile
Helmfile enhances Helm by providing a way to compose multiple charts, manage environment-specific values, and synchronize releases across namespaces. When combined with Java applications, it creates a robust deployment pipeline that can handle development, staging, and production environments consistently.
1. Project Structure for Multi-Environment Java Apps
java-helmfile-project/ ├── src/ │ └── main/ │ └── java/ │ └── com/myapp/ ├── helm/ │ ├── my-java-app/ │ │ ├── Chart.yaml │ │ ├── values.yaml │ │ ├── templates/ │ │ │ ├── deployment.yaml │ │ │ ├── service.yaml │ │ │ ├── configmap.yaml │ │ │ └── ingress.yaml │ │ └── charts/ │ ├── dependencies/ │ │ ├── redis/ │ │ └── postgresql/ │ └── environments/ │ ├── helmfile.dev.yaml │ ├── helmfile.staging.yaml │ ├── helmfile.prod.yaml │ └── values/ │ ├── dev/ │ │ ├── my-java-app.yaml │ │ ├── redis.yaml │ │ └── postgresql.yaml │ ├── staging/ │ └── production/ ├── helmfile.yaml └── scripts/ └── deploy.sh
2. Java Application Helm Chart
Chart.yaml:
apiVersion: v2 name: order-service description: A Spring Boot Order Service type: application version: 1.0.0 appVersion: "1.0.0" dependencies: - name: redis version: "17.0.0" repository: "https://charts.bitnami.com/bitnami" condition: redis.enabled - name: postgresql version: "12.0.0" repository: "https://charts.bitnami.com/bitnami" condition: postgresql.enabled
values.yaml (Default):
# Default values for Java application image: repository: my-registry/order-service tag: latest pullPolicy: IfNotPresent replicaCount: 2 javaApp: name: order-service port: 8080 serviceType: ClusterIP # JVM Configuration jvm: memory: max: "512m" min: "256m" opts: "-XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom" profiling: false # Spring Boot Actuator actuator: enabled: true path: "/actuator" # Environment Configuration env: SPRING_PROFILES_ACTIVE: "kubernetes" LOGGING_LEVEL_COM_MYAPP: "INFO" # Resource limits resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "768Mi" cpu: "500m" # Probes livenessProbe: path: /actuator/health/liveness initialDelaySeconds: 60 periodSeconds: 10 readinessProbe: path: /actuator/health/readiness initialDelaySeconds: 30 periodSeconds: 5 # Dependencies redis: enabled: true architecture: standalone postgresql: enabled: true auth: database: "order_service" username: "order_user"
templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Chart.Name }}
labels:
app: {{ .Chart.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
version: {{ .Values.image.tag }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Chart.Name }}
template:
metadata:
labels:
app: {{ .Chart.Name }}
version: {{ .Values.image.tag }}
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.javaApp.port }}
protocol: TCP
env:
- name: JAVA_OPTS
value: "-Xmx{{ .Values.javaApp.jvm.memory.max }} -Xms{{ .Values.javaApp.jvm.memory.min }} {{ .Values.javaApp.jvm.opts }}"
{{- if .Values.javaApp.jvm.profiling }}
- name: JAVA_TOOL_OPTIONS
value: "-agentpath:/opt/java-async-profiler/build/libasyncProfiler.so=start,event=cpu,file=/tmp/profile.html"
{{- end }}
- name: SPRING_PROFILES_ACTIVE
value: {{ .Values.javaApp.env.SPRING_PROFILES_ACTIVE | quote }}
- name: LOGGING_LEVEL_COM_MYAPP
value: {{ .Values.javaApp.env.LOGGING_LEVEL_COM_MYAPP | quote }}
- name: SPRING_DATASOURCE_URL
value: "jdbc:postgresql://{{ .Chart.Name }}-postgresql:5432/{{ .Values.postgresql.auth.database }}"
- name: SPRING_REDIS_HOST
value: "{{ .Chart.Name }}-redis-master"
{{- include "java-app.envVars" . }}
resources:
{{- toYaml .Values.javaApp.resources | nindent 12 }}
livenessProbe:
httpGet:
path: {{ .Values.javaApp.livenessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.javaApp.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.javaApp.livenessProbe.periodSeconds }}
readinessProbe:
httpGet:
path: {{ .Values.javaApp.readinessProbe.path }}
port: http
initialDelaySeconds: {{ .Values.javaApp.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.javaApp.readinessProbe.periodSeconds }}
{{- if .Values.javaApp.volumeMounts }}
volumeMounts:
{{- toYaml .Values.javaApp.volumeMounts | nindent 12 }}
{{- end }}
{{- if .Values.javaApp.volumes }}
volumes:
{{- toYaml .Values.javaApp.volumes | nindent 8 }}
{{- end }}
3. Helmfile Configuration
Root helmfile.yaml:
# Base configuration
apiVersion: v1
environments:
default:
values:
- environments/values/dev/my-java-app.yaml
dev:
values:
- environments/values/dev/my-java-app.yaml
staging:
values:
- environments/values/staging/my-java-app.yaml
production:
values:
- environments/values/production/my-java-app.yaml
# Common releases
releases:
# Java Application
- name: order-service
namespace: "{{ .Environment.Name }}-services"
chart: ./helm/my-java-app
version: 1.0.0
values:
- "{{ .Environment.Name }}/order-service.yaml.gotmpl"
wait: true
timeout: 600
labels:
app: order-service
team: backend
# Monitoring Stack
- name: monitoring
namespace: "{{ .Environment.Name }}-monitoring"
chart: prometheus-community/kube-prometheus-stack
version: 46.0.0
values:
- "{{ .Environment.Name }}/monitoring.yaml.gotmpl"
labels:
app: monitoring
team: platform
# Templates for value files
templates:
order-service.yaml.gotmpl: |
image:
tag: "{{ .Values.imageTag | default "latest" }}"
replicaCount: {{ .Values.replicaCount | default 2 }}
javaApp:
jvm:
memory:
max: "{{ .Values.jvmMemoryMax | default "512m" }}"
min: "{{ .Values.jvmMemoryMin | default "256m" }}"
resources:
requests:
memory: "{{ .Values.resourceRequestsMemory | default "512Mi" }}"
cpu: "{{ .Values.resourceRequestsCpu | default "250m" }}"
limits:
memory: "{{ .Values.resourceLimitsMemory | default "768Mi" }}"
cpu: "{{ .Values.resourceLimitsCpu | default "500m" }}"
4. Environment-Specific Values
environments/values/dev/order-service.yaml:
# Development Environment replicaCount: 1 imageTag: "latest" jvmMemoryMax: "256m" jvmMemoryMin: "128m" resourceRequestsMemory: "256Mi" resourceRequestsCpu: "100m" resourceLimitsMemory: "512Mi" resourceLimitsCpu: "250m" javaApp: env: SPRING_PROFILES_ACTIVE: "dev,kubernetes" LOGGING_LEVEL_COM_MYAPP: "DEBUG" jvm: profiling: true opts: "-XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005" redis: enabled: true architecture: standalone postgresql: enabled: true auth: password: "dev_password"
environments/values/staging/order-service.yaml:
# Staging Environment
replicaCount: 2
imageTag: "{{ requiredEnv "STAGING_IMAGE_TAG" }}"
jvmMemoryMax: "512m"
jvmMemoryMin: "256m"
resourceRequestsMemory: "512Mi"
resourceRequestsCpu: "250m"
resourceLimitsMemory: "1024Mi"
resourceLimitsCpu: "500m"
javaApp:
env:
SPRING_PROFILES_ACTIVE: "staging,kubernetes"
LOGGING_LEVEL_COM_MYAPP: "INFO"
jvm:
profiling: false
opts: "-XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"
redis:
enabled: true
architecture: standalone
auth:
password: "{{ requiredEnv "REDIS_PASSWORD" }}"
postgresql:
enabled: true
auth:
password: "{{ requiredEnv "POSTGRES_PASSWORD" }}"
environments/values/production/order-service.yaml:
# Production Environment
replicaCount: 3
imageTag: "{{ requiredEnv "PRODUCTION_IMAGE_TAG" }}"
jvmMemoryMax: "1024m"
jvmMemoryMin: "512m"
resourceRequestsMemory: "1024Mi"
resourceRequestsCpu: "500m"
resourceLimitsMemory: "2048Mi"
resourceLimitsCpu: "1000m"
javaApp:
env:
SPRING_PROFILES_ACTIVE: "production,kubernetes"
LOGGING_LEVEL_COM_MYAPP: "WARN"
jvm:
profiling: false
opts: "-XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom"
# Production-specific configuration
ingress:
enabled: true
className: "nginx"
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
hosts:
- host: orders.mycompany.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: order-service-tls
hosts:
- orders.mycompany.com
redis:
enabled: true
architecture: replication
auth:
password: "{{ requiredEnv "REDIS_PASSWORD" }}"
postgresql:
enabled: true
architecture: replication
auth:
password: "{{ requiredEnv "POSTGRES_PASSWORD" }}"
5. Advanced Helmfile Features
Secrets Management with SOPS:
# helmfile.secrets.yaml
releases:
- name: order-service
namespace: "{{ .Environment.Name }}-services"
chart: ./helm/my-java-app
values:
- "{{ .Environment.Name }}/order-service.yaml.gotmpl"
- "secrets/{{ .Environment.Name }}/encrypted-secrets.yaml"
# Encrypted secrets file (secrets/production/encrypted-secrets.yaml)
javaApp:
env:
SPRING_DATASOURCE_PASSWORD: "ENC[AES256_GCM,data:...]"
SPRING_REDIS_PASSWORD: "ENC[AES256_GCM,data:...]"
CUSTOMER_SERVICE_API_KEY: "ENC[AES256_GCM,data:...]"
Dependent Releases:
releases:
- name: postgresql
namespace: "{{ .Environment.Name }}-data"
chart: bitnami/postgresql
version: 12.0.0
values:
- "{{ .Environment.Name }}/postgresql.yaml.gotmpl"
labels:
app: database
team: data
- name: order-service
namespace: "{{ .Environment.Name }}-services"
chart: ./helm/my-java-app
version: 1.0.0
needs:
- "{{ .Environment.Name }}-data/postgresql"
values:
- "{{ .Environment.Name }}/order-service.yaml.gotmpl"
Hooks for Pre/Post Deployment:
releases:
- name: order-service
namespace: "{{ .Environment.Name }}-services"
chart: ./helm/my-java-app
values:
- "{{ .Environment.Name }}/order-service.yaml.gotmpl"
# Pre-install hooks
hooks:
- events: ["presync"]
command: "kubectl"
args: ["-n", "{{ .Environment.Name }}-services", "wait", "--for=condition=ready", "pod", "-l", "app=postgresql", "--timeout=300s"]
- events: ["postsync"]
command: "bash"
args: ["-c", "echo 'Deployment completed for {{ .Release.Name }} in {{ .Environment.Name }}'"]
# Run database migrations for Java app
- events: ["presync"]
command: "kubectl"
args:
- "run"
- "{{ .Release.Name }}-migrations"
- "--image"
- "my-registry/order-service:{{ .Values.imageTag }}"
- "--env=SPRING_PROFILES_ACTIVE={{ .Values.javaApp.env.SPRING_PROFILES_ACTIVE }}"
- "--command"
- "--"
- "java"
- "-jar"
- "/app.jar"
- "--spring.flyway.locations=classpath:db/migration"
- "--spring.flyway.baseline-on-migrate=true"
6. Java Application Deployment Scripts
deploy.sh:
#!/bin/bash
set -e
ENVIRONMENT="${1:-dev}"
ACTION="${2:-apply}"
IMAGE_TAG="${3:-latest}"
# Validate environment
if [[ ! "$ENVIRONMENT" =~ ^(dev|staging|production)$ ]]; then
echo "Error: Environment must be one of: dev, staging, production"
exit 1
fi
# Export environment variables
export ENVIRONMENT=$ENVIRONMENT
# Set image tag based on environment
if [ "$ENVIRONMENT" == "production" ]; then
export PRODUCTION_IMAGE_TAG=$IMAGE_TAG
elif [ "$ENVIRONMENT" == "staging" ]; then
export STAGING_IMAGE_TAG=$IMAGE_TAG
fi
echo "Deploying to $ENVIRONMENT environment with image tag: $IMAGE_TAG"
# Select appropriate helmfile
HELMFILE="helmfile.yaml"
case $ACTION in
"apply")
echo "Applying Helmfile..."
helmfile --environment $ENVIRONMENT -f $HELMFILE apply
;;
"diff")
echo "Showing diff..."
helmfile --environment $ENVIRONMENT -f $HELMFILE diff
;;
"sync")
echo "Syncing releases..."
helmfile --environment $ENVIRONMENT -f $HELMFILE sync
;;
"destroy")
read -p "Are you sure you want to destroy $ENVIRONMENT environment? (y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
helmfile --environment $ENVIRONMENT -f $HELMFILE destroy
fi
;;
*)
echo "Usage: $0 {dev|staging|production} {apply|diff|sync|destroy} [image-tag]"
exit 1
;;
esac
GitHub Actions Workflow:
name: Deploy Java Application
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Helm
uses: azure/setup-helm@v3
with:
version: '3.12.0'
- name: Set up Helmfile
run: |
curl -fsSL -o helmfile_linux_amd64 https://github.com/helmfile/helmfile/releases/download/v0.156.0/helmfile_linux_amd64
chmod +x helmfile_linux_amd64
sudo mv helmfile_linux_amd64 /usr/local/bin/helmfile
- name: Lint Helm charts
run: |
helm lint helm/my-java-app
helmfile --environment dev lint
deploy-dev:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/develop'
environment: dev
steps:
- uses: actions/checkout@v3
- name: Set up Kubernetes
uses: azure/setup-kubectl@v3
with:
version: 'v1.27.0'
- name: Set up Helmfile
run: |
curl -fsSL -o helmfile_linux_amd64 https://github.com/helmfile/helmfile/releases/download/v0.156.0/helmfile_linux_amd64
chmod +x helmfile_linux_amd64
sudo mv helmfile_linux_amd64 /usr/local/bin/helmfile
- name: Configure Kubernetes
run: |
echo "${{ secrets.DEV_KUBECONFIG }}" > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy to Dev
run: |
./scripts/deploy.sh dev apply ${{ github.sha }}
deploy-production:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v3
- name: Set up Kubernetes
uses: azure/setup-kubectl@v3
with:
version: 'v1.27.0'
- name: Set up Helmfile
run: |
curl -fsSL -o helmfile_linux_amd64 https://github.com/helmfile/helmfile/releases/download/v0.156.0/helmfile_linux_amd64
chmod +x helmfile_linux_amd64
sudo mv helmfile_linux_amd64 /usr/local/bin/helmfile
- name: Configure Kubernetes
run: |
echo "${{ secrets.PRODUCTION_KUBECONFIG }}" > kubeconfig
export KUBECONFIG=kubeconfig
- name: Deploy to Production
run: |
./scripts/deploy.sh production apply ${{ github.sha }}
7. Java-Specific Helmfile Enhancements
Health Check and Readiness Verification:
releases:
- name: order-service
namespace: "{{ .Environment.Name }}-services"
chart: ./helm/my-java-app
values:
- "{{ .Environment.Name }}/order-service.yaml.gotmpl"
hooks:
- events: ["postsync"]
command: "bash"
args:
- "-c"
- |
echo "Waiting for Java application to be ready..."
kubectl wait --for=condition=ready pod \
-l app=order-service \
-n {{ .Environment.Name }}-services \
--timeout=300s
echo "Testing application health endpoint..."
SERVICE_URL=$(kubectl get svc order-service -n {{ .Environment.Name }}-services -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -f http://$SERVICE_URL:8080/actuator/health || exit 1
echo "Application is healthy!"
Database Migration Hook:
hooks:
- events: ["presync"]
command: "kubectl"
args:
- "run"
- "{{ .Release.Name }}-flyway-migration"
- "-n"
- "{{ .Environment.Name }}-services"
- "--image"
- "my-registry/order-service:{{ .Values.imageTag }}"
- "--restart=Never"
- "--rm=true"
- "--attach=true"
- "--env=SPRING_PROFILES_ACTIVE={{ .Values.javaApp.env.SPRING_PROFILES_ACTIVE }}"
- "--env=SPRING_DATASOURCE_URL=jdbc:postgresql://order-service-postgresql:5432/order_service"
- "--env=SPRING_DATASOURCE_USERNAME={{ .Values.postgresql.auth.username }}"
- "--env=SPRING_DATASOURCE_PASSWORD={{ .Values.postgresql.auth.password }}"
- "--command"
- "--"
- "java"
- "-jar"
- "/app.jar"
- "--spring.flyway.locations=classpath:db/migration"
- "--spring.flyway.baseline-on-migrate=true"
- "--spring.flyway.validate-on-migrate=true"
8. Best Practices for Java Applications
1. Environment-Specific JVM Configuration:
# Development jvmOpts: "-XX:+UseG1GC -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005" # Production jvmOpts: "-XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
2. Resource Management:
resources:
requests:
memory: "{{ .Values.jvmMemoryMin | default "256m" | replace "m" "Mi" | replace "g" "Gi" }}"
cpu: "{{ .Values.resourceRequestsCpu | default "250m" }}"
limits:
memory: "{{ .Values.jvmMemoryMax | default "512m" | replace "m" "Mi" | replace "g" "Gi" }}"
cpu: "{{ .Values.resourceLimitsCpu | default "500m" }}"
3. Graceful Shutdown Configuration:
javaApp: lifecycle: preStop: exec: command: ["sh", "-c", "sleep 30"] # Give time for graceful shutdown
Benefits of Helmfile for Java Applications
- Consistent Environments - Identical configuration across dev, staging, and production
- GitOps Workflow - Version-controlled infrastructure changes
- Dependency Management - Coordinated deployment of Java apps and their dependencies
- Secret Management - Secure handling of database credentials and API keys
- Rollback Capabilities - Easy rollback to previous versions
- Automated Testing - Integration with CI/CD pipelines
Conclusion
Helmfile provides a powerful, declarative approach to managing multi-environment Kubernetes deployments for Java applications. By combining Helm's packaging capabilities with Helmfile's environment management and lifecycle hooks, teams can create robust, maintainable deployment pipelines.
The key to success with Helmfile is establishing clear patterns for environment separation, secret management, and deployment verification—especially important for stateful Java applications that require database migrations and health checks.
With the approach outlined in this guide, Java development teams can achieve reliable, repeatable deployments across all environments while maintaining the flexibility to customize configurations for each stage of the development lifecycle.
Call to Action: Start by converting a single Java application's Helm chart to use Helmfile. Create environment-specific value files and implement basic hooks for health checks. Gradually expand to include dependencies and more sophisticated deployment patterns as your team becomes comfortable with the workflow.
Java Logistics, Shipping Integration & Enterprise Inventory Automation (Tracking, ERP, RFID & Billing Systems)
https://macronepal.com/blog/aftership-tracking-in-java-enterprise-package-visibility/
Explains how to integrate AfterShip tracking services into Java applications to provide real-time shipment visibility, delivery status updates, and centralized tracking across multiple courier services.
https://macronepal.com/blog/shipping-integration-using-fedex-api-with-java-for-logistics-automation/
Explains how to integrate the FedEx API into Java systems to automate shipping tasks such as creating shipments, calculating delivery costs, generating shipping labels, and tracking packages.
https://macronepal.com/blog/shipping-and-logistics-integrating-ups-apis-with-java-applications/
Explains UPS API integration in Java to enable automated shipping operations including rate calculation, shipment scheduling, tracking, and delivery confirmation management.
https://macronepal.com/blog/generating-and-reading-qr-codes-for-products-in-java/
Explains how Java applications generate and read QR codes for product identification, tracking, and authentication, supporting faster inventory handling and product verification processes.
https://macronepal.com/blog/designing-a-robust-pick-and-pack-workflow-in-java/
Explains how to design an efficient pick-and-pack workflow in Java warehouse systems, covering order processing, item selection, packaging steps, and logistics preparation to improve fulfillment efficiency.
https://macronepal.com/blog/rfid-inventory-management-system-in-java-a-complete-guide/
Explains how RFID technology integrates with Java applications to automate inventory tracking, reduce manual errors, and enable real-time stock monitoring in warehouses and retail environments.
https://macronepal.com/blog/erp-integration-with-odoo-in-java/
Explains how Java applications connect with Odoo ERP systems to synchronize inventory, orders, customer records, and financial data across enterprise systems.
https://macronepal.com/blog/automated-invoice-generation-creating-professional-excel-invoices-with-apache-poi-in-java/
Explains how to automatically generate professional Excel invoices in Java using Apache POI, enabling structured billing documents and automated financial record creation.
https://macronepal.com/blog/enterprise-financial-integration-using-quickbooks-api-in-java-applications/
Explains QuickBooks API integration in Java to automate financial workflows such as invoice management, payment tracking, accounting synchronization, and financial reporting.