NRIネットコム社員が様々な視点で、日々の気づきやナレッジを発信するメディアです

注目のタグ

    AWS CDKの落とし穴!? grantメソッドに泣かされたお話

    本記事は  IaCウィーク  9日目の記事です。
    ⚙️  8日目  ▶▶ 本記事 ▶▶  10日目  💻

    はじめに

    こんにちは。入社3年目の深瀬です。

    前回のブログ投稿から、1年と10ヶ月が経ったようです。時の流れ、早すぎませんか?
    前回は、Amazon API Gatewayのテスト作成に関する体験記を記載しました。あの頃から変わらず、今もなおAWS CDK(※以下CDK)などのIaCを用いて日々業務に励んでおります。

    tech.nri-net.com

    今回はIaCウィークということで、入社3年目に遭遇したCDKの落とし穴についてお話したいと思います。 皆さんも同じ落とし穴にはまってしまわないよう、ぜひご一読ください。

    AWS CDKとは

    まず最初に、CDKって何だろう?となった方に向けてざっくりご説明します。

    AWS CDK(AWS Cloud Development Kit)は、PythonやTypeScriptなどのプログラミング言語を用いてAWSリソースを構築できる、ソフトウェア開発フレームワークです。 CDKはAWS CloudFormationと統合してインフラストラクチャをデプロイおよびプロビジョニングするので、エラー時の自動ロールバックやドリフト検出の実行などが可能となっています。

    ちょっと難しいことを記載しましたが、要は「コードを書いてコマンドを打つだけで、AWSリソースが構築できちゃう!」といった優れものです。

    docs.aws.amazon.com

    CDKの落とし穴

    それでは、私がハマったCDKの落とし穴についてお話ししていきます。

    今回私がハマったのは、CDKのgrantメソッドです。
    とあるIAMロールに、いつも通り権限追加をしていたところ・・・唐突なデプロイ失敗!
    なんだろう?と思ったら、こんなエラー。

    6:46:02 AM | CREATE_FAILED        | AWS::IAM::ManagedPolicy | xxxxOverflowPolicyxxxxxxxxx
    Resource handler returned message: "Cannot exceed quota for PoliciesPerRole: 10 (Service: Iam, Status Code: 409, Request ID: 18d98b3f-a8a4-4f9c-8c04-7172996cb1ab) (SDK Attempt Count: 1)" (RequestToken: 6abd153f-61a4-70a8-07cc-067bcb805186, HandlerErrorCode: ServiceLimitExceeded)
    
    ❌  xxx-stack failed: The stack named xxx-stack failed to deploy: UPDATE_ROLLBACK_COMPLETE: Resource handler returned message: "Cannot exceed quota for PoliciesPerRole: 10 (Service: Iam, Status Code: 409, Request ID: 18d98b3f-a8a4-4f9c-8c04-7172996cb1ab) (SDK Attempt Count: 1)" (RequestToken: 6abd153f-61a4-70a8-07cc-067bcb805186, HandlerErrorCode: ServiceLimitExceeded)

    IAMロールに付与できるIAM管理ポリシー数のサービスクォータ(上限値)に引っかかっちゃった、というものです。

    こちらの内容を詳細にお話しする前に、grantメソッドについて少しお話ししていきます。

    grantメソッドとは

    CDKには、grantメソッドというものが存在します。 こちらは、Amazon S3やAmazon DynamoDBなどのリソースへアクセスする際に必要な権限を、CDKが"自動で"付与してくれるメソッドです。 例えば、Amazon S3だと grantReadメソッドや grantPutメソッドなど、さまざまなgrantメソッドが存在します。

    docs.aws.amazon.com

    こちら、すごく便利なメソッドなんです。
    例えば、grant-test-bucket というS3バケットに対してGetObject権限とPutObject権限を付与したい場合を考えてみましょう。

    grantメソッドを使用しない場合

    grantメソッドを使用しない場合(明示的に記載する場合)のコードはこちらになります。

    # Create IAM Role
    role = iam.Role(
        self,
        role_name="grant-test-role",
        assumed_by=iam.ServicePrincipal("s3.amazonaws.com"),
    )
    
    # Create IAM Managed Policy
    iam.ManagedPolicy(
        self,
        managed_policy_name="not-grant-policy",
        statements=[
            iam.PolicyStatement(
                effect=iam.Effect.ALLOW,
                actions = [
                    "s3:GetObject",
                    "s3:PutObject",
                ],
                resources=["arn:aws:s3:::grant-test-bucket"],
            ),
        ],
        roles=[role],
    )
    

    IAMロールとIAMポリシーを作成して、ポリシーのactions部分にGetObject権限とPutObject権限を記載したコードとなりました。 GetObject権限とPutObject権限を付与するために、それなりのコード量が必要ですね。

    grantメソッドを使用した場合

    対して、grantメソッドを使用した場合のコードがこちらになります。

    # Create IAM Role
    role = iam.Role(
        self,
        role_name="grant-test-role",
        assumed_by=iam.ServicePrincipal("s3.amazonaws.com"),
    )
    
    # grant to s3
    bucket_name = "grant-test-bucket"
    bucket = s3.Bucket.from_bucket_name(self, id=bucket_name, bucket_name=bucket_name)
    bucket.grant_read(role)
    bucket.grant_put(role)
    

    え!短すぎませんか?

    grantメソッドを使用しない場合は(IAMロールの作成を除いて)15行となっていましたが、grantメソッドを使用するとたったの4行で権限を付与することができました。 これは、grantメソッドを使用するとCDKが自動でIAMポリシーを作成してくれるため、明示的にIAMポリシーを作成する必要がなく、その結果大幅にコード量を削減できたのです。

    grantメソッドには罠がある

    ここまで見ると、grantメソッドは「自動で権限付与してくれるし、コード量も削減できるし、めっちゃ良いやつじゃん!」て思われるでしょう。 ちっちっち!甘いですね。😈

    ここからが本題、私がハマったgrantメソッドの罠について詳細にお話しします。
    先にも述べましたが、ある日突然サービスクォータに引っかかってしまったのです。 皆さん、ご存知でしたか?AWSには様々なサービスクォータが存在しますが、IAMロールに付与できるIAM管理ポリシー数にもサービスクォータが存在するんです。 デフォルトでは、1つのIAMロールに10個のIAM管理ポリシーが付与できるそうです。 これは、クォータの増加申請によって最大20個まで増やすことが可能です。

    repost.aws

    ・・・え?grantメソッドと何が関係あるの?
    と思われた方もいらっしゃるかと思います。それが、grantメソッドのとある挙動に関係があるんです!!

    grantメソッドの挙動

    実は、今回の問題が発生するまでも、grantメソッドの挙動に対してはなんとなーく違和感があったんです。 私が違和感を覚えていた挙動は2つあります。

    1つ目の挙動:権限は広めに付けるよ

    grantメソッドは、最小権限ではなくs3:GetBucket* のように広めの権限で付与してくれます。 先ほどのコードだと、下記の権限が付与されます。

    • grant_read
      • s3:GetBucket*
      • s3:GetObject*
      • s3:List*
    • grant_put
      • s3:Abort*
      • s3:PutObject
      • s3:PutObjectLegalHold
      • s3:PutObjectRetention
      • s3:PutObjectTagging
      • s3:PutObjectVersionTagging

    え~!そんなにたくさん付けちゃうの!?って感じですよね。 コード量の削減はできたけど、IAMポリシーの文字数はとっても増えちゃいました。 また、最小権限にも反しているので、付与したい権限が明確で、grantメソッドだと過剰になっちゃう場合はおすすめできないですね。

    2つ目の挙動:いい感じに、ポリシー分割するよ

    grantメソッドは自動で権限を付与してくれます。ということは、自動でIAMポリシーを作成してくれているのです。 IAMポリシーにもサービスクォータは存在していて、明示的にポリシーを作成する場合は特に気を付けたい「文字数制限」なんかが有名ですね。 この文字数制限を突破しないように、grantメソッドは良しなにポリシーを分割して作成してくれるのです。

    ところがどっこい!この優しさが今回、私を泣かせました。
    実は、grantメソッドはIAM管理ポリシーの文字数上限である6,144文字や、IAMインラインポリシーの文字数上限である10,240文字ギリギリを狙うのではなく、3,000文字や4,000文字など、まだまだ文字数にゆとりのある状態でもポリシーを分割しちゃうのです。

    ちょっとだけ検証

    ということで、実際に何文字でポリシーが分割されちゃうのかちょっとだけ検証してみました。
    検証方法は単純です。grantメソッドで権限追加するS3バケットの数を増やしていきます。すると、IAMポリシーの文字数が3,576文字になるよ!ってところでポリシーが分割される差分結果となりました。

    Resources
    [+] AWS::IAM::ManagedPolicy grant-test-role/OverflowPolicy1 granttestroleOverflowPolicy1DCDE308A 
    [~] AWS::IAM::Policy grant-test-role/DefaultPolicy granttestroleDefaultPolicy2AAED2F7 
    ~~~

    grantメソッドは、最初はDefaultPolicy(=インラインポリシー)で作成しますが、とある段階でOverflowPolicy{数字}として新たなIAM管理ポリシーを作成していきます。 結果、DefaultPolicyは3,071文字、OverflowPolicy1は460文字となりました。まだまだDefaultPolicyに収められる文字数なのに…リソースの無駄遣い感が半端ないですね。

    もう少しだけ検証してみます。
    次はOverflowPolicy2が作成されるタイミングを確認してみます。すると、DefaultPolicyが3,240文字、OverflowPolicy1が2,150文字で、新たに169文字増えるよ!ってところでポリシーが分割される差分結果となりました。

    Resources
    [+] AWS::IAM::ManagedPolicy grant-test-role/OverflowPolicy2 granttestroleOverflowPolicy2D712E556 
    [~] AWS::IAM::Policy grant-test-role/DefaultPolicy granttestroleDefaultPolicy2AAED2F7 
    ~~~
    [~] AWS::IAM::ManagedPolicy grant-test-role/OverflowPolicy1 granttestroleOverflowPolicy1DCDE308A 
    ~~~

    もう分割しちゃうの?なんてもったいない…!って感じですね。 結果、DefaultPolicyは3,128文字、OverflowPolicy1は2,207文字、OverflowPolicy2は348文字となりました。 OverflowPolicy1だけではなく、DefaultPolicyの中身もちょこちょこ入れ替わっているようですね。

    以上のように、ちょっとだけ検証してみましたが、どういう基準でポリシー分割が実施されるのかよくわかりませんでした。なんとなく、文字数上限の3割前後の値になるとポリシー分割するのかな?なんて思いました。もう少し検証することで規則をつかめるかもしれないですが、一旦今回はここまで…。

    対応策

    ということで、今回私がハマったのは「権限を広めに付ける」「良しなにポリシーを分割する」というgrantメソッドの2つの挙動によって、普段実施していた権限付与作業が失敗してしまう、というものでした。 こちら、どうやって回避すべきなのか…なかなか難しいですよね。

    今回私たちが取った対応策は、「grantメソッドを卒業し、明示的に権限を付与するように変更する」です。 というのも、検証結果からもわかるように、grantメソッドの挙動「良しなにポリシーを分割する」はなかなか把握しづらいんです。もちろん、差分確認コマンド cdk diff でポリシーが増えることは把握できます。なので、サービスクォータの存在を知っていて、かつ、各IAMロールに付与されているIAM管理ポリシー数を把握できているなら、回避は可能かもしれません。
    ですが、どういう基準で分割するのか分からない、そんな曖昧な機能を採用するよりも、こちらできちんと設計し、付与しているポリシー数やポリシーの文字数をしっかり管理する方が良い、となりました。

    明示的に記載することで、コード量の増加やタイポ、設計ミスによるIAMポリシーの文字数上限超過など、デメリットもあります。どちらの方法を採用するのが良いか、各々でしっかり見極める必要がありますね。

    さいごに

    ここまで、CDKのgrantメソッドに泣かされたお話をしてきましたが、いかがだったでしょうか?
    grantメソッドを使用していたことで、IAMロールに付与できるIAM管理ポリシー数のサービスクォータに引っかかるなんて、どんだけのリソースに権限付与してんねん!て話ですよね。
    この経験を機に、権限付与の仕方について考え直すこととなり、システムとしてはより改善された気がしています。それに、私自身すごく勉強になりました。なので、結果的には良かったのかも。

    ということで、皆さんもgrantメソッドにはお気を付けください。もしポリシー分割の基準が分かったら、ぜひぜひ教えてください!笑

    それでは、最後までお読みいただきありがとうございました。
    これからもCDKと共にズンズン成長していきたいです。がんばりましょう。

    執筆者:深瀬仁菜 インフラエンジニア