もりはやメモφ(・ω・ )

インフラなエンジニアからSREへ

AWS S3でバケット自体も中身も削除禁止のバケットを作る

「Glueを使ってRDSからS3にコールドデータを出力し、Athenaでクエリ実行できるようにする」みたいな検証を行なっていて、間違ってもS3のデータを削除されては困るという要件が出てきて調べたのでメモ。

S3のPolicyでできる

結論を先に書くと以下のPolicyを設定するだけでした。Administratorさんでも消せない安心・安全なS3バケットの出来上がり!!><

  • 2019-02-10追記: ただしこの設定だとGlueのジョブもエラーになることが分かったのでConditionを使って特定ロールを指定する方法を追記しました

<Your Bucket Name> のところを対象のバケットネームに変更します。

{
    "Version": "2012-10-17",
    "Id": "Policy1548909673384",
    "Statement": [
        {
            "Sid": "Stmt1548909665435",
            "Effect": "Deny",
            "Principal": "*",
            "Action": [
                "s3:DeleteBucket",
                "s3:DeleteBucketPolicy",
                "s3:DeleteBucketWebsite"
            ],
            "Resource": "arn:aws:s3:::<Your Bucket Name>"
        },
        {
            "Sid": "Stmt1548909665435",
            "Effect": "Deny",
            "Principal": "*",
            "Action": [
                "s3:DeleteObject",
                "s3:DeleteObjectTagging",
                "s3:DeleteObjectVersion",
                "s3:DeleteObjectVersionTagging"
            ],
            "Resource": "arn:aws:s3:::<Your Bucket Name>/*"
        }
    ]
}

ポイント

バケット用のポリシーと、オブジェクト用のポリシーを分ける必要があります。 StackOverflowにも記載がありました。

  • s3:xxxxBucket -> Resource でバケットネームを指定
  • s3:xxxxObject -> Resource でバケットネーム + /* を指定

ダメな例が以下です。この場合Policy保存時にエラー Action does not apply to any resource(s) in statement が発生し、そもそもPolicyを設定できません。

{
    "Version": "2012-10-17",
    "Id": "Policy1548909673384",
    "Statement": [
        {
            "Sid": "Stmt1548909665435",
            "Effect": "Deny",
            "Principal": "*",
            "Action": [
                "s3:DeleteBucket",
                "s3:DeleteBucketPolicy",
                "s3:DeleteBucketWebsite",
                "s3:DeleteObject",
                "s3:DeleteObjectTagging",
                "s3:DeleteObjectVersion",
                "s3:DeleteObjectVersionTagging"
            ],
            "Resource": "arn:aws:s3:::<Your Bucket Name>"
        }
    ]
}

その他

助言を頂いた@rakiに感謝です!!><

Glueのロールだけには許可をする必要があった

上述した設定をしたS3バケットに対し、意気揚々とGlueのジョブを実行したところ以下のエラーが発生しました。

An error occurred while calling o132.pyWriteDynamicFrame. Access Denied (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied; Request ID: 326DAD746xxxxxxx)

修正して以下の要件を満たしたポリシーを記載します。

  • Glueが使用するRoleで正しくファイル出力ができる
  • Glueが使用するRoleで内部動作で作成される _temporary ディレクトリをGlueのRoleが削除できる*1
  • 人間は消せない
{
    "Version": "2012-10-17",
    "Id": "Policy1548909673384",
    "Statement": [
        {
            "Sid": "Stmt1548909665435",
            "Effect": "Deny",
            "Principal": "*",
            "Action": [
                "s3:DeleteBucket",
                "s3:DeleteBucketPolicy",
                "s3:DeleteBucketWebsite"
            ],
            "Resource": "arn:aws:s3:::<Your Bucket Name>",
            "Condition": {
                "StringNotLike": {
                    "aws:userid": "<Your Role ID>:*"
                }
            }
        },
        {
            "Sid": "Stmt1548909665435",
            "Effect": "Deny",
            "Principal": "*",
            "Action": [
                "s3:DeleteObject",
                "s3:DeleteObjectTagging",
                "s3:DeleteObjectVersion",
                "s3:DeleteObjectVersionTagging"
            ],
            "Resource": "arn:aws:s3:::<Your Bucket Name>/*",
            "Condition": {
                "StringNotLike": {
                    "aws:userid": "<Your Role ID>*"
                }
            }
        }
    ]
}

エラーの原因を調査した結果、一時ディレクトの削除に失敗している様でした。出力先のS3バケット内に _temporary と言うディレクトリが残っており、Glueの仕様として出力データが小さくても必ず作成されるディレクトリの用です。つまりGlueが動作するIAM Roleに対して出力先のS3バケットに対しDeleteObjectの権限が必要であることが分かりました。

公式ドキュメント:AWS グローバル条件コンテキストキーなどを見ると 明示的なDenyは全てに優先する とあり、"Effect": "Deny" かつ "Principal": "*", とした時点で全てのIAMが拒否されている状況となります。

つまり以下の様な特定のIAM Roleに対して明示的なAllowを追加したところで、明示的なDenyが全てを拒否してしまうことが分かりました。

        {
            "Sid": "Stmt1548909665435",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::<Your AWS Account ID>:role/<Your Role Name>"
            },
            "Action": "*",
            "Resource": "arn:aws:s3:::<Your Bucket Name>/*"
        },

そして調べること数分、Qiitaの [AWS] S3バケットポリシーで、特定のIAMロールだけがバケットにアクセス出来るようにする。の記事にやりたいことがそのまま書かれており、大変助かりました。

具体的には以下の2点

  • aws iam get-role --role-name <Your Role Name でRoleのIDを取得する*2
  • Condition 句で StringNotLike を使用し、aws:userid に上記aws iam get-roleで調べたIDを記述する

抜粋すると下の部分です。Role IDはARNではなく AROAJKU2FGZAVxxxxxxxx といった見慣れないIDになります。

...省略..
            "Condition": {
                "StringNotLike": {
                    "aws:userid": "<Your Role ID>*"
                }
            }
...省略..

このConditionを使う対応を知ることができたのはQiitaの 3イイネ の記事*3のおかげで、公式ドキュメントを読むだけではなかなかたどり着けなかったと想定されます。ノウハウを記載してくれたn-ishidaに感謝を捧げると共に、私もどんどんアウトプットして行こうという気持ちを新たにしました。

  • 2019-02-10 追記 Versioning有効が前提ですが、WORM(Write Once Read Many)なポリシーがS3でリリース済みのため、こっちを使う方がポリシーを利用するよりシンプルかもしれない....です。

*1:Glueの仕様として_tempraryというディレクトリを作成し、最後に削除するらしい

*2:このRole IDはWebコンソールからは確認できないらしい。ARNではないことに注意

*3:2019-02-10時点