SuperAPI AI Directory

containerization

Deployment & operations

View repository

Updated 10-15 19:58

About this skill

When to use

  • to-be-architecture.md When 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 == Rehost When 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.md Exists and compute field confirmed
  • aidlc/skills/construction/risk-discovery PASS (data consistency, rollback path verification complete)
  • Docker 24+ installed, docker buildx plugin 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:

  1. Base image upgrade (e.g., node:20.11-alpine → node:20.12-alpine)
  2. remove or pin the vulnerable package
  3. If unavoidable, .trivyignore record the CVE number and expiration date in + audit.md state 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:latest or FROM node:latest — reproducibility compromised
  • USER root or 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 + .trivyignore rationale not recorded

References

Official documentation

Source methodology (MIT-0)

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