參考大禮包的目錄結構,再回頭看第8天的文章裡的一句:「使用Podman Quadlet的最大好處作為說服理由,我一定選.container裡的Environment變數。」
而大禮包的結構根本像單體系統的思維建置的,包山包海且角色失焦,不如依環境拆成數個小禮包,開發環境(DEV)、SIT/UAT都有自己的小禮包結構。因為A2A專案規模不是很大,所以只有在開發環境小禮包:
% tree
.
├── dockerfiles
│ ├── a2a-api
│ │ └── Dockerfile
│ ├── a2a-auth
│ │ └── Dockerfile
│ ├── a2a-batch
│ │ └── Dockerfile
│ ├── a2a-auth
│ │ └── Dockerfile
│ ├── a2a-proxy
│ │ ├── Dockerfile
│ │ └── nginx.conf
│ └── a2a-web
│ └── Dockerfile
├── quadlet
│ ├── a2a-api.container
│ ├── a2a-api.container.d
│ │ └── environment.conf
│ ├── a2a-auth.container
│ ├── a2a-auth.container.d
│ │ └── environment.conf
│ ├── a2a-batch.container
│ ├── a2a-batch.container.d
│ │ └── environment.conf
│ ├── a2a-net.network
│ ├── a2a-proxy.container
│ └── a2a-web.container
└── scripts
├── buildImage.sh
└── cleanupImages.sh
開發環境也就只有對dockerfile、quadlet與script版控。若客戶環境並不提供包版,就不需要dockerfile與script,而quadlet在DEV/SIT/UAT/PROD會有所不同。
至於scripts下兩個sh檔是透過AI產出,cleanupImages.sh是同一個容器image保留最近兩筆,避免占硬碟空間:
#!/usr/bin/env bash
set -Eeuo pipefail
usage() {
cat <<'USAGE'
Usage:
./cleanupImages.sh [service-name ...]
Examples:
./cleanupImages.sh
./cleanupImages.sh a2a-batch a2a-api
Behavior:
- Keeps the newest 2 version tags for each localhost/<service> image.
- Deletes older tags whose tag format is yyyyMMdd-NN, for example 20260611-01.
- Deletes images with repository or tag shown as <none>.
Optional environment variables:
KEEP_IMAGE_VEv
RSIONS Number of versions to keep per service. Default: 2
IMAGE_REPOSITORY_NS Image repository namespace. Default: localhost
DRY_RUN Set to true to print deletions without removing images.
USAGE
}
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
require_podman_connection() {
podman info >/dev/null 2>&1 || die "Cannot connect to Podman. Start Podman and try again."
}
remove_image() {
local image_ref="$1"
if [[ "$DRY_RUN" == "true" ]]; then
log "DRY_RUN remove: ${image_ref}"
return 0
fi
log "Removing ${image_ref}"
podman rmi "$image_ref"
}
list_services_from_podman() {
podman images --format '{{.Repository}}|{{.Tag}}' |
while IFS='|' read -r repo tag; do
[[ "$repo" == "${IMAGE_REPOSITORY_NS}/a2a-"* ]] || continue
[[ "$tag" =~ ^[0-9]{8}-[0-9]{2}$ ]] || continue
printf '%s\n' "${repo#${IMAGE_REPOSITORY_NS}/}"
done |
sort -u
}
cleanup_old_versions_for_service() {
local service="$1"
local repo="${IMAGE_REPOSITORY_NS}/${service}"
local count=0
local image_ref
log "Checking ${repo}; keeping newest ${KEEP_IMAGE_VERSIONS} version(s)"
while IFS= read -r image_ref; do
count=$((count + 1))
if (( count <= KEEP_IMAGE_VERSIONS )); then
log "Keep ${image_ref}"
else
remove_image "$image_ref"
fi
done < <(
podman images --format '{{.Repository}}|{{.Tag}}' |
while IFS='|' read -r image_repo image_tag; do
[[ "$image_repo" == "$repo" ]] || continue
[[ "$image_tag" =~ ^[0-9]{8}-[0-9]{2}$ ]] || continue
printf '%s:%s\n' "$image_repo" "$image_tag"
done |
sort -t ':' -k 2,2r
)
if (( count == 0 )); then
log "No versioned images found for ${repo}"
fi
}
cleanup_none_images() {
local ids=()
local id
log "Checking <none> images"
while IFS= read -r id; do
[[ -n "$id" ]] || continue
ids+=("$id")
done < <(
{
podman images --filter dangling=true -q
podman images --format '{{.Repository}}|{{.Tag}}|{{.ID}}' |
while IFS='|' read -r repo tag image_id; do
if [[ "$repo" == "<none>" || "$tag" == "<none>" ]]; then
printf '%s\n' "$image_id"
fi
done
} | sort -u
)
if (( ${#ids[@]} == 0 )); then
log "No <none> images found"
return 0
fi
for id in "${ids[@]}"; do
remove_image "$id"
done
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
require_command podman
KEEP_IMAGE_VERSIONS="${KEEP_IMAGE_VERSIONS:-2}"
IMAGE_REPOSITORY_NS="${IMAGE_REPOSITORY_NS:-localhost}"
DRY_RUN="${DRY_RUN:-false}"
[[ "$KEEP_IMAGE_VERSIONS" =~ ^[0-9]+$ ]] || die "KEEP_IMAGE_VERSIONS must be a number"
(( KEEP_IMAGE_VERSIONS >= 0 )) || die "KEEP_IMAGE_VERSIONS must be >= 0"
require_podman_connection
if (( $# > 0 )); then
SERVICES=("$@")
else
SERVICES=()
while IFS= read -r service; do
[[ -n "$service" ]] || continue
SERVICES+=("$service")
done < <(list_services_from_podman)
fi
if (( ${#SERVICES[@]} == 0 )); then
log "No matching ${IMAGE_REPOSITORY_NS}/a2a-* images found"
else
for service in "${SERVICES[@]}"; do
cleanup_old_versions_for_service "$service"
done
fi
cleanup_none_images
log "Cleanup completed"
而buildImage.sh則是從Git更新Source Code再進行compiler與包成image,省事很多。
#!/usr/bin/env bash
set -Eeuo pipefail
usage() {
cat <<'USAGE'
Usage:
./buildImage.sh <service-name>
Example:
./buildImage.sh a2a-batch
Optional environment variables:
JAVA_HOME_21 JDK 21 home path.
Default: /Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home
REMOTE_USER SCP remote user. Default: appuser
REMOTE_HOST SCP remote host. Default: 10.7.20.22
REMOTE_PATH SCP remote path. Default: .
SCP_PASSWORD_FILE Password file for sshpass -f.
Default: podman-microservices/scripts/.scp_password
Password upload:
1. Install sshpass if needed.
2. Put only the SSH password in the password file.
3. chmod 600 the password file.
If SCP_PASSWORD_FILE does not exist, the script uses plain scp, so SSH key auth
or interactive password entry can still work.
USAGE
}
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
die() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
}
find_built_jar() {
local service="$1"
local target_dir="$2"
local expected_jar="${target_dir}/${service}-1.0-SNAPSHOT.jar"
if [[ -f "$expected_jar" ]]; then
printf '%s\n' "$expected_jar"
return 0
fi
find "$target_dir" -maxdepth 1 -type f -name '*.jar' \
! -name '*-sources.jar' \
! -name '*-javadoc.jar' \
! -name '*.original' \
-print |
sort |
tail -n 1
}
next_image_tag() {
local service="$1"
local tag_date="$2"
local seq candidate image
for seq in $(seq 1 99); do
candidate="$(printf '%s-%02d' "$tag_date" "$seq")"
image="localhost/${service}:${candidate}"
if ! podman image exists "$image" >/dev/null 2>&1; then
printf '%s\n' "$candidate"
return 0
fi
done
die "No available image tag for ${service}:${tag_date}-01..99"
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
SERVICE="${1:-}"
[[ -n "$SERVICE" ]] || {
usage
exit 1
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKEND_SRC_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
CORE_DIR="${BACKEND_SRC_DIR}/systex-app-a2a-core"
SERVICE_DIR="${BACKEND_SRC_DIR}/${SERVICE}"
DOCKER_DIR="${BACKEND_SRC_DIR}/podman-microservices/dockerfiles/${SERVICE}"
JAVA_HOME_21="${JAVA_HOME_21:-/Library/Java/JavaVirtualMachines/openjdk-21.jdk/Contents/Home}"
REMOTE_USER="${REMOTE_USER:-appuser}"
REMOTE_HOST="${REMOTE_HOST:-10.7.20.22}"
REMOTE_PATH="${REMOTE_PATH:-.}"
SCP_PASSWORD_FILE="${SCP_PASSWORD_FILE:-${SCRIPT_DIR}/.scp_password}"
[[ -d "$CORE_DIR" ]] || die "Core project directory not found: $CORE_DIR"
[[ -d "$SERVICE_DIR" ]] || die "Service project directory not found: $SERVICE_DIR"
[[ -d "$DOCKER_DIR" ]] || die "Dockerfile directory not found: $DOCKER_DIR"
[[ -f "${DOCKER_DIR}/Dockerfile" ]] || die "Dockerfile not found: ${DOCKER_DIR}/Dockerfile"
[[ -d "$JAVA_HOME_21" ]] || die "JDK 21 not found: $JAVA_HOME_21"
require_command git
require_command mvn
require_command podman
require_command scp
export JAVA_HOME="$JAVA_HOME_21"
export PATH="${JAVA_HOME}/bin:${PATH}"
log "Using JAVA_HOME=${JAVA_HOME}"
java -version
log "Pulling latest source in ${BACKEND_SRC_DIR}"
cd "$BACKEND_SRC_DIR"
git pull
log "Installing latest systex-app-a2a-core to local Maven repository"
cd "$CORE_DIR"
mvn clean install
log "Packaging ${SERVICE}"
cd "$SERVICE_DIR"
mvn clean package
JAR_PATH="$(find_built_jar "$SERVICE" "${SERVICE_DIR}/target")"
[[ -n "$JAR_PATH" && -f "$JAR_PATH" ]] || die "No built jar found under ${SERVICE_DIR}/target"
JAR_NAME="$(basename "$JAR_PATH")"
log "Copying ${JAR_NAME} to ${DOCKER_DIR}"
cp "$JAR_PATH" "${DOCKER_DIR}/${JAR_NAME}"
TAG_DATE="$(date '+%Y%m%d')"
IMAGE_TAG="$(next_image_tag "$SERVICE" "$TAG_DATE")"
IMAGE_NAME="localhost/${SERVICE}:${IMAGE_TAG}"
TAR_NAME="${SERVICE}.tar"
log "Building image ${IMAGE_NAME}"
cd "$DOCKER_DIR"
podman build --arch amd64 --build-arg "JAR_FILE=./${JAR_NAME}" -t "$IMAGE_NAME" .
log "Saving image to ${DOCKER_DIR}/${TAR_NAME}"
podman save -o "$TAR_NAME" "$IMAGE_NAME"
log "Uploading ${TAR_NAME} to ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}"
if [[ -f "$SCP_PASSWORD_FILE" ]]; then
require_command sshpass
sshpass -f "$SCP_PASSWORD_FILE" scp "./${TAR_NAME}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}"
else
scp "./${TAR_NAME}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}"
fi
log "Done: ${IMAGE_NAME}"
這裡有個心得是:現在用AI產出script比人工思考產出的高效很多。