THỰC CHIẾN Trung bình

GitHub Actions → AWS ECR: build, test Docker bằng OIDC, không lưu access key

Lab CI độc lập: tạo ứng dụng Python, kiểm thử container, cấu hình IAM trust theo nhánh main và đẩy image lên ECR bằng thông tin xác thực ngắn hạn, kèm chẩn đoán lỗi.

Vibe DevOps·· 8 phút đọc
PythonDockerGitHub ActionsAWS IAMAmazon ECR

Đọc nhanh trước khi bắt tay làm

  • Chỉ push vào main mới chạy; container phải qua smoke test trước khi xin quyền AWS.
  • OIDC đổi danh tính workflow lấy phiên IAM ngắn hạn, không cần AWS access key trong GitHub Secrets.
  • Trust giới hạn đúng repository và nhánh; quyền upload giới hạn một ECR repository.
  • Image dùng tag commit SHA, kiến trúc linux/amd64; bài này không triển khai ECS.

Ghép các mảnh lại với nhau

  1. 01 →Push mainCheckout source
  2. 02 →Build & testHTTP trên loopback runner
  3. 03 →OIDC → STSNhận phiên IAM tạm thời
  4. 04 →Push ECRTag commit SHA bất biến
Trong bài này

Đẩy image từ laptop dễ; cho CI quyền đẩy mà không giữ access key lâu dài mới là phần cần thiết kế. Lab này tạo một HTTP service nhỏ, build đúng image sẽ phát hành, kiểm tra HTTP rồi push vào ECR private. Nó dừng ở registry: không tạo ECS service, không thay task definition, không mở ứng dụng ra Internet.

GitHub cấp OIDC token cho job; AWS kiểm tra audience và subject, sau đó STS cấp thông tin xác thực ngắn hạn. id-token: write chỉ cho phép yêu cầu token, không tự cấp quyền ghi ECR. Quyền thật đến từ IAM role.[1]

1. Chuẩn bị và ranh giới bảo mật

Cần repository GitHub có nhánh main, Actions được bật; tài khoản AWS; AWS CLI v2 trên máy quản trị đã đăng nhập bằng IAM Identity Center/SSO và có quyền tạo IAM role, OIDC provider, ECR repository. Không dùng root, không tạo access key cho workflow. Docker chỉ cần ở runner ubuntu-latest.

Ví dụ dùng Region ap-southeast-1, account mẫu 111122223333, repository ORG/REPO, ECR oidc-http-lab. Thay mọi account/ORG/REPO trước khi chạy. Lệnh AWS bên dưới dành cho terminal quản trị, không chép vào workflow.

Trust theo main không thay thế review code: ai sửa được workflow trên main có thể dùng role. Bật branch protection, bắt buộc review, hạn chế quyền sửa .github/workflows/. Lab không nhận pull_request, không chạy mã PR với quyền AWS.

2. Tạo ECR và IAM role

Tạo repository mới dành riêng cho lab:

aws sts get-caller-identity
aws ecr create-repository \
  --region ap-southeast-1 \
  --repository-name oidc-http-lab \
  --image-tag-mutability IMMUTABLE

IMMUTABLE ngăn ghi đè tag đã tồn tại; ECR báo ImageTagAlreadyExistsException nếu push lại tag cũ.[3] Dùng SHA thay latest để truy ra commit, nhưng SHA không tự khiến nội dung build tái lập nếu base image còn dùng tag.

Trong IAM → Identity providers → Add provider, chọn OpenID Connect, Provider URL https://token.actions.githubusercontent.com, Audience sts.amazonaws.com. Nếu provider đã có trong account, dùng lại, không tạo bản trùng.[1]

Lưu trust.json trên máy quản trị:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
        "token.actions.githubusercontent.com:sub": "repo:ORG/REPO:ref:refs/heads/main"
      }
    }
  }]
}

Chú ý subject mới: mẫu trên dành cho repository dùng subject truyền thống, thường là repository tạo trước 15/07/2026 chưa opt-in immutable subject. Upstream hiện ghi nhận repository mới dùng dạng repo:ORG@ORG_ID/REPO@REPO_ID:ref:refs/heads/main. Với repository đó, thay đúng subject có ID trong trust; không dùng nguyên mẫu legacy rồi kỳ vọng đăng nhập được.[1][5]

Subject phải khớp tên owner/repository thực tế, không thêm wildcard. Nếu sau này thêm GitHub Environment, subject chuyển sang repo:ORG/REPO:environment:TEN_ENV; phải sửa trust và đặt protection rules cho environment, không mở rộng thành repo:ORG/REPO:* để chữa lỗi.[1]

Lưu ecr-policy.json. Các quyền upload chỉ áp dụng một repository; riêng GetAuthorizationToken cần Resource: "*" theo mẫu AWS.[2]

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage",
        "ecr:BatchGetImage"
      ],
      "Resource": "arn:aws:ecr:ap-southeast-1:111122223333:repository/oidc-http-lab"
    },
    {
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    }
  ]
}

Tạo role, gắn policy, lấy ARN:

aws iam create-role --role-name github-ecr-lab \
  --assume-role-policy-document file://trust.json
aws iam put-role-policy --role-name github-ecr-lab \
  --policy-name PushOneRepository \
  --policy-document file://ecr-policy.json
aws iam get-role --role-name github-ecr-lab \
  --query Role.Arn --output text

Trong GitHub Settings → Secrets and variables → Actions → Variables, tạo ba repository variables: AWS_REGION=ap-southeast-1, AWS_ROLE_ARN bằng ARN vừa lấy, ECR_REPOSITORY=oidc-http-lab. Đây là định danh, không phải mật khẩu. Không tạo AWS_ACCESS_KEY_ID hay AWS_SECRET_ACCESS_KEY.

3. Ứng dụng độc lập và Dockerfile

Ở gốc repository GitHub, tạo app.py:

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        healthy = self.path == "/health"
        body = b"ok\n" if healthy else b"not found\n"
        self.send_response(200 if healthy else 404)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


if __name__ == "__main__":
    ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()

Tạo Dockerfile; chỉ copy file cần thiết, không đưa credential hay toàn bộ working directory vào image:

FROM python:3.13-slim
WORKDIR /app
COPY app.py .
RUN python -m py_compile app.py
USER 10001:10001
EXPOSE 8080
CMD ["python", "-u", "app.py"]

Đây là service minh họa, không phải web server production có TLS, xác thực hay giới hạn tải. Base image dùng tag để lab dễ chạy; pipeline production nên khóa digest đã rà soát và có lịch cập nhật.

4. Workflow build → smoke test → push

Tạo .github/workflows/ecr.yml như dưới. Các major tag tương ứng được upstream dùng trong tài liệu: checkout v7, configure-aws-credentials v6, amazon-ecr-login v2.[4][5][6] Trước khi dùng production, kiểm tra release rồi pin full commit SHA đã audit, không tự đoán SHA.

name: Build test and push ECR
on:
  push:
    branches: [main]
permissions:
  contents: read
  id-token: write
concurrency:
  group: ecr-main
  cancel-in-progress: false
jobs:
  image:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    env:
      AWS_REGION: ${{ vars.AWS_REGION }}
      ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - name: Build amd64 image
        run: docker build --platform linux/amd64 -t lab:${{ github.sha }} .
      - name: Smoke test isolated container
        shell: bash
        run: |
          set -euo pipefail
          cleanup() {
            docker logs smoke || true
            docker rm -f smoke || true
          }
          trap cleanup EXIT
          docker run -d --name smoke --platform linux/amd64 \
            -p 127.0.0.1:18080:8080 "lab:$GITHUB_SHA"
          ready=0
          for attempt in $(seq 1 30); do
            if curl --fail --silent --show-error --max-time 1 \
              http://127.0.0.1:18080/health -o /tmp/health; then
              ready=1
              break
            fi
            sleep 1
          done
          test "$ready" -eq 1
          printf 'ok\n' | cmp - /tmp/health
          status=$(curl --silent --show-error --max-time 2 \
            -o /tmp/missing -w '%{http_code}' \
            http://127.0.0.1:18080/missing)
          test "$status" = 404
          printf 'not found\n' | cmp - /tmp/missing
      - name: Assume AWS role through OIDC
        uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: ${{ vars.AWS_REGION }}
          role-session-name: gha-${{ github.run_id }}
          role-duration-seconds: 900
      - name: Verify AWS identity
        run: aws sts get-caller-identity
      - name: Login ECR
        id: login
        uses: aws-actions/amazon-ecr-login@v2
      - name: Push tested image
        env:
          REGISTRY: ${{ steps.login.outputs.registry }}
        run: |
          set -euo pipefail
          image="$REGISTRY/$ECR_REPOSITORY:$GITHUB_SHA"
          docker tag "lab:$GITHUB_SHA" "$image"
          docker push "$image"
          printf 'Published `%s`\n' "$image" >> "$GITHUB_STEP_SUMMARY"

Test chạy trước khi nhận credential AWS; cổng chỉ bind loopback của runner, không gọi production. Vòng chờ có hạn, kiểm tra cả status lẫn body; trap thu log và xóa container dù test lỗi. Image đã test được tag lại, không build lần hai. linux/amd64 dành cho ECS task cấu hình X86_64; không dùng image này cho task ARM64.

5. Xác minh và chẩn đoán

Push commit mới lên main, mở tab Actions. Điều kiện đạt: build thành công, /health đúng ok\n, đường dẫn lạ trả 404; identity có account mong đợi và ARN assumed-role/github-ecr-lab/...; bước push trả digest. Từ máy quản trị, kiểm tra SHA thực tế:

SHA=THAY_BANG_40_KY_TU_COMMIT_SHA
aws ecr describe-images --region ap-southeast-1 \
  --repository-name oidc-http-lab --image-ids imageTag="$SHA" \
  --query 'imageDetails[0].{digest:imageDigest,tags:imageTags}'

Lệnh đọc này dùng quyền quản trị/đọc của bạn; role CI không được cấp DescribeImages chỉ để chẩn đoán.

Triệu chứng Kiểm tra trước
AssumeRoleWithWebIdentity bị từ chối Account của provider/role, audience, subject đúng main, có vô tình thêm environment không.
Không lấy được OIDC token id-token: write, chính sách tổ chức có chặn action không.
Login được nhưng upload AccessDenied ARN repository, Region và upload policy; đăng nhập registry không đồng nghĩa có quyền push.
Repository không tồn tại Tạo ECR trước; đối chiếu variable và đúng account.
Smoke test timeout Xem log container, process có listen 0.0.0.0:8080, cổng và lỗi build.
ImageTagAlreadyExistsException SHA đã được phát hành; không chuyển repository sang mutable để vượt lỗi.[3]

Quy ước retry: workflow cố ý fail nếu SHA đã tồn tại. Re-run trước khi push thành công có thể chạy lại; re-run sau khi tag đã được ghi phải dùng artifact cũ hoặc commit mới. Nếu push mất mạng, kiểm tra ECR trước: job đỏ không chứng minh image chưa lên. Không xóa tag cũ chỉ để làm CI xanh.

6. Chi phí, dọn lab và bước tiếp

ECR tính phí dung lượng lưu trữ và một số luồng truyền dữ liệu; không mặc định mọi tài khoản đều được miễn phí.[7] GitHub Actions cũng cần đối chiếu hạn mức/phí của gói đang dùng. Lab này không tạo ECS, NAT Gateway hoặc load balancer. Với nhiều SHA, đặt lifecycle policy sau khi xác định thời hạn rollback.

Chỉ khi chắc chắn repository riêng cho lab, không có workload đang dùng, mới xóa toàn bộ image:

aws ecr delete-repository --region ap-southeast-1 \
  --repository-name oidc-http-lab --force
aws iam delete-role-policy --role-name github-ecr-lab \
  --policy-name PushOneRepository
aws iam delete-role --role-name github-ecr-lab

Xóa repository variables nếu ngừng lab; giữ OIDC provider nếu role khác còn dùng. CI push thành công chỉ chứng minh artifact đã vào ECR, chưa chứng minh ứng dụng chạy trên AWS. Tiếp tục với triển khai container lên ECS Fargate bằng URI/digest vừa xác minh.

Phạm vi kiểm chứng: đây là cấu hình hướng dẫn, không phải báo cáo đã chạy AWS. Cần thực hiện các kiểm tra Actions/ECR ở trên trong account của bạn; không có kết quả cloud giả lập để thay thế.

Sources

[1] https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws — GitHub OIDC for AWS [2] https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-push-iam.html — ECR push permissions [3] https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-tag-mutability.html — ECR immutable tags [4] https://raw.githubusercontent.com/actions/checkout/main/README.md — Checkout action [5] https://raw.githubusercontent.com/aws-actions/configure-aws-credentials/main/README.md — Configure AWS credentials action [6] https://raw.githubusercontent.com/aws-actions/amazon-ecr-login/main/README.md — ECR login action [7] https://aws.amazon.com/ecr/pricing — ECR pricing

Đọc có chọn lọc, chạy có kiểm tra. Ví dụ dùng đường dẫn của dự án Vibe DevOps; thay lại cấu hình theo máy của bạn và thử trên môi trường riêng trước khi áp dụng.← Về mục Thực chiến

Làm tiếp một việc nữa