Lambda(Amazon Redshift Data API)からRedshift severlessへリクエストした際に「permission denied for relation」エラー

2022.12.31

はじめに

こんばんは、データアナリティクス事業本部ビッグデータチームのkasamaです。
今年ももう大晦日ですね。年明ける前に躓いたエラーをブログ化することですっきりとした年越しをしたいと思います。

  • Lambda実行時のエラー
  • 前提条件
  • Lambda関数作成
  • ソースコード置き換え
  • タイムアウト秒数の変更
  • Lambdaロールに許可ポリシー追加
  • 最新のboto3のレイヤーの作成と追加
  • Lambda関数実行
  • Redshift権限調査
  • 対策
  • 再実行結果
  • 最後に

Lambda実行時のエラー

Lambda上で、Redshift Serverlessへのselect文をAmazon Redshift Data APIで実行したところ以下のエラーとなりました。

[ERROR] BadRequestException: 
{
    "Status": 400,
    "ErrorReason": "{'CreatedAt': datetime.datetime(2022, 12, 31, 11, 25, 54, 140000, tzinfo=tzlocal()), 'Duration': -1, 'Error': 'ERROR: permission denied for relation flightdata', 'HasResultSet': False, 'Id': '222ad592-07d2-468c-96dc-24e85b8d5aea', 'QueryString': 'SELECT count(*) FROM public.\"flightdata\"', 'RedshiftQueryId': 0, 'ResultRows': -1, 'ResultSize': -1, 'Status': 'FAILED', 'UpdatedAt': datetime.datetime(2022, 12, 31, 11, 25, 54, 608000, tzinfo=tzlocal()), 'WorkgroupName': 'name', 'ResponseMetadata': {'RequestId': '97742941-852a-465d-a25c-cdf094906949', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': '97742941-852a-465d-a25c-cdf094906949', 'content-type': 'application/x-amz-json-1.1', 'content-length': '375', 'date': 'Sat, 31 Dec 2022 11:25:55 GMT'}, 'RetryAttempts': 0}}"
}
Traceback (most recent call last):
  File "/var/task/lambda_function.py", line 69, in lambda_handler
    execute_select_query(
  File "/var/task/lambda_function.py", line 63, in execute_select_query
    raise BadRequestException(f"{statement}")

実行したLambdaにアタッチされたロールからRedshift serverless該当tableへの参照が許可されていなかったことが原因だと思われますが、今までの設定も含めて確認していきます。

前提条件

前提条件としてRedshift serverlessは事前に構築されていることとします。 まだの方は以下ブログの「Redshift Serverless構築」項目を参考にしてみてください。今回も以下ブログで構築したflightdata tableを使用します。
IICS CDI Amazon Redshift V2 ConnectorでRedshift Serverlessと接続できるか試してみた

Lambda関数作成

AWSのマネジメントコンソール上で、Lambda関数を作成していきます。「一から作成」を選択し、ランタイムに「python3.9」を選択、実行ロールは「基本的な Lambda アクセス権限で新しいロールを作成」を選択し、他はデフォルトの設定でLambda関数を作成します。

ソースコード置き換え

次に作成したLambda関数のソースコードを検証用に以下に置き換えます。

import os
import time
import json
import boto3


class LambdaException(Exception):
    def __init__(self, status_code: int, error_msg: str):
        self.status_code = status_code
        self.error_msg = error_msg

    def __str__(self):
        obj = {
            "Status": self.status_code,
            "ErrorReason": self.error_msg
        }
        return json.dumps(obj)


class BadRequestException(LambdaException):
    def __init__(self, error_msg: str):
        super().__init__(400, error_msg)


class InternalServerErrorException(LambdaException):
    def __init__(self, error_msg: str):
        super().__init__(500, error_msg)


# selectするメソッド
# --------------------------------------
def execute_select_query(work_group_name, db_name, target_table):
    redshift_client = boto3.client('redshift-data')
    select_query = "SELECT count(*) FROM public.\"%s\"" % (
        target_table)

    result = redshift_client.execute_statement(
        WorkgroupName=work_group_name,
        Database=db_name,
        Sql=select_query
    )
    start_time = time.time()
    # 実行IDを取得
    id = result['Id']

    # クエリが終わるのを待つ
    statement = ''
    status = ''
    while status != 'FINISHED' and status != 'FAILED' and status != 'ABORTED':
        statement = redshift_client.describe_statement(Id=id)
        status = statement['Status']
        print("Status:", status)
        time.sleep(1)
    end_time = time.time()
    print('process_time:', end_time-start_time)
    print(json.dumps(statement, indent=4, default=str))
    # 結果の表示
    try:
        statement = redshift_client.get_statement_result(Id=id)
        print(json.dumps(statement, indent=4, default=str))
    except:
        if status == 'FAILED' or status == 'ABORTED':
            raise BadRequestException(f"{statement}")


def lambda_handler(event, context):
    try:
        print("event:", event)
        execute_select_query(
            event["work_group_name"],
            event["db_name"],
            event["target_table"]
        )
        return {
            'Status': 200,
            'event': json.dumps({'Status': 'success', 'ErrorReason': 'None'})
        }
    except Exception as e:
        print(e)
        if e.__class__.__name__ == 'BadRequestException':
            raise
        raise InternalServerErrorException(str(e))

今回実行するsql文はtableデータ件数をcountしたもの取得するものとしています。

タイムアウト秒数の変更

デフォルトの設定だとタイムアウトが3秒なので、Redshiftへアクセス時にタイムアウトが発生する可能性があります。ですので「設定」の「一般設定」から9秒に修正します。

Lambdaロールに許可ポリシー追加

デフォルトのポリシーだけでは、Redshift Serverlessデータを参照できないので、許可ポリシーを追加します。 「設定」→「アクセス権限」からロール名をクリックします。

遷移したIAMの画面で、「許可を追加」→「ポリシーをアタッチ」から「AmazonRedshiftFullAccess」をアタッチします。

最新のboto3のレイヤーの作成と追加

2022/12現在、Lambdaのデフォルトのboto3ですと、Redshift severlessへリクエストできないので最新のboto3をインストールする必要があります。以下、AWS公式の「最新の Boto 3 バージョンを使用する Lambda レイヤーを作成する」方法がありますのでそちらをご参照し、項目3番でzipファイルまで作成してください。
Python (Boto 3) Lambda 関数からの「不明なサービス」、「パラメータの検証に失敗しました」、「オブジェクトには属性がありません」というエラーを解決するにはどうすればよいですか?

今回は互換性のあるランタイムにPython3.9を指定する必要がありますので、AWSマネコン上の「Lambda」→「レイヤー 」→「レイヤーの作成」から作成します。
zipファイルに先ほど作成したzipファイルを選択し、互換性のあるランタイムに「python3.9」を選択し、作成します。

作成したレイヤーは、該当のLambda関数の「レイヤーの追加」から追加します。

Lambda関数実行

引数に「work_group_name」「db_name」「target_table」を指定してテスト実行します。

実行結果は、最初に記載したエラー内容になります。

Redshift権限調査

以下のブログでユーザー単位での権限参照するsql文がありました。
AWS Redshift ユーザ単位で各テーブルに対する権限の有無を確認してみた

今回は、以下のようにtablenameで検索するようにします。

select
  usename
  , schemaname
  , tablename
  , has_table_privilege(usename, schemaname || '.' || tablename, 'select') as select
  , has_table_privilege(usename, schemaname || '.' || tablename, 'insert') as insert
  , has_table_privilege(usename, schemaname || '.' || tablename, 'update') as update
  , has_table_privilege(usename, schemaname || '.' || tablename, 'delete') as delete
  , has_table_privilege(usename, schemaname || '.' || tablename, 'references') as references
  , has_schema_privilege(usename, schemaname, 'usage') as usage
from
  pg_tables, pg_user
  where tablename = 'flightdata'
;

実行結果は以下のようになりました。「IAMR:redshift-select-blog-role-1vysetlj」がLambda上から実行した際に使用されるuserですが、「select」が「false」となっていることがわかります。

対策

以下のGRANT文で対象tableのselect権限を対象ユーザーに付与します。

GRANT  SELECT ON TABLE  flightdata TO "IAMR:redshift-select-blog-role-1vysetlj"

再実行結果

ステータス200で成功しました!

Test Event Name
test

Response
{
  "Status": 200,
  "event": "{\"Status\": \"success\", \"ErrorReason\": \"None\"}"
}

データ件数もreturnされることを確認できています!

最後に

何とかエラーを解決して、すっきりした気持ちで来年を迎えられそうです。来年はもっと多くのブログを書いて少しでも多くの方にお役に立てればと思います。それでは良いお年を!