When to use
to-be-architecture.mdWhen ECS Fargate or EKS is specified as the compute- The step of building an existing JAR/WAR/Node.js/Python application into a Docker image
- Refactoring when an existing Dockerfile does not have at least one of multi-stage, multi-arch, or non-root applied
- When a draft ECS Task Definition or Kubernetes Deployment manifest is needed after pushing to ECR
When not to use
decided_pattern == RehostWhen deploying only to EC2 targets — containerization not applicable- Serverless Lambda-only path — use a separate skill for zip packaging or Container Image for Lambda
- When already running in production and the only purpose is CVE patching — use a separate patching process
Prerequisites
.omao/plans/modernization/to-be-architecture.mdExists andcomputefield confirmedaidlc/skills/construction/risk-discoveryPASS (data consistency, rollback path verification complete)- Docker 24+ installed,
docker buildxplugin enabled - AWS CLI + ECR push permission (IRSA or local profile)
- Trivy v0.48+ or grype installed (for security scanning)
Procedure
Step 1. Write Multi-Stage Dockerfile
Separate build dependencies from the runtime. The minimal base images per language are as follows.
| Runtime | Builder | Runtime |
|---|---|---|
| Node.js 20 | node:20-alpine |
gcr.io/distroless/nodejs20-debian12 |
| Python 3.11 | python:3.11-slim |
gcr.io/distroless/python3-debian12 |
| Java 21 | eclipse-temurin:21-jdk-alpine |
eclipse-temurin:21-jre-alpine |
| Go 1.22 | golang:1.22-alpine |
gcr.io/distroless/static-debian12 |
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Runtime stage (distroless, non-root)
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER nonroot
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["/nodejs/bin/node", "dist/health.js"]
CMD ["dist/main.js"]
Step 2. Multi-Arch Build (amd64 + arm64)
Because AWS Graviton (arm64) is up to 40% cheaper for the same performance, multi-arch builds are the default.
# One-time setup
docker buildx create --name oma-builder --use
docker buildx inspect --bootstrap
# Build + push
ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
REGION=ap-northeast-2
REPO=${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com/${APP_NAME}
aws ecr get-login-password --region ${REGION} | \
docker login --username AWS --password-stdin ${REPO}
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ${REPO}:${VERSION} \
-t ${REPO}:latest \
--push \
.
Step 3. Security Scan (Trivy + grype)
In the CI pipeline, run both scanners in parallel before pushing the image and HIGH/CRITICAL set 0 findings as the gate.
# Trivy
trivy image --severity HIGH,CRITICAL --exit-code 1 ${REPO}:${VERSION}
# grype (2차 검증)
grype ${REPO}:${VERSION} --fail-on high
Response upon detection:
- Base image upgrade (e.g.,
node:20.11-alpine→node:20.12-alpine) - remove or pin the vulnerable package
- If unavoidable,
.trivyignorerecord the CVE number and expiration date in +audit.mdstate the rationale in
Step 4. ECR Lifecycle Policy
Apply a lifecycle policy to prevent accumulation of untagged images.
{
"rules": [
{
"rulePriority": 1,
"description": "Expire untagged images older than 14 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 14
},
"action": {"type": "expire"}
}
]
}
Step 5. ECS Task Definition Template
to-be-architecture.compute == ECS Fargate When the path is:
{
"family": "${APP_NAME}",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"runtimePlatform": {"cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX"},
"executionRoleArn": "arn:aws:iam::${ACCOUNT}:role/${APP_NAME}-exec",
"taskRoleArn": "arn:aws:iam::${ACCOUNT}:role/${APP_NAME}-task",
"containerDefinitions": [
{
"name": "${APP_NAME}",
"image": "${REPO}:${VERSION}",
"essential": true,
"portMappings": [{"containerPort": 8080, "protocol": "tcp"}],
"healthCheck": {
"command": ["CMD-SHELL", "wget -qO- http://localhost:8080/healthz || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 10
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/${APP_NAME}",
"awslogs-region": "${REGION}",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
Step 6. EKS Deployment + Service Template
to-be-architecture.compute == EKS When the path is:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${APP_NAME}
namespace: ${NS}
spec:
replicas: 3
selector:
matchLabels: {app: ${APP_NAME}}
template:
metadata:
labels: {app: ${APP_NAME}}
spec:
serviceAccountName: ${APP_NAME}-sa
securityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
seccompProfile: {type: RuntimeDefault}
containers:
- name: app
image: ${REPO}:${VERSION}
ports: [{containerPort: 8080}]
resources:
requests: {cpu: 500m, memory: 512Mi}
limits: {cpu: 1000m, memory: 1Gi}
livenessProbe:
httpGet: {path: /healthz, port: 8080}
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet: {path: /readyz, port: 8080}
initialDelaySeconds: 5
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: [ALL]}
Step 7. Output Artifacts
.omao/plans/modernization/containerization-report.md record in.
# Containerization Report
- app: ${APP_NAME}
- base_image: gcr.io/distroless/nodejs20-debian12
- architectures: [linux/amd64, linux/arm64]
- image_size_mb: 142
- cve_scan: PASS (0 HIGH/CRITICAL)
- ecr_uri: ${REPO}:${VERSION}
- manifest_path: manifests/${APP_NAME}-task-def.json (ECS) | deployment.yaml (EKS)
- next_skill: cutover-planning
Good examples
gcr.io/distroless/nodejs20-debian12+ buildx multi-arch + Trivy 0 HIGH → production deployment ready- Java Spring Boot →
eclipse-temurin:21-jre-alpine+ non-root user 10001 + JLink slim JRE - Go static binary →
distroless/static+ CGO_ENABLED=0
Bad examples (prohibited)
FROM ubuntu:latestorFROM node:latest— reproducibility compromisedUSER rootor missing USER directive — security control violation- Health Check undefined — ECS/EKS cannot restart unhealthy Tasks/Pods
- Deployment fails on the Graviton NodePool after skipping the multi-arch build
- Ignoring Trivy HIGH +
.trivyignorerationale not recorded
References
Official documentation
- Docker multi-stage builds — official guide
- Docker buildx — multi-arch build
- Amazon ECR User Guide — ECR official documentation
- Trivy Documentation — container vulnerability scanning
- Distroless images — minimal base image
Source methodology (MIT-0)
- container-best-practices.md (Kiro) — original Best Practice
Related documents (internal)
../to-be-architecture/SKILL.md— preceding skill../cutover-planning/SKILL.md— following skill/home/ubuntu/workspace/oh-my-aidlcops/plugins/aidlc/CLAUDE.md— provided by risk-discovery