michimani.net

AWS Chalice で必要な IAM ポリシーが正しく作成されなかったときの話

2020-02-26

AWS Chalice でサーバレスアプリケーションを実装しているときに、自動で作成されるはずの IAM ポリシーが作成されなかった場面があったので、そのときの話です。

目次

AWS Chalice とは

AWS Chalice とは、 Python でサーバレスアプリケーションを作成するためのマイクロフレームワークです。AWS 環境上でサーバレスアプリケーションを構築するフレームワークには AWS SAM Serverless Framework などがありますが、 Chalice はそれらに比べて非常にシンプルで、 AWS でのサーバレスアプリケーション開発を始めたいという人にとっては導入にもってこいのフレームワークです。

Chalice で自動的に作成される AWS リソースは API Gateway の API と Lambda の関数、 IAM ロールとポリシーのみです。他のフレームワークではその他のリソースも作成できたりしますが、 Chalice では必要最低限のリソースのみが作成されます。また、開発言語も Python 固定なので、それも含めてシンプルでとっつきやすいフレームワークになっています。

今回はいわゆるチュートリアル的な話ではなくて、 Chalice で自動作成される IAM ポリシーに関する話です。チュートリアル的な話はまた別で書きたいと思います。

ソースコードを解析して必要なポリシーを自動的に付与

では、簡単な REST API を実装して、 Chalice によって作成される IAM ポリシーを確認してみます。

REST API の内容としては、 DynamoDB に存在する Thread テーブルからデータを取得するというものです。データに関しては、下記の DynamoDB サンプルデータを使用します。

AWS Chalice では、基本的に app.py に API の処理を実装していきます。そして chalice deploy コマンドで AWS 環境にデプロイします。デプロイ時には、 API Gateway の API 、 Lambda 関数、 IAM ロール を自動で作成してくれます。この際に作成される IAM ロールには、ソースコードを解析して、必要なポリシーをよしなに付与してくれます。

今回ハマったポイントとしては、実装方法によっては、 本来ソースコードを解析して必要なポリシーを持つ IAM ロールを作成してくれるはずが、権限が足りない場合が起こりうる ということです。なので、正しく作成される場合とそうでない場合の実装方法の違いを比較していきます。

ソースコードからよしなに自動作成される場合

まずは正しく必要なポリシーが付与される場合の実装です。

app.py を下記のような内容で実装します。

from chalice import Chalice, Response
import boto3
import urllib.parse

app = Chalice(app_name='chalice-sample')
dynamo_client = boto3.client('dynamodb')

TABLE_NAME = 'Thread'


@app.route('/threads', methods=['GET'])
def get_threads():
    items = dynamo_client.scan(TableName=TABLE_NAME)

    return Response(body=items['Items'],
                    status_code=200,
                    headers={'Content-Type': 'application/json'})


@app.route('/threads/{forum_name_url}', methods=['GET'])
def get_forum_threads(forum_name_url):
    forum_name = urllib.parse.unquote(forum_name_url)
    items = dynamo_client.query(
        TableName=TABLE_NAME,
        KeyConditions={
            'ForumName': {
                'AttributeValueList': [
                    {'S': forum_name}
                ],
                'ComparisonOperator': 'EQ'
            }
        })

    return Response(body=items['Items'],
                    status_code=200,
                    headers={'Content-Type': 'application/json'})

API のエンドポイントとしては GET /threadsGET /threads/{forum_name} を用意していて、前者はテーブル内のデータ前取得、後者はパーティションキーである ForumName を指定したデータの取得を行います。
それぞれの処理の中で boto3 を用いて DynamoDB に対する ScanQuery の API を呼んでいます。

この実装状態で chalice deploy を実行すると、下記のようなポリシーをもつ IAM ロールが作成されます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "dynamodb:Query",
                "dynamodb:Scan"
            ],
            "Resource": [
                "*"
            ],
            "Sid": "ae5b14e8a77347428XXXXXXXXXXXXX"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

実装通り、 dynamodb:Querydynamodb:Scan の権限が付与されています。

ソースコードからよしなに自動作成されない場合

続いては、必要なポリシーが正しく作成されない場合の実装です。
app.py は下記のように実装します。

from chalice import Chalice, Response
import boto3
import urllib.parse

app = Chalice(app_name='chalice-sample')
dynamo_resource = boto3.resource('dynamodb')

TABLE_NAME = 'Thread'


@app.route('/threads', methods=['GET'])
def get_threads():
    items = dynamo_resource.Table(TABLE_NAME).scan()

    return Response(body=items['Items'],
                    status_code=200,
                    headers={'Content-Type': 'application/json'})


@app.route('/threads/{forum_name_url}', methods=['GET'])
def get_forum_threads(forum_name_url):
    forum_name = urllib.parse.unquote(forum_name_url)
    items = dynamo_resource.Table(TABLE_NAME).query(KeyConditions={
        'ForumName': {
            'AttributeValueList': [
                forum_name
            ],
            'ComparisonOperator': 'EQ'
        }
    })

    return Response(body=items['Items'],
                    status_code=200,
                    headers={'Content-Type': 'application/json'})

API のエンドポイントとしては先ほどと同様に、 GET /threadsGET /threads/{forum_name} を用意していて、前者はテーブル内のデータ前取得、後者はパーティションキーである ForumName を指定したデータの取得を行います。

実装方法で先ほどと異なる点は、 boto3 の Client class ではなく Resource class を使用した実装になっているという点です。
この場合、 chalice deploy を実行すると、下記のようなポリシーをもつ IAM ロールが作成されます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        }
    ]
}

はい。CloudWatch Logs に対するポリシーしか付与されていません。
これではデプロイされた API を実行しても、権限が足りずにエラーになってしまいます。

botocore.exceptions.ClientError: An error occurred (AccessDeniedException) when calling the Query operation: User: 
arn:aws:sts::123456789012:assumed-role/chalice-sample-dev/chalice-sample-dev is not authorized to perform: 
dynamodb:Query on resource: arn:aws:dynamodb:ap-northeast-1:123456789012:table/Thread

AWC Chalice のソースコード解析は Client class での実装時のみ有効

上記のように作成されるポリシーに差異が生じるのは、実装を boto3 の Client class を使うか、 Resource class を使うかが原因になっています。

AWS Chalice ではデプロイ時のソースコード解析に、 boto3 の Client class の API を呼んでいるかどうかを見ています。

Chalice のソースコードではこの辺りです。

Client class と Resource class の性質の違いを考えるとこうなっていることは理解できます。

Client class は AWS の各サービスに対する API をほぼ網羅しているので、必要なポリシーが判定しやすいです。ただし、低レベルな API のためコール時には細かいパラメータを指定したり、レスポンスの加工が必要になったりと、実装時には手間がかかることが多いです。

一方で Resource class は Client class を高レベルで抽象化した class で、同じ処理をするにしてもコール時のパラメータやレスポンスが扱いやすい形になっているため、実装フレンドリーだと言えます。

Client と Resource のレスポンスの違い

では実際にそれぞれの実装で正しくデータが取得できた場合のレスポンスを比較してみます。対象のエンドポイントは、 GET /threads/Amazon%20S3 として比較します。

まずは Client class を用いた実装の場合のレスポンス。

[
    {
        "Answered": {
            "N": "0"
        },
        "ForumName": {
            "S": "Amazon S3"
        },
        "LastPostedBy": {
            "S": "User A"
        },
        "LastPostedDateTime": {
            "S": "2015-09-29T19:58:22.514Z"
        },
        "Message": {
            "S": "S3 thread 1 message"
        },
        "Replies": {
            "N": "0"
        },
        "Subject": {
            "S": "S3 Thread 1"
        },
        "Tags": {
            "L": [
                {
                    "S": "largeobjects"
                },
                {
                    "S": "multipart upload"
                }
            ]
        },
        "Views": {
            "N": "0"
        }
    }
]

各項目の値に対して、型を示す情報も入った状態でレスポンスが返ってきます。

続いて Resource class を用いた場合のレスポンス。

[
    {
        "Answered": 0.0,
        "ForumName": "Amazon S3",
        "LastPostedBy": "User A",
        "LastPostedDateTime": "2015-09-29T19:58:22.514Z",
        "Message": "S3 thread 1 message",
        "Replies": 0.0,
        "Subject": "S3 Thread 1",
        "Tags": [
            "largeobjects",
            "multipart upload"
        ],
        "Views": 0.0
    }
]

自然なデータ構造で返ってきます。実装時では、間違いなくこちらの形の方が扱いやすいです。

今回は DynamoDB に対する操作ですが、他のリソースに対する操作でも Resouce class を用いたほうが実装は楽になるでしょう。

AWS Chalice で作成される IAM ロールにを手動でポリシーをアタッチする

自動で作成される IAM ロールに必要なポリシーが付与されないのは困るけど、実装は楽な方がいいからな… という悩みが出てくると思いますが、 AWS Chalice では、自動で作成される IAM ロールに手動で作成したポリシーをアタッチすることができます。
方法は簡単で、 .chalice/config.json で下記のような設定をします。

{
  "version": "2.0",
  "app_name": "chalice-sample",
  "stages": {
    "dev": {
      "api_gateway_stage": "api",
      "autogen_policy": false,
      "iam_policy_file": "custom-policy.json"
    }
  }
}

"autogen_policy": false でポリシーの自動生成をオフにして、 "iam_policy_file": "custom-policy.json" で手動で作成したポリシーの JSON ファイルを指定します。今回であれば下記のような JSON ファイルを作成します。

{
  "Version": "2012-10-17",
  "Statement": [
      {
          "Effect": "Allow",
          "Action": [
              "dynamodb:Query",
              "dynamodb:Scan"
          ],
          "Resource": [
              "*"
          ]
      },
      {
          "Effect": "Allow",
          "Action": [
              "logs:CreateLogGroup",
              "logs:CreateLogStream",
              "logs:PutLogEvents"
          ],
          "Resource": "arn:aws:logs:*:*:*"
      }
  ]
}

この状態で chalice deploy を実行すると、自動で作成される IAM ロールには上記のポリシーがアタッチされます。

自分で必要な権限を考える必要はありますが、 Resource class を用いた実装をする場合にはこのような形でポリシーを指定する形になります。

まとめ

AWS Chalice でサーバレスアプリケーションを実装する際に、必要な IAM ポリシーが自動で作成されなかった場面があったという話でした。

最初は なんで Resouce class だとポリシー作成されへんねん って思ってたんですが、 Client class との抽象度の違いとかを考えると納得できました。必要なポリシーを考えるのって AWS 触り始めたばかりだとよくわからんっていう感じですけど、自分で必要なポリシーを調べて都度足していくっていう作業は実は AWS 触る上では重要なことなんじゃないかなと思ってます。最初のうちは とりあえず Full Access で ってやりがちですけど、そうやって必要な権限を知って絞っていく作業が地味に大事だなと、最近感じてます。

冒頭にも書いたように、 AWS Chalice はとてもシンプルなサーバレスフレームワークなので、とりあえず AWS でサーバレスやりたい とか、 Python でサーバレスやりたい という方にはおすすめのフレームワークです。

参考


comments powered by Disqus