エラーの内容
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
このエラーは、CI/CDパイプラインで terraform plan や terraform apply を実行したときに発生することが多いです。Terraformは処理を行う前にS3から現在のステートファイルを取得しようとしますが、ジョブを実行しているIAMユーザーまたはロールにそのファイルを読み取る権限がない場合に起こります。
発生原因
S3バックエンドを設定すると、Terraformは実行のたびにステートファイルを読み書きする必要があります。実行するIAMアイデンティティ(CIユーザー、EC2インスタンスプロファイル、ECSタスクロールなど)が対象バケットおよびパスに対して適切なS3権限を持っていない場合、AWSは403を返し、Terraformがこのエラーとして表示します。
よくある原因:
- 新しいCI/CDユーザーが作成されたが、ステートバケット用のIAMポリシーがアタッチされていない。
- TerraformのセットアップAfter、S3バケットポリシーが厳格化された(明示的な拒否や条件が追加されたなど)。
- バックエンドの設定が、現在のIAMアイデンティティではアクセスできない別のバケットまたはキーパスに移動された。
- ステートバケットをカバーするインラインポリシーまたはアタッチ済みポリシーを持たないIAMロールを使用している。
修正手順
1. 対象ユーザーとバケットを確認する
エラーメッセージに両方の情報が含まれています。上記の例では:
- IAMプリンシパル:
arn:aws:iam::123456789:user/ci - リソース:エラーメッセージ内のS3オブジェクトパス
バックエンドの設定を確認し、正しいバケットとキーを把握しておきましょう:
# 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. 必要な権限を持つIAMポリシーを作成する
TerraformのS3バックエンドには、最低限以下のS3アクションが必要です:
{
"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/*"
]
}
]
}
ステートロックにDynamoDBを使用している場合(推奨)、以下のステートメントも追加してください:
{
"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"
}
これを terraform-state-policy.json として保存し、CLIで作成します:
aws iam create-policy \
--policy-name TerraformStateAccess \
--policy-document file://terraform-state-policy.json
3. IAMユーザーまたはロールにポリシーをアタッチする
CIユーザー(ci)の場合:
aws iam attach-user-policy \
--user-name ci \
--policy-arn arn:aws:iam::123456789:policy/TerraformStateAccess
IAMロール(EC2インスタンスプロファイル、ECSタスクロール、GitHub Actions OIDCロールなど)の場合:
aws iam attach-role-policy \
--role-name my-ci-role \
--policy-arn arn:aws:iam::123456789:policy/TerraformStateAccess
4. バケットポリシーやSCPの競合を確認する
適切なIAMポリシーが設定されていても、S3バケットポリシーに明示的な Deny が含まれていたり、AWS OrganizationsのService Control Policy(SCP)が設定されていたりすると、それらが優先される場合があります。バケットポリシーを確認しましょう:
aws s3api get-bucket-policy \
--bucket my-terraform-state-bucket \
--query Policy \
--output text | python3 -m json.tool
ユーザーをブロックしている可能性のある "Effect": "Deny" のステートメントがないか確認してください。見つかった場合は、削除するか、CIプリンシパルの例外を追加してください。
また、バケットがカスタマーマネージドのKMSキーで暗号化されているかどうかも確認してください。その場合、IAMアイデンティティにはそのキーに対する kms:Decrypt と kms:GenerateDataKey の権限も必要です。
{
"Sid": "TerraformStateKMS",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:ap-northeast-1:123456789:key/YOUR-KEY-ID"
}
修正の確認
Terraformが使用するのと同じ認証情報を使い、AWS CLIで直接S3アクセスをテストします:
# 特定のプロファイルを使用する場合:
aws s3 cp s3://my-terraform-state-bucket/prod/terraform.tfstate /tmp/test.tfstate \
--profile ci-user
# ロールの引き受けをテストする場合:
aws sts assume-role \
--role-arn arn:aws:iam::123456789:role/my-ci-role \
--role-session-name test-session
# 返された認証情報をエクスポートして、s3 cp を再試行する
成功したら、Terraformを実行します:
terraform init
terraform plan
terraform plan の出力が正常に表示されれば(変更がない場合も含め)、ステートが正しく読み込まれたことを確認できます。Error refreshing state のメッセージは表示されなくなります。
補足Tips
- ポリシーのスコープを絞り込む。 バケット全体にワイルドカードを使うのではなく、キープレフィックスを含む完全なARN(
arn:aws:s3:::bucket/path/*)を使用しましょう。特に複数チームが同じバケットを異なるステートパスで共有している場合に重要です。 - 長期的なユーザーではなくIAMロールを使用する。 GitHub Actionsの場合はOIDCを設定し、EC2/ECSの場合はインスタンスプロファイルを使用しましょう。
ciユーザーの長期アクセスキーはローテーションの手間がかかり、セキュリティリスクにもなります。 - IAM Access Analyzerを活用する。 実際に必要なアクションが不明な場合は、バケットでAccess Analyzerを有効にすると、実際のアクセスパターンに基づいてポリシーを生成してくれます。
- ポリシーの反映には数秒かかる。 ポリシーをアタッチしてすぐに再試行しても失敗する場合は、10〜15秒待ってから再試行してください。IAMの変更は結果整合性のため、即時反映されないことがあります。

