iT邦幫忙

2026 iThome 鐵人賽

DAY 28
0

參考大禮包的目錄結構,再回頭看第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比人工思考產出的高效很多。


上一篇
大禮包
系列文
Podman Quadlet-容器服務化(在單體系統到微服務之間)28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言