Skip to main content

DevOps Integration

1. Overview

Unlike standard Java projects, ElastiCORE projects require a code generation step before the build. The elcore Gradle task, which generates Java source code from DSL files (.ecore), must run before compilation.

The standard build pipeline is structured in the following order:

Code Checkout → DSL Validation → Code Generation (elcore) → Compile → Test → Package → Deploy
info

The elcore task is not automatically wired as a dependency of Gradle's compileJava task. You must explicitly run elcore first in your CI/CD pipeline or Dockerfile.


2. Gradle Build Commands

Common Gradle commands used in both local development and CI environments.

# Generate Java code from DSL only
./gradlew elcore

# Generate code, then run full build
./gradlew elcore build

# Run code generation, build, and tests
./gradlew elcore build test

# Package an executable JAR for production deployment
./gradlew elcore bootJar

# Clean and full rebuild (use when cache issues occur)
./gradlew clean elcore build
Disable the Daemon

In CI environments, the Gradle daemon is unnecessary. Adding the --no-daemon flag reduces memory usage.

./gradlew --no-daemon elcore bootJar

3. GitHub Actions

Create a .github/workflows/build.yml file at the project root to configure the GitHub Actions pipeline.

.github/workflows/build.yml
name: CI/CD Pipeline

on:
push:
branches:
- main
- develop
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout source code
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Generate code with ElastiCORE
run: ./gradlew --no-daemon elcore

- name: Build project
run: ./gradlew --no-daemon build -x test

- name: Run tests
run: ./gradlew --no-daemon test

- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: build/reports/tests/

docker:
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'

steps:
- name: Checkout source code
uses: actions/checkout@v4

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/my-app:latest
${{ secrets.DOCKER_USERNAME }}/my-app:${{ github.sha }}
Secrets Required

To integrate with Docker Hub, register DOCKER_USERNAME and DOCKER_PASSWORD in your GitHub repository under Settings → Secrets and variables → Actions.


4. GitLab CI/CD

Create a .gitlab-ci.yml file at the project root. The pipeline consists of four stages: generate, build, test, and deploy.

.gitlab-ci.yml
stages:
- generate
- build
- test
- deploy

variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle"

cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .gradle/
- build/

# Stage 1: DSL → Java code generation
generate:
stage: generate
image: gradle:8.5-jdk17
script:
- gradle elcore
artifacts:
paths:
- build/generated/
expire_in: 1 hour

# Stage 2: Compile and package JAR
build:
stage: build
image: gradle:8.5-jdk17
script:
- gradle build -x test
artifacts:
paths:
- build/libs/*.jar
expire_in: 1 day

# Stage 3: Unit tests and integration tests
test:
stage: test
image: gradle:8.5-jdk17
script:
- gradle test
artifacts:
when: always
reports:
junit: build/test-results/test/**/TEST-*.xml
paths:
- build/reports/tests/
expire_in: 7 days

# Stage 4: Deploy to production (main branch only)
deploy:
stage: deploy
image: docker:24
services:
- docker:24-dind
only:
- main
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest

5. Docker

Dockerfile (Multi-Stage Build)

A multi-stage build minimizes the final image size. The build stage uses elcore to generate code and package the JAR, while the runtime stage uses a lightweight image containing only the JRE.

Dockerfile
# ─── Stage 1: Build ─────────────────────────────────────────────────────────
FROM gradle:8.5-jdk17 AS builder

WORKDIR /app

# Dependency cache optimization: copy Gradle wrapper and build files first
COPY gradlew gradlew
COPY gradle/ gradle/
COPY build.gradle settings.gradle ./

RUN chmod +x gradlew && ./gradlew dependencies --no-daemon || true

# Copy full source
COPY . .

# Generate ElastiCORE code and package JAR
RUN ./gradlew --no-daemon elcore bootJar

# ─── Stage 2: Runtime ────────────────────────────────────────────────────────
FROM eclipse-temurin:17-jre-jammy

WORKDIR /app

# Create a non-root user for security
RUN groupadd --system appgroup && useradd --system --gid appgroup appuser

# Copy only the JAR from the build stage
COPY --from=builder /app/build/libs/*.jar app.jar

# Change file ownership
RUN chown appuser:appgroup app.jar

USER appuser

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]

Building and Running the Image

# Build the image
docker build -t my-elasticore-app:latest .

# Run the container (inject configuration via environment variables)
docker run -d \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_HOST=postgres \
-e DB_PASSWORD=secret \
--name my-app \
my-elasticore-app:latest

6. Docker Compose (Development Environment)

A configuration to run the application alongside PostgreSQL and Redis in a local development environment.

docker-compose.yml
version: '3.8'

services:
app:
build: .
container_name: elasticore-app
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: dev
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: appdb
DB_USERNAME: appuser
DB_PASSWORD: apppassword
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
restart: unless-stopped

postgres:
image: postgres:16-alpine
container_name: elasticore-postgres
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppassword
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network

redis:
image: redis:7-alpine
container_name: elasticore-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
networks:
- app-network

volumes:
postgres-data:
redis-data:

networks:
app-network:
driver: bridge
# Start the development environment (background)
docker compose up -d

# Check logs
docker compose logs -f app

# Stop environment and remove volumes
docker compose down -v
Local Development Tip

You can start only the infrastructure services (excluding the app service) and run the application directly from your IDE. This lets you see code changes reflected immediately.

# Start only PostgreSQL and Redis
docker compose up -d postgres redis

7. Kubernetes

Deployment

k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticore-app
namespace: production
labels:
app: elasticore-app
version: "1.0.0"
spec:
replicas: 3
selector:
matchLabels:
app: elasticore-app
template:
metadata:
labels:
app: elasticore-app
spec:
containers:
- name: app
image: my-registry/elasticore-app:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: db.host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: elasticore-app-svc
namespace: production
spec:
selector:
app: elasticore-app
ports:
- port: 80
targetPort: 8080
type: ClusterIP

ConfigMap (Per-Environment Datasource Configuration)

The datasource settings defined in ElastiCORE's env.yml are separated into per-environment ConfigMaps.

k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
data:
db.host: "postgres-service.production.svc.cluster.local"
db.port: "5432"
db.name: "appdb"
redis.host: "redis-service.production.svc.cluster.local"
redis.port: "6379"
spring.datasource.hikari.maximum-pool-size: "20"

Secret (Sensitive Information)

k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
namespace: production
type: Opaque
stringData:
db-password: "your-secure-password"
db-username: "appuser"
Secret Management

Do not commit Secret resources to Git in plain text. It is strongly recommended to manage secrets securely using Sealed Secrets or External Secrets Operator.

env.yml Per-Environment Separation Strategy

In Kubernetes environments, it is recommended to inject the values from ElastiCORE's env.yml via ConfigMaps and Secrets.

src/main/resources/env.yml (example)
datasource:
main:
driver: postgresql
host: ${DB_HOST}
port: ${DB_PORT:5432}
database: ${DB_NAME}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
pool:
maximumPoolSize: ${HIKARI_MAX_POOL_SIZE:10}

Environment variables are automatically injected from ConfigMaps and Secrets via the env or envFrom fields in the Kubernetes Deployment.


8. Build Considerations

elcore Task Execution Order

Critical Note

The elcore task must always run before compileJava. Whether in a CI/CD pipeline, Dockerfile, or script, ./gradlew elcore must be called before the build.

# Correct order
./gradlew elcore build

# Incorrect order (compiles without generated code → build failure)
./gradlew build

Regeneration Required After DSL Changes

When a .ecore DSL file is modified, the elcore task must be re-run to regenerate the Java code. If the generated code does not match the latest DSL definitions, runtime errors may occur.

# Clean regeneration after DSL changes
./gradlew clean elcore build

Excluding Generated Code from Git

Since generated Java source code can be regenerated at any time from the DSL files, it is recommended to exclude it from the Git repository.

.gitignore
# ElastiCORE generated code
build/generated/
src/main/generated/

# Gradle build artifacts
build/
.gradle/
When Generated Code Should Be Included in Git

Some teams choose to include generated code in Git for code review or audit purposes. In that case, remove the relevant paths from .gitignore and establish a process to review DSL changes alongside generated code changes in pull requests.