Sửa lỗi Terraform 'AccessDenied: s3:GetObject' khi Refresh State từ S3 Backend

intermediate🏗️ Terraform2026-06-02| Terraform 1.x, AWS S3 backend, IAM user hoặc role (thường dùng trong CI/CD pipelines: GitHub Actions, GitLab CI, Jenkins)

Error Message

Error refreshing state: AccessDenied: User: arn:aws:iam::123456789:user/ci is not authorized to perform: s3:GetObject on resource
#terraform#aws#s3#iam#backend#state#accessdenied

Lỗi Gặp Phải

Error refreshing state: AccessDenied: User: arn:aws:iam::123456789:user/ci is not authorized to perform: s3:GetObject on resource: arn:aws:s3:::my-terraform-state-bucket/path/to/terraform.tfstate
        status code: 403, request id: XXXXXXXXXXXXXXXX

Lỗi này thường xảy ra khi chạy terraform plan hoặc terraform apply trong CI/CD pipeline. Terraform cần tải file state hiện tại từ S3 trước khi thực hiện bất kỳ thao tác nào, nhưng IAM user hoặc role đang chạy job lại không có quyền đọc file đó.

Nguyên Nhân

Khi bạn cấu hình S3 backend, Terraform cần đọc và ghi file state trong mỗi lần chạy. Nếu IAM identity thực hiện việc chạy đó — có thể là CI user, EC2 instance profile, hay ECS task role — không có đủ quyền S3 trên bucket và đường dẫn tương ứng, AWS sẽ trả về lỗi 403 và Terraform hiển thị thông báo lỗi như trên.

Các nguyên nhân phổ biến:

  • CI/CD user mới được tạo nhưng chưa được gắn IAM policy cho state bucket.
  • S3 bucket policy bị thắt chặt sau khi Terraform đã được thiết lập (ví dụ: thêm explicit deny hoặc một điều kiện mới).
  • Backend config đã được chuyển sang bucket hoặc key path khác mà IAM identity hiện tại không có quyền truy cập.
  • Đang dùng IAM role không có inline policy hoặc attached policy bao phủ state bucket.

Cách Khắc Phục Từng Bước

1. Xác định đúng user và bucket

Thông báo lỗi đã cung cấp đầy đủ thông tin. Từ ví dụ trên:

  • IAM principal: arn:aws:iam::123456789:user/ci
  • Resource: đường dẫn S3 object trong thông báo lỗi

Kiểm tra lại backend config để đảm bảo bạn biết đúng bucket và key đang dùng:

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}

2. Tạo IAM policy với các quyền cần thiết

S3 backend của Terraform yêu cầu tối thiểu các S3 action sau:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "TerraformStateAccess",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-terraform-state-bucket",
        "arn:aws:s3:::my-terraform-state-bucket/*"
      ]
    }
  ]
}

Nếu bạn đang dùng DynamoDB để khóa state (nên dùng), hãy thêm statement sau:

    {
      "Sid": "TerraformStateLock",
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:DeleteItem",
        "dynamodb:DescribeTable"
      ],
      "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789:table/terraform-state-lock"
    }

Lưu nội dung trên thành file terraform-state-policy.json, sau đó tạo policy bằng CLI:

aws iam create-policy \
  --policy-name TerraformStateAccess \
  --policy-document file://terraform-state-policy.json

3. Gắn policy vào IAM user hoặc role

Với CI user (ci):

aws iam attach-user-policy \
  --user-name ci \
  --policy-arn arn:aws:iam::123456789:policy/TerraformStateAccess

Với IAM role (EC2 instance profile, ECS task role, GitHub Actions OIDC role, v.v.):

aws iam attach-role-policy \
  --role-name my-ci-role \
  --policy-arn arn:aws:iam::123456789:policy/TerraformStateAccess

4. Kiểm tra bucket policy hoặc SCP xung đột

Dù đã có IAM policy đúng, một S3 bucket policy chứa Deny tường minh — hoặc AWS Organizations Service Control Policy (SCP) — vẫn có thể ghi đè. Hãy kiểm tra bucket policy:

aws s3api get-bucket-policy \
  --bucket my-terraform-state-bucket \
  --query Policy \
  --output text | python3 -m json.tool

Tìm các statement có "Effect": "Deny" có thể đang chặn user của bạn. Nếu tìm thấy, hãy xóa đi hoặc thêm ngoại lệ cho CI principal của bạn.

Ngoài ra, hãy kiểm tra xem bucket có được mã hóa bằng customer-managed KMS key hay không. Nếu có, IAM identity của bạn cũng cần thêm quyền kms:Decryptkms:GenerateDataKey trên key đó.

{
  "Sid": "TerraformStateKMS",
  "Effect": "Allow",
  "Action": [
    "kms:Decrypt",
    "kms:GenerateDataKey"
  ],
  "Resource": "arn:aws:kms:ap-northeast-1:123456789:key/YOUR-KEY-ID"
}

Kiểm Tra Sau Khi Sửa

Kiểm tra quyền truy cập S3 trực tiếp bằng AWS CLI, sử dụng đúng credentials mà Terraform sẽ dùng:

# Nếu dùng profile cụ thể:
aws s3 cp s3://my-terraform-state-bucket/prod/terraform.tfstate /tmp/test.tfstate \
  --profile ci-user

# Nếu kiểm tra với assumed role:
aws sts assume-role \
  --role-arn arn:aws:iam::123456789:role/my-ci-role \
  --role-session-name test-session
# Sau đó export các credentials trả về và thử lại lệnh s3 cp

Nếu lệnh trên thành công, hãy chạy Terraform:

terraform init
terraform plan

Kết quả terraform plan chạy thành công (dù không có thay đổi nào) xác nhận rằng state đã được đọc đúng. Thông báo Error refreshing state sẽ không còn xuất hiện nữa.

Mẹo Nhanh

  • Giới hạn phạm vi policy thật chặt. Dùng ARN đầy đủ kèm key prefix (arn:aws:s3:::bucket/path/*) thay vì wildcard cho toàn bộ bucket, đặc biệt khi nhiều team dùng chung một bucket với các state path khác nhau.
  • Dùng IAM role thay vì long-lived user. Với GitHub Actions, hãy thiết lập OIDC. Với EC2/ECS, dùng instance profile. Access key tồn tại lâu dài cho ci user vừa tốn công xoay vòng vừa tiềm ẩn rủi ro bảo mật.
  • Dùng IAM Access Analyzer. Nếu không chắc cần những action nào, hãy bật Access Analyzer trên bucket và để nó tự tạo policy dựa trên các pattern truy cập thực tế.
  • Thay đổi policy mất vài giây để có hiệu lực. Nếu bạn gắn policy xong và thử ngay nhưng vẫn thất bại, hãy chờ 10–15 giây rồi thử lại — thay đổi IAM được cập nhật theo cơ chế eventual consistency.

Related Error Notes