วิธีสร้าง Custom multi-arch container image สำหรับใช้ใน CI/CD Pipeline
ในโลกของ CI/CD ที่รวดเร็ว ทีม DevOps จำเป็นต้องมีเครื่องมือที่ยืดหยุ่นและมีประสิทธิภาพ การสร้าง custom container image ที่รองรับหลายสถาปัตยกรรม (multi-arch) เป็นกุญแจสำคัญในการปรับปรุงขั้นตอนการทำงานให้คล่องตัวและปลอดภัยยิ่งขึ้น ในบทความนี้ เราจะมาเจาะลึกวิธีการสร้าง multi-arch container image สำหรับ Bitbucket Pipeline บน AWS พร้อมตัวอย่างและคำอธิบาย Dockerfile อย่างละเอียด
ทำไมต้องสร้าง Custom Container Image
CI/CD platform ยอดนิยมอย่าง GitLab CI, GitHub Actions และ Bitbucket Pipeline มักใช้งานผ่าน container ซึ่งบางครั้งอาจไม่ตอบโจทย์ทุกความต้องการ การสร้าง custom image ช่วยให้:
- รวมเครื่องมือที่จำเป็น: เพิ่มเครื่องมือเฉพาะ เช่น AWS CLI, notation, oras หรือ jq เข้าไปใน image ได้โดยตรง
- เพิ่มความปลอดภัย: หลีกเลี่ยงการใช้ image จากแหล่งที่ไม่น่าเชื่อถือ
- ควบคุมเวอร์ชัน: กำหนดเวอร์ชันของเครื่องมือต่างๆ ได้เอง
เจาะลึก Dockerfile
ในตัวอย่างนี้ เราจะสร้าง image ที่มี AWS CLI, notation, oras OCI registry client และ jq สำหรับใช้งานใน Bitbucket Pipeline บน AWS Infrastructure
Dockerfile นี้สร้าง image ที่รองรับทั้งสถาปัตยกรรม amd64 และ arm64
# --- Installer Stage (Multi-Arch) ---
FROM --platform=$BUILDPLATFORM public.ecr.aws/amazonlinux/amazonlinux:2023 AS installer
# Define architecture-specific variables
ARG TARGETOS TARGETARCH
ARG AWSCLI_FILE="awscliv2.zip"
ARG SIGNER_BINARY_FILE="aws-signer-notation-cli.rpm"
ARG ORAS_FILE="oras_linux.tar.gz"
ARG AWSCLI_LINK_AMD64="https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip"
ARG SIGNER_BINARY_LINK_AMD64="https://d2hvyiie56hcat.cloudfront.net/linux/amd64/installer/rpm/latest/aws-signer-notation-cli_amd64.rpm"
ARG ORAS_LINK_AMD64="https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_amd64.tar.gz"
ARG AWSCLI_LINK_ARM64="https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip"
ARG SIGNER_BINARY_LINK_ARM64="https://d2hvyiie56hcat.cloudfront.net/linux/arm64/installer/rpm/latest/aws-signer-notation-cli_arm64.rpm"
ARG ORAS_LINK_ARM64="https://github.com/oras-project/oras/releases/download/v1.2.0/oras_1.2.0_linux_arm64.tar.gz"
RUN yum update -y && \
yum install -y unzip less groff jq tar gzip && \
# Conditional download and installation based on architecture
if [ "$TARGETARCH" == "amd64" ]; then \
curl $AWSCLI_LINK_AMD64 -o $AWSCLI_FILE && \
curl $SIGNER_BINARY_LINK_AMD64 -o $SIGNER_BINARY_FILE && \
curl -L $ORAS_LINK_AMD64 -o $ORAS_FILE && \
unzip $AWSCLI_FILE && \
./aws/install --bin-dir /aws-cli-bin/ && \
yum install -y $SIGNER_BINARY_FILE && \
tar -xzf $ORAS_FILE; \
elif [ "$TARGETARCH" == "arm64" ]; then \
curl $AWSCLI_LINK_ARM64 -o $AWSCLI_FILE && \
curl $SIGNER_BINARY_LINK_ARM64 -o $SIGNER_BINARY_FILE && \
curl -L $ORAS_LINK_AMD64 -o $ORAS_FILE && \
unzip $AWSCLI_FILE && \
./aws/install --bin-dir /aws-cli-bin/ && \
yum install -y $SIGNER_BINARY_FILE && \
tar -xzf $ORAS_FILE; \
fi && \
yum clean all
# --- Final Stage ---
FROM public.ecr.aws/amazonlinux/amazonlinux:2023
RUN yum update -y && \
yum install -y jq && \
yum clean all
COPY --from=installer /usr/local/aws-cli/ /usr/local/aws-cli/
COPY --from=installer /aws-cli-bin/ /usr/local/bin/
COPY --from=installer /usr/local/bin/notation /usr/local/bin/notation
COPY --from=installer /oras /usr/local/bin/oras
COPY --from=installer /root/.config /root/.config
WORKDIR /aws
ENTRYPOINT ["/usr/local/bin/aws"]
Dockerfile นี้สร้าง image ของ Docker ที่มี AWS CLI (Command Line Interface), AWS Signer binary, jq command-line JSON processor, และ ORAS (OCI Registry as Storage) ติดตั้งอยู่ ซึ่ง image นี้ถูกออกแบบมาให้รองรับหลายสถาปัตยกรรม (multi-architecture) โดยเฉพาะ amd64 และ arm64
Installer Stage (Multi-Arch)
FROM --platform=$BUILDPLATFORM public.ecr.aws/amazonlinux/amazonlinux:2023 AS installer
สร้าง stage ชื่อ “installer” โดยใช้ image amazonlinux:2023
เป็น base image
--platform=$BUILDPLATFORM
ทำให้ Docker สร้าง image สำหรับสถาปัตยกรรมที่ตรงกับเครื่องที่ใช้สร้าง
ARG ...
กำหนดตัวแปรต่าง ๆ ที่จะใช้ใน Dockerfile เช่น ชื่อไฟล์, URL สำหรับดาวน์โหลด, และตัวแปรเฉพาะสถาปัตยกรรม
RUN ...
ทำการอัปเดตและติดตั้ง package ที่จำเป็น เช่น unzip
, less
, groff
, jq
, tar
, gzip
ใน Dockerfile เดียวนี้สามารถใช้สร้างได้ทั้ง container image บน สถาปัตยกรรม amd64 และ arm64 โดยตรวจสอบสถาปัตยกรรม (TARGETARCH
) และดาวน์โหลด/ติดตั้งเครื่องมือที่เกี่ยวข้องตามสถาปัตยกรรมนั้น ๆ โดยใน block ของแต่ละสถาปัตยกรรมจะมีการทำงานดังต่อไปนี้
- ดาวน์โหลดไฟล์ AWS CLI, AWS Signer binary, และ ORAS
- unzip ไฟล์ AWS CLI และติดตั้ง
- ติดตั้ง AWS Signer binary
- แตกไฟล์ ORAS
- ลบไฟล์ที่ไม่จำเป็นออกเพื่อลดขนาด image
Final Stage
FROM public.ecr.aws/amazonlinux/amazonlinux:2023
สร้าง stage สุดท้ายโดยใช้ amazonlinux:2023
เป็น base image อีกครั้ง
RUN ...
อัปเดตและติดตั้ง jq
และลบไฟล์ที่ไม่จำเป็นออก
COPY --from=installer ...
คัดลอกไฟล์และโฟลเดอร์ที่ติดตั้งไว้ใน stage “installer” ไปยัง stage สุดท้าย คัดลอก AWS CLI, AWS Signer binary, ORAS, และไฟล์ configuration บางส่วน
WORKDIR /aws
ตั้งค่า working directory ภายใน container เป็น /was
ENTRYPOINT ["/usr/local/bin/aws"]
กำหนดให้ /usr/local/bin/aws
(AWS CLI) เป็นคำสั่งเริ่มต้นเมื่อ container เริ่มทำงาน
Docker manifest
ในขั้นตอนสุดท้ายคือการสร้าง container image ในแต่ละสถาปัตยกรรมแล้วนำ image ที่สร้างมาในแต่ละสถาปัตยกรรมรวมี้เข้าด้วยกันเป็น Docker manifest เดียว ทำให้ง่ายต่อการดึง (pull) และใช้งาน image ที่ถูกต้องโดยอัตโนมัติตามสถาปัตยกรรมของระบบที่ใช้ในการ pull
การสร้าง container image จะทำผ่าน Bitbucket Pipeline โดยการสร้าง container image ในแต่ละสถาปัตยกรรมจะใช้วิธีแบบ native โดยไม่ผ่าน emulator อย่าง buildx เนื่องจากทำงานค่อนข้างช้า และบางครั้ง buildx ทำงานใน runner ไม่เสร็จจน Job ตายไป
การสร้าง multi-arch container image ด้วย Bitbucket pipeline ต้องทำการเตรียม Bitbucket runner ตาม สถาปัตยกรรมของ container ที่ต้องการสร้างเช่นในกรณีตัวอย่างนีคือสถาปัตยกรรม amd64 และ arm64 อย่างละตัว
Job ของ Bitbucket pipeline จะทำงานบน runner ในแต่ละสถาปัตกรรมโดยดูจาก label เช่นในตัวอย่างใช้ label my-amd64-runner สำหรับสถาปัตยกรรมแบบ amd64 และ my-arm64-runner สำหรับสถาปัตยกรรมแบบ arm64
bitbucket-pipelines.yml เพื่อใช้ในการสร้าง container image
# You can specify a custom docker image from Docker Hub as your build environment.
image: public.ecr.aws/aws-cli/aws-cli
definitions:
services:
docker: # Define a custom docker daemon - can only be used with a self-hosted runner
image: public.ecr.aws/docker/library/docker:dind
memory: 2048
pipelines:
default:
- parallel:
- step:
name: Container build AMD64
oidc: true
runs-on:
- linux
- self.hosted
- my-amd64-runner
services:
- docker
script:
- export TAG=$(date +%Y%m%d) # Format: YYYYMMDD
- export CI_REGISTRY_IMAGE="$CI_ECR_URI/cicd/aws-cli"
# Retrieve an authentication token and authenticate your Docker client to the registr
- export AWS_REGION=$CI_AWS_DEFAULT_REGION
- export AWS_ROLE_ARN=$CI_AWS_OIDC_ROLE_ARN
- export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
- echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
# Login ECR container registry
- export $(printf "AWS_ECR_LOGIN_PASSWORD=%s" $(aws ecr get-login-password --region $CI_AWS_DEFAULT_REGION))
- echo $AWS_ECR_LOGIN_PASSWORD | docker login --username AWS --password-stdin $CI_ECR_URI
# Build and Push container image
- docker build --platform linux/amd64 --pull --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$TAG-amd64 .
- docker push $CI_REGISTRY_IMAGE --all-tags
- step:
name: Container build ARM64
oidc: true
runs-on:
- linux.arm64
- self.hosted
- my-arm64-runner
services:
- docker
script:
- export TAG=$(date +%Y%m%d) # Format: YYYYMMDD
- export CI_REGISTRY_IMAGE="$CI_ECR_URI/cicd/aws-cli"
# Retrieve an authentication token and authenticate your Docker client to the registr
- export AWS_REGION=$CI_AWS_DEFAULT_REGION
- export AWS_ROLE_ARN=$CI_AWS_OIDC_ROLE_ARN
- export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
- echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
# Login ECR container registry
- export $(printf "AWS_ECR_LOGIN_PASSWORD=%s" $(aws ecr get-login-password --region $CI_AWS_DEFAULT_REGION))
- echo $AWS_ECR_LOGIN_PASSWORD | docker login --username AWS --password-stdin $CI_ECR_URI
# Build and Push container image
- docker build --platform linux/arm64 --pull --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$TAG-arm64 .
- docker push $CI_REGISTRY_IMAGE --all-tags
- step:
name: Manifest
oidc: true
runs-on:
- linux
- self.hosted
- my-amd64-runner
services:
- docker
script:
- export TAG=$(date +%Y%m%d) # Format: YYYYMMDD
- export CI_REGISTRY_IMAGE="$CI_ECR_URI/cicd/aws-cli"
# Retrieve an authentication token and authenticate your Docker client to the registr
- export AWS_REGION=$CI_AWS_DEFAULT_REGION
- export AWS_ROLE_ARN=$CI_AWS_OIDC_ROLE_ARN
- export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
- echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
# Login ECR container registry
- export $(printf "AWS_ECR_LOGIN_PASSWORD=%s" $(aws ecr get-login-password --region $CI_AWS_DEFAULT_REGION))
- echo $AWS_ECR_LOGIN_PASSWORD | docker login --username AWS --password-stdin $CI_ECR_URI
- docker pull $CI_REGISTRY_IMAGE:$TAG-arm64
- docker pull $CI_REGISTRY_IMAGE:$TAG-amd64
- docker manifest create $CI_REGISTRY_IMAGE:$TAG $CI_REGISTRY_IMAGE:$TAG-amd64 $CI_REGISTRY_IMAGE:$TAG-arm64
- docker manifest annotate $CI_REGISTRY_IMAGE:$TAG $CI_REGISTRY_IMAGE:$TAG-amd64 --os linux --arch amd64
- docker manifest annotate $CI_REGISTRY_IMAGE:$TAG $CI_REGISTRY_IMAGE:$TAG-arm64 --os linux --arch arm64
- docker manifest push $CI_REGISTRY_IMAGE:$TAG
code ด้านบนเป็น Bitbucket Pipeline ที่สร้าง Docker image สำหรับสถาปัตยกรรม amd64 และ arm64 โดย Job ที่ใช้สร้างจะทำงานพร้อมกัน จากที่ระบุ parallel step ของ Job Container build AMD64 และ Container build ARM64
จากนั้นจึงรวมเข้าด้วยกันเป็น Docker manifest เดียว ใน Job Manifest โดยอธิบายการทำงานของ code คร่าวๆดังนี้
docker pull $CI_REGISTRY_IMAGE:$TAG-arm64
pull image ที่มีชื่อเก็บในตัวแปร$CI_REGISTRY_IMAGE
และ tag $TAG-arm64
จาก registry ที่ระบุในตัวแปร $CI_REGISTRY_IMAGE
ของ Bitbucket pipeline ซึ่งคือ URI ของ ECR เช่น <My AWS Account ID>.dkr.ecr.ap-southeast-1.amazonaws.com และ ตัวแปร TAG ได้มาจากวันที่ปัจจุบันตามรูปแบบ YYYYMMDD นอกจากนั้น Image นี้สร้างขึ้นสำหรับสถาปัตยกรรม arm64
docker pull $CI_REGISTRY_IMAGE:$TAG-amd64
เหมือน Image ก่อนหน้าแต่สำหรับสถาปัตยกรรม amd64
docker manifest create $CI_REGISTRY_IMAGE:$TAG $CI_REGISTRY_IMAGE:$TAG-amd64 $CI_REGISTRY_IMAGE:$TAG-arm64
สร้าง manifest ใหม่ที่มีชื่อ $CI_REGISTRY_IMAGE
และ tag $TAG
Manifest นี้จะอ้างอิงถึง image ทั้งสองที่ดึงมาในขั้นตอนก่อนหน้า ($CI_REGISTRY_IMAGE:$TAG-amd64
และ $CI_REGISTRY_IMAGE:$TAG-arm64
)
docker manifest annotate $CI_REGISTRY_IMAGE:$TAG $CI_REGISTRY_IMAGE:$TAG-amd64 --os linux --arch amd64
เพิ่ม metadata ให้กับ manifest เพื่อระบุว่า image $CI_REGISTRY_IMAGE:$TAG-amd64
มีไว้สำหรับระบบปฏิบัติการ Linux และสถาปัตยกรรม amd64
docker manifest annotate $CI_REGISTRY_IMAGE:$TAG $CI_REGISTRY_IMAGE:$TAG-arm64 --os linux --arch arm64
เพิ่ม metadata ให้กับ manifest เพื่อระบุว่า image $CI_REGISTRY_IMAGE:$TAG-arm64
มีไว้สำหรับระบบปฏิบัติการ Linux และสถาปัตยกรรม arm64
docker manifest push $CI_REGISTRY_IMAGE:$TAG
อัปโหลด (push) manifest ที่สร้างและกำหนดค่าแล้วไปยัง registry ที่ระบุในตัวแปร $CI_REGISTRY_IMAGE
การใช้งาน
เวลาเรียกใช้ หรือ pull image ถ้าอยู่บน environment ที่เป็น amd64 เช่นบน EC2 แล้วใช้คำสั่ง docker pull
docker pull <My AWS Account ID>.dkr.ecr.ap-southeast-1.amazonaws.com/cicd/aws-cli:20240922
Docker จะไป pull image ที่เป็น สถาปัตยกรรม amd64 ซึ่ง image จริงๆคือ <My AWS Account ID>.dkr.ecr.ap-southeast-1.amazonaws.com/cicd/aws-cli:20240922-amd64
เช่นเดียวกัน ถ้าอยู่บน environment ที่เป็น arm64 ก็จะได้ image AWS Account ID>.dkr.ecr.ap-southeast-1.amazonaws.com/cicd/aws-cli:20240922-amd64 กลับมา