Skip to content

Commit 70f3f57

Browse files
author
Yuriy Bezsonov
committed
feat(apps): optimize Java 25 Docker images with improved layer caching and user permissions
1 parent 63d202b commit 70f3f57

File tree

4 files changed

+155
-75
lines changed

4 files changed

+155
-75
lines changed

apps/dockerfiles-java25/Dockerfile.06-cds

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ RUN java -XX:ArchiveClassesAtExit=/app.jsa \
2020

2121
FROM public.ecr.aws/docker/library/amazoncorretto:25-al2023
2222

23-
RUN yum install -y shadow-utils
24-
25-
COPY --from=builder /store-spring.jar /store-spring.jar
26-
COPY --from=builder /app.jsa /app.jsa
27-
28-
RUN groupadd --system spring -g 1000 && \
23+
RUN yum install -y shadow-utils && \
24+
groupadd --system spring -g 1000 && \
2925
adduser spring -u 1000 -g 1000
3026

27+
COPY --from=builder --chown=1000:1000 /store-spring.jar /store-spring.jar
28+
COPY --from=builder --chown=1000:1000 /app.jsa /app.jsa
29+
3130
USER 1000:1000
3231
EXPOSE 8080
3332

apps/dockerfiles-java25/Dockerfile.07-aot

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,14 @@ RUN set -e; \
4747

4848
FROM public.ecr.aws/docker/library/amazoncorretto:25-al2023
4949

50-
RUN yum install -y shadow-utils
51-
52-
COPY --from=trainer /opt/app/training/classes.jar /opt/app/training/classes.jar
53-
COPY --from=trainer /opt/app/lib/ /opt/app/lib/
54-
COPY --from=trainer /opt/app/app.aot /opt/app/app.aot
55-
COPY --from=trainer /opt/app/lib-cp.txt /opt/app/lib-cp.txt
56-
57-
RUN groupadd --system spring -g 1000
58-
RUN adduser spring -u 1000 -g 1000
50+
RUN yum install -y shadow-utils && \
51+
groupadd --system spring -g 1000 && \
52+
adduser spring -u 1000 -g 1000
53+
54+
COPY --from=trainer --chown=1000:1000 /opt/app/training/classes.jar /opt/app/training/classes.jar
55+
COPY --from=trainer --chown=1000:1000 /opt/app/lib/ /opt/app/lib/
56+
COPY --from=trainer --chown=1000:1000 /opt/app/app.aot /opt/app/app.aot
57+
COPY --from=trainer --chown=1000:1000 /opt/app/lib-cp.txt /opt/app/lib-cp.txt
5958

6059
USER 1000:1000
6160
EXPOSE 8080

infra/scripts/deploy/test-optimizations.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,17 @@ For each method:
7676
└─────────────────────────────────────────────────────────────────┘
7777
```
7878

79-
## Output Files (deploy mode only)
79+
## Output Files (deploy mode)
80+
81+
All output goes to `/tmp/test-optimizations/` (or `${SCRIPT_DIR}/.test-optimizations/` if /tmp not writable):
8082

8183
| File | Description |
8284
|------|-------------|
83-
| `test-optimizations-queue.txt` | Build→Deploy queue (status\|tag\|size\|build_time) |
84-
| `test-optimizations-results.txt` | Final results table |
85-
| `.watcher.pid` | Background watcher PID (temporary) |
85+
| `queue.txt` | Build→Deploy queue (status\|tag\|size_local\|size_ecr\|build_time\|error) |
86+
| `results.txt` | Final results table |
87+
| `watcher.pid` | Background watcher PID (temporary) |
88+
| `<tag>-build.txt` | Build and push logs per image |
89+
| `<tag>-deploy.txt` | Deploy logs per image |
8690

8791
## Results Format
8892

@@ -97,11 +101,11 @@ Method | Build | Size Local | Time
97101

98102
### Deploy mode results file
99103
```
100-
Method | Size Local | Size ECR | Build Time | Startup Time | Restart Time
101-
---------------|------------|----------|------------|---------------|-------------
102-
02-multi-stage | 598MB | 580MB | 45s | 8.234 seconds | 7.891 seconds
103-
06-cds | 1.34GB | 1.2GB | 2m15s | 2.156 seconds | 2.089 seconds
104-
09-crac | 1.1GB | 1.0GB | 3m20s | 0.087 seconds | 0.072 seconds
104+
Method | Size Local | Size ECR | Build Time | Startup Time
105+
---------------|------------|----------|------------|-------------
106+
02-multi-stage | 598MB | 580MB | 45s | 8.234 seconds
107+
06-cds | 1.34GB | 1.2GB | 2m15s | 2.156 seconds
108+
09-crac | 1.1GB | 1.0GB | 3m20s | 0.087 seconds
105109
```
106110

107111
## Environment

infra/scripts/deploy/test-optimizations.sh

Lines changed: 129 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,48 @@ APP_DIR="${REPO_ROOT}/apps/unicorn-store-spring-java25"
1717
DOCKERFILES_DIR="${REPO_ROOT}/apps/dockerfiles-java25"
1818
IMAGE_NAME="unicorn-store-spring"
1919

20-
# Output files (only used in deploy mode)
21-
QUEUE_FILE="${SCRIPT_DIR}/test-optimizations-queue.txt"
22-
RESULTS_FILE="${SCRIPT_DIR}/test-optimizations-results.txt"
23-
WATCHER_PID_FILE="${SCRIPT_DIR}/.watcher.pid"
20+
# Output directory (use /tmp if writable, otherwise script dir)
21+
if [[ -w /tmp ]]; then
22+
OUTPUT_DIR="/tmp/test-optimizations"
23+
else
24+
OUTPUT_DIR="${SCRIPT_DIR}/.test-optimizations"
25+
fi
26+
27+
# Cleanup on exit/interrupt
28+
cleanup() {
29+
local exit_code=$?
30+
log_info "Cleaning up..."
31+
32+
# Kill watcher if running
33+
if [[ -f "${WATCHER_PID_FILE}" ]]; then
34+
local pid=$(cat "${WATCHER_PID_FILE}")
35+
kill "$pid" 2>/dev/null || true
36+
rm -f "${WATCHER_PID_FILE}"
37+
fi
38+
39+
# Stop build database if running
40+
docker rm -f build-postgres 2>/dev/null || true
41+
42+
# Restore UnicornPublisher if backup exists
43+
if [[ -f "${APP_DIR}/src/main/java/com/unicorn/store/data/UnicornPublisher.java.orig" ]]; then
44+
mv "${APP_DIR}/src/main/java/com/unicorn/store/data/UnicornPublisher.java.orig" \
45+
"${APP_DIR}/src/main/java/com/unicorn/store/data/UnicornPublisher.java"
46+
fi
47+
48+
# Revert deployment to baseline if in deploy mode (disabled for testing)
49+
# if [[ "$DEPLOY_MODE" == true && -n "$ACCOUNT_ID" ]]; then
50+
# log_info "Reverting deployment to :latest..."
51+
# local ecr_uri="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMAGE_NAME}"
52+
# kubectl set image deployment/unicorn-store-spring \
53+
# unicorn-store-spring="${ecr_uri}:latest" -n unicorn-store-spring 2>/dev/null || true
54+
# fi
55+
56+
exit $exit_code
57+
}
58+
trap cleanup EXIT INT TERM
59+
QUEUE_FILE="${OUTPUT_DIR}/queue.txt"
60+
RESULTS_FILE="${OUTPUT_DIR}/results.txt"
61+
WATCHER_PID_FILE="${OUTPUT_DIR}/watcher.pid"
2462

2563
# Methods in order (tag names)
2664
METHODS=(
@@ -125,6 +163,7 @@ format_time() {
125163
# Build single image
126164
build_image() {
127165
local tag="$1"
166+
local log_file="$2"
128167
local dockerfile="${DOCKERFILES_DIR}/Dockerfile.${tag}"
129168
local build_args=""
130169

@@ -133,13 +172,14 @@ build_image() {
133172
# Special case: jib uses maven
134173
if [[ "$tag" == "03-jib" ]]; then
135174
log_info "Using Maven Jib plugin..."
136-
(cd "${APP_DIR}" && mvn compile jib:dockerBuild -Dimage=${IMAGE_NAME}:${tag} -q)
175+
(cd "${APP_DIR}" && mvn compile jib:dockerBuild -Dimage=${IMAGE_NAME}:${tag}) >> "${log_file}" 2>&1
137176
return $?
138177
fi
139178

140179
# Check Dockerfile exists
141180
if [[ ! -f "${dockerfile}" ]]; then
142181
log_error "Dockerfile not found: ${dockerfile}"
182+
echo "ERROR: Dockerfile not found: ${dockerfile}" >> "${log_file}"
143183
return 1
144184
fi
145185

@@ -156,9 +196,14 @@ build_image() {
156196
build_args="${build_args} --build-arg SPRING_DATASOURCE_PASSWORD=unicorn"
157197
fi
158198

159-
# Build
199+
# Build with --progress=plain for cleaner logs
200+
# Use --no-cache for methods that need fresh training (CDS, AOT, CRaC)
201+
local no_cache=""
202+
if needs_db "$tag"; then
203+
no_cache="--no-cache"
204+
fi
160205
local result=0
161-
docker build ${build_args} -f "${dockerfile}" -t "${IMAGE_NAME}:${tag}" "${APP_DIR}" || result=$?
206+
docker build --progress=plain ${no_cache} ${build_args} -f "${dockerfile}" -t "${IMAGE_NAME}:${tag}" "${APP_DIR}" >> "${log_file}" 2>&1 || result=$?
162207

163208
# Cleanup
164209
if needs_db "$tag"; then
@@ -174,18 +219,31 @@ build_image() {
174219
}
175220

176221
# Push image to ECR and return ECR size
222+
# Returns: "SIZE" on success, "PUSH_FAILED:reason" on failure
177223
push_image() {
178224
local tag="$1"
225+
local log_file="$2"
179226
local ecr_uri="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMAGE_NAME}"
180227

228+
echo "=== PUSH ${tag} ===" >> "${log_file}"
181229
docker tag "${IMAGE_NAME}:${tag}" "${ecr_uri}:${tag}"
182-
docker push "${ecr_uri}:${tag}"
230+
231+
# Capture push output for error reporting and logging
232+
local push_output
233+
if ! push_output=$(docker push "${ecr_uri}:${tag}" 2>&1); then
234+
echo "$push_output" >> "${log_file}"
235+
# Extract last meaningful error line
236+
local error_msg=$(echo "$push_output" | grep -iE '(error|denied|failed|unauthorized)' | tail -1 | cut -c1-50)
237+
echo "PUSH_FAILED:${error_msg:-push failed}"
238+
return 1
239+
fi
240+
echo "$push_output" >> "${log_file}"
183241

184242
# SOCI index for soci method
185243
if [[ "$tag" == "05-soci" ]]; then
186-
log_info "Creating SOCI index..."
187-
sudo soci create "${ecr_uri}:${tag}" 2>/dev/null || true
188-
sudo soci push "${ecr_uri}:${tag}" 2>/dev/null || true
244+
echo "=== SOCI INDEX ===" >> "${log_file}"
245+
sudo soci create "${ecr_uri}:${tag}" >> "${log_file}" 2>&1 || true
246+
sudo soci push "${ecr_uri}:${tag}" >> "${log_file}" 2>&1 || true
189247
fi
190248

191249
# Get ECR image size
@@ -211,8 +269,8 @@ deploy_watcher() {
211269
local ecr_uri="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${IMAGE_NAME}"
212270

213271
# Initialize results file
214-
echo "Method | Size Local | Size ECR | Build Time | Startup Time | Restart Time" > "${RESULTS_FILE}"
215-
echo "-------|------------|----------|------------|--------------|-------------" >> "${RESULTS_FILE}"
272+
echo "Method | Size Local | Size ECR | Build Time | Startup Time" > "${RESULTS_FILE}"
273+
echo "-------|------------|----------|------------|-------------" >> "${RESULTS_FILE}"
216274

217275
local last_line_num=0
218276

@@ -228,41 +286,46 @@ deploy_watcher() {
228286

229287
# Check for END marker
230288
if [[ "$status" == "END" ]]; then
231-
# Revert to baseline
232-
log_info "Reverting to baseline (:latest)..."
233-
kubectl set image deployment/unicorn-store-spring \
234-
unicorn-store-spring="${ecr_uri}:latest" -n unicorn-store-spring 2>/dev/null
235-
kubectl rollout status deployment unicorn-store-spring -n unicorn-store-spring --timeout=180s 2>/dev/null
289+
# Revert to baseline (disabled for testing)
290+
# log_info "Reverting to baseline (:latest)..."
291+
# kubectl set image deployment/unicorn-store-spring \
292+
# unicorn-store-spring="${ecr_uri}:latest" -n unicorn-store-spring 2>/dev/null
293+
# kubectl rollout status deployment unicorn-store-spring -n unicorn-store-spring --timeout=180s 2>/dev/null
236294
return 0
237295
fi
238296

239-
# Handle failed builds
297+
local deploy_log="${OUTPUT_DIR}/${tag}-deploy.txt"
298+
echo "=== DEPLOY ${tag} ===" > "${deploy_log}"
299+
300+
# Handle failed builds/pushes
240301
if [[ "$status" == "FAILED" ]]; then
241-
echo "${tag} | ${size_local:-N/A} | ${size_ecr:-N/A} | ${build_time:-N/A} | BUILD FAILED | -" >> "${RESULTS_FILE}"
302+
local fail_reason="${error_msg:-BUILD FAILED}"
303+
echo "${tag} | ${size_local:-N/A} | ${size_ecr:-N/A} | ${build_time:-N/A} | ${fail_reason}" >> "${RESULTS_FILE}"
304+
echo "Skipped: ${fail_reason}" >> "${deploy_log}"
305+
log_info "${tag}: ${fail_reason}"
242306
continue
243307
fi
244308

245-
# Deploy with new image (cold start)
309+
# Deploy with new image
246310
log_info "Deploying ${tag}..."
311+
echo "--- kubectl set image ---" >> "${deploy_log}"
247312
kubectl set image deployment/unicorn-store-spring \
248-
unicorn-store-spring="${ecr_uri}:${tag}" -n unicorn-store-spring 2>/dev/null
249-
if ! kubectl rollout status deployment unicorn-store-spring -n unicorn-store-spring --timeout=180s 2>/dev/null; then
250-
echo "${tag} | ${size_local} | ${size_ecr} | ${build_time} | DEPLOY FAILED | -" >> "${RESULTS_FILE}"
313+
unicorn-store-spring="${ecr_uri}:${tag}" -n unicorn-store-spring >> "${deploy_log}" 2>&1
314+
echo "--- kubectl rollout status ---" >> "${deploy_log}"
315+
if ! kubectl rollout status deployment unicorn-store-spring -n unicorn-store-spring --timeout=180s >> "${deploy_log}" 2>&1; then
316+
echo "--- kubectl describe deployment ---" >> "${deploy_log}"
317+
kubectl describe deployment unicorn-store-spring -n unicorn-store-spring >> "${deploy_log}" 2>&1
318+
echo "--- kubectl get events ---" >> "${deploy_log}"
319+
kubectl get events -n unicorn-store-spring --sort-by='.lastTimestamp' | tail -20 >> "${deploy_log}" 2>&1
320+
echo "${tag} | ${size_local} | ${size_ecr} | ${build_time} | DEPLOY FAILED" >> "${RESULTS_FILE}"
251321
continue
252322
fi
253323
sleep 15
254324
local startup_time=$(get_startup_time "$tag")
325+
echo "Startup time: ${startup_time}" >> "${deploy_log}"
255326

256-
# Restart (warm restart)
257-
kubectl rollout restart deployment unicorn-store-spring -n unicorn-store-spring 2>/dev/null
258-
local restart_time="RESTART FAILED"
259-
if kubectl rollout status deployment unicorn-store-spring -n unicorn-store-spring --timeout=180s 2>/dev/null; then
260-
sleep 15
261-
restart_time=$(get_startup_time "$tag")
262-
fi
263-
264-
echo "${tag} | ${size_local} | ${size_ecr} | ${build_time} | ${startup_time} | ${restart_time}" >> "${RESULTS_FILE}"
265-
log_info "${tag}: startup=${startup_time}, restart=${restart_time}"
327+
echo "${tag} | ${size_local} | ${size_ecr} | ${build_time} | ${startup_time}" >> "${RESULTS_FILE}"
328+
log_info "${tag}: startup=${startup_time}"
266329
done
267330
fi
268331

@@ -272,10 +335,6 @@ deploy_watcher() {
272335

273336
# Start deploy watcher in background
274337
start_watcher() {
275-
# Clean up old files
276-
rm -f "${QUEUE_FILE}" "${RESULTS_FILE}" "${WATCHER_PID_FILE}"
277-
touch "${QUEUE_FILE}"
278-
279338
# Start watcher in background
280339
deploy_watcher &
281340
echo $! > "${WATCHER_PID_FILE}"
@@ -292,22 +351,28 @@ wait_for_watcher() {
292351
fi
293352
}
294353

295-
# Write to queue
354+
# Write to queue (atomic write to avoid race conditions)
296355
queue_build_result() {
297356
local status="$1"
298357
local tag="$2"
299358
local size_local="$3"
300359
local size_ecr="$4"
301360
local build_time="$5"
302-
local error_msg="$6"
361+
local error_msg="${6:-}"
303362

304-
if [[ "$status" == "OK" ]]; then
305-
echo "OK|${tag}|${size_local}|${size_ecr}|${build_time}|" >> "${QUEUE_FILE}"
306-
else
307-
echo "FAILED|${tag}|${size_local}|${size_ecr}|${build_time}|${error_msg}" >> "${QUEUE_FILE}"
308-
fi
363+
# Sanitize fields - remove newlines and limit length
364+
size_ecr=$(echo "$size_ecr" | tr -d '\n' | head -c 20)
365+
error_msg=$(echo "$error_msg" | tr -d '\n' | head -c 50)
366+
367+
# Use consistent format: status|tag|size_local|size_ecr|build_time|error_msg
368+
echo "${status}|${tag}|${size_local}|${size_ecr}|${build_time}|${error_msg}" >> "${QUEUE_FILE}"
309369
}
310370

371+
# Initialize output directory
372+
rm -rf "${OUTPUT_DIR}"
373+
mkdir -p "${OUTPUT_DIR}"
374+
log_info "Output: ${OUTPUT_DIR}"
375+
311376
# Initialize deploy mode
312377
if [[ "$DEPLOY_MODE" == true ]]; then
313378
log_info "Deploy mode enabled - will push to ECR and deploy to EKS"
@@ -324,7 +389,8 @@ if [[ "$DEPLOY_MODE" == true ]]; then
324389
aws ecr get-login-password --region "${AWS_REGION}" \
325390
| docker login --username AWS --password-stdin "${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
326391

327-
# Start deploy watcher in background
392+
# Initialize queue and start deploy watcher
393+
touch "${QUEUE_FILE}"
328394
start_watcher
329395
fi
330396

@@ -343,17 +409,29 @@ for tag in "${METHODS[@]}"; do
343409
start_time=$(date +%s)
344410

345411
build_status=""
412+
push_status="OK"
346413
size_local="N/A"
347414
size_ecr="N/A"
415+
error_msg=""
348416

349-
if build_image "$tag" >/dev/null 2>&1; then
417+
build_log="${OUTPUT_DIR}/${tag}-build.txt"
418+
419+
if build_image "$tag" "$build_log"; then
350420
build_status=""
351421
size_local=$(docker images "${IMAGE_NAME}:${tag}" --format "{{.Size}}" 2>/dev/null || echo "N/A")
352422

353423
# Deploy mode: push and queue for deployment
354424
if [[ "$DEPLOY_MODE" == true ]]; then
355-
size_ecr=$(push_image "$tag" 2>/dev/null)
425+
size_ecr=$(push_image "$tag" "$build_log")
426+
# Check if push failed
427+
if [[ "$size_ecr" == PUSH_FAILED:* ]]; then
428+
error_msg="${size_ecr#PUSH_FAILED:}"
429+
size_ecr="N/A"
430+
push_status="FAILED"
431+
fi
356432
fi
433+
else
434+
error_msg="Build failed"
357435
fi
358436

359437
end_time=$(date +%s)
@@ -369,10 +447,10 @@ for tag in "${METHODS[@]}"; do
369447

370448
# Queue for deploy watcher
371449
if [[ "$DEPLOY_MODE" == true ]]; then
372-
if [[ "$build_status" == "" ]]; then
450+
if [[ "$build_status" == "" && "$push_status" == "OK" ]]; then
373451
queue_build_result "OK" "$tag" "$size_local" "$size_ecr" "$elapsed_fmt"
374452
else
375-
queue_build_result "FAILED" "$tag" "$size_local" "$size_ecr" "$elapsed_fmt" "Build failed"
453+
queue_build_result "FAILED" "$tag" "$size_local" "$size_ecr" "$elapsed_fmt" "$error_msg"
376454
fi
377455
fi
378456
done
@@ -388,4 +466,4 @@ if [[ "$DEPLOY_MODE" == true ]]; then
388466
fi
389467

390468
echo ""
391-
log_info "Complete"
469+
log_info "Complete (logs: ${OUTPUT_DIR})"

0 commit comments

Comments
 (0)