AWS LambdaのIAMポリシーとリソースポリシーを理解しよう

こんにちは、虎塚です。

先日のAWSアップデートで、Lambda関数ポリシーがAWS Management Consoleに表示されるようになりました。

Lambda関数のアクセス権限設定には、このようなLambda関数ポリシーと呼ばれるリソースベースのポリシーと、IAMポリシーと呼ばれるアイデンティティベースのポリシーがあります。この記事では、これら2種類のポリシーについて整理します。

2種類のポリシーの概要

IAMポリシー (アイデンティティベースのポリシー)

アイデンティティベースのポリシーは、IAMポリシーや実行ロールとも呼ばれます。この記事では、IAMポリシーと書くことにします。

Lambda関数を作る時、実行ロールとして1個のIAMロールを必ず指定します。実行ロールに適用するポリシーが、この記事でIAMポリシーと呼ぶものにあたります。

実行ロールのIAMポリシーには、AWS LambdaにLambda関数の実行をまかせるための信頼ポリシーや、Lambda関数がほかのAWSリソースにアクセスすることを許可するためのアクセスポリシーが含まれます。

リソースポリシー (リソースベースのポリシー)

リソースベースのポリシーは、Lambda関数ポリシーやリソースポリシーとも呼ばれます。この記事では、リソースポリシーと書くことにします。

Lambda関数を定義するとき、リソースポリシーの指定は必須ではありません。指定するには、AWS APIのAddPermissionを使います。

リソースポリシーでは、誰がそのLambda関数を呼び出せるかを定義します。リソースポリシーを使うと、Lambda関数を呼び出す主体として、AWSサービスやAWSアカウントを指定できます。

IAMポリシーとリソースポリシーの違い

IAMポリシーとリソースポリシーは、どちらもJSONで記述します。各ポリシー内の要素は、次のとおりです。

項目 IAMポリシー リソースポリシー
リソース ポリシー適用対象のリソースのARN 同左
アクション リソースオペレーション 同左
効果 Allow or Deny 同左
プリンシパル 指定しない 権限を受け取りたいエンティティ(サービス、アカウント、ユーザ)

IAMポリシーでは、ポリシー適用対象のユーザやサービスが暗黙的にプリンシパルになります。

IAMポリシーとリソースポリシーの使い方

IAMポリシーの使い方

IAMポリシーは、次のいずれかの方法で作成します。

  • IAMポリシーを作成して、IAMロール に適用する
  • IAMロールにインラインのIAMポリシーを追加する

このようにして作成したIAMロールを、Lambda関数の実行ロールとして指定します。

リソースポリシーの使い方

一方、リソースポリシーは、Lambda関数に対して直接設定します。AWS CLIでは、aws lambda add-permissionコマンドで作成します。

今回のサービスアップデートで、AWS Management Consoleから見えるようになったのは、こちらのポリシーです。

Lambdaポリシーの表示

それぞれのポリシーをいつ使うか

IAMポリシーは、AWS Lambdaのイベントソースマッピングで、ストリームベースのAWSサービスに対してpull型のLambda関数呼び出しをするときに利用します。AWS LambdaがDynamoDB StreamやKinesis Streamをポーリングして、Lambda関数を呼び出す場合が当てはまります。

リソースポリシーは、AWS Lambdaのイベントソースマッピングで、あるAWSサービスからpush型のLambda関数呼び出しをするときに利用します。たとえば、S3、CloudFront、SNSなどからLambda関数を呼び出す場合が当てはまります。

上の基本的な方針に加えて、クロスアカウントでLambda関数呼び出すときは、たとえストリームベースのAWSサービスとLambda関数を連携する場合でも、リソースポリシーを使うと便利です。

クロスアカウントでLambdaからLambdaを呼び出す場合のポリシー例

ところで、AWSの公式ドキュメントに次のような記述があります。

IAMロールでアイデンティティベースのポリシーを使用する代わりに、Lambda関数ポリシーを使用して、クロスアカウントのアクセス権限を付与できます。たとえば、IAMロールを作成する代わりに、単純にLambda関数ポリシーにアクセス権限を追加することで、Amazon S3アクセス権限を付与して Lambda 関数を呼び出すことができます。

クロスアカウントでLambda関数を呼び出すときには、IAMポリシーを使うことも、リソースポリシーを使うこともできるようです。それぞれどんなふうに使うのでしょうか。

次のようなAWS環境を題材に、ポリシーをどのように使うかを見てみましょう。

クロスアカウントでLambdaを呼び出す構成の例

以下では、呼び出し側のLambda関数をcaller、呼び出される側のLambda関数をcalleeとします。callerはアカウント000000000000、calleeはアカウント111111111111にあります。

IAMポリシーを使う場合

IAMポリシーを使ったLambdaのクロスアカウント呼び出し

アカウント間の連携は、次の2箇所で実現されています。

  • calleeの実行ロールの信頼ポリシーで、呼び出す側のアカウントによるAssumeRoleを許可する
  • callerの実行ロールのアクセスポリシーで、calleeの実行ロールに対するAssumeRoleを許可する

calleeの実行ロールに付与する信頼ポリシーを次に示します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::000000000000:root"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

AWS LambdaへのAssumeRole許可に加えて、caller側アカウント (000000000000) によるAssumeRoleを許可しています。

一方、callerの実行ポリシーのアクセスポリシーは、次のようになります。アカウント間の連携に関係するステートメントを抜粋します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::111111111111:role/callee-role"
        }
    ]
}

呼び出し側のAWS Lambdaは、calleeの実行ロール (callee-role) を引き受けることで、calleeの実行ロールに付与されたアクセスポリシーを使うことができます。

また、一時的な認証情報をLambda関数のコードの中で扱います。callerのコード例を次に示します。

from __future__ import print_function

import base64
import boto3
import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def create_function_arn(data):
    region = data['region']
    account = data['accountId']
    function_name = data['functionName']
    return 'arn:aws:lambda:' + region + ':' + account + ':function:' + function_name

def create_role_arn(data):
    # I defined a role by adding a hyphen and 'role' after the function name.
    account = data['accountId']
    function_name = data['functionName']
    return 'arn:aws:iam::' + account + ':role/' + function_name + '-role'

def lambda_handler(event, context):
    logger.info(event)
    for record in event['Records']:
        data = json.loads(base64.b64decode(record['kinesis']['data']))

        role_arn = create_role_arn(data)
        function_arn = create_function_arn(data)
        params = {
            'instanceId': data['parameters']['instanceId']
            }

        sts_response = boto3.client('sts').assume_role(
            RoleArn = role_arn,                              
            RoleSessionName = 'AssumeCalleeRole',
            DurationSeconds=900
            )

        client = boto3.client(
            service_name='lambda',
            aws_access_key_id = sts_response['Credentials']['AccessKeyId'],
            aws_secret_access_key = sts_response['Credentials']['SecretAccessKey'],
            aws_session_token = sts_response['Credentials']['SessionToken']
            )

        response = client.invoke(
            ClientContext = 'string',
            FunctionName = function_arn,
            InvocationType = 'Event',
            LogType = 'Tail',
            Payload = json.dumps(params)
            )
        logger.info(response)

リソースベースのポリシーを使う場合

リソースポリシーを使ったLambdaのクロスアカウント呼び出し

アカウント間の連携は、calleeのリソースポリシーで定義します。実行ロールの信頼ポリシーにアカウント情報を記述したり、AssumeRoleの使用許可をアクセスポリシーで与えたりする必要はありません。calleeにリソースポリシーを付与するコマンドを次に示します。

aws lambda add-permission --profile callee \
  --function-name callee \
  --statement-id AllowActionFromCaller \
  --principal 000000000000 \
  --action lambda:InvokeFunction

callerによるcalleeの呼び出しは、callerの実行ロールに付けたアクセスポリシーで許可します。ポリシーを次に示します。

{
    "Version": "2012-10-17",
    "Statement": {
        "Effect": "Allow",
        "Action": "lambda:InvokeFunction",
        "Resource": "arn:aws:lambda:ap-northeast-1:111111111111:function:callee"
    }
}

また、一時的な認証情報は透過的にやりとりされ、コードの中には表われません。リソースポリシーでクロスアカウント呼び出しを実現する場合のcallerのコード例を次に示します。

from __future__ import print_function

import base64
import boto3
import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def create_function_arn(data):
    region = data['region']
    account = data['accountId']
    function_name = data['functionName']
    return 'arn:aws:lambda:' + region + ':' + account + ':function:' + function_name

def lambda_handler(event, context):
    logger.info(event)
    for record in event['Records']:
        data = json.loads(base64.b64decode(record['kinesis']['data']))

        function_arn = create_function_arn(data)

        params = {
            'instanceId': data['parameters']['instanceId']
        }
        response = boto3.client('lambda').invoke(
            ClientContext = 'string',
            FunctionName = function_arn,
            InvocationType = 'Event',
            LogType = 'Tail',
            Payload = json.dumps(params)
        )
        logger.info(response)

IAMポリシーまたはリソースポリシーを使うときの留意点

ここまで見てきたように、Lambda関数のクロスアカウント呼び出しは、IAMポリシーとリソースポリシーのどちらを使っても実現できます。IAMポリシーまたはリソースポリシーを使う時のそれぞれの留意点を整理します。

IAMポリシーを使うときの留意点

この場合、calleeの実行ロールの信頼ポリシーで、caller側アカウントのAssumeRoleを明示的に許可する必要があります。AWS Management ConsoleからLambdaサービスロールを作る時に自動で付与される信頼ポリシーは、AWS LambdaサービスへのAssumeRoleだけを許可する内容になっています。意図どおり動かないときは、信頼ポリシーが足りているか確認しましょう。

また、callerの実行ロールでは、calleeの実行ロールを正確に指定してAssumeRoleを許可しましょう。すべてのロールに対してAssumeRoleを許可すると、callee側アカウントにある他のIAMロールに対して意図しないAssumeRoleを許可してしまう可能性があります。

リソースポリシーを使うときの留意点

今回のサービスアップデートでリソースポリシーの設定内容が見えやすくなりましたが、リソースポリシーの追加や削除はAWS Management Console上でサポートされていません(2017年2月末時点)。編集が必要な時は、AWS CLIやAWS SDKを使いましょう。

また、callerの実行ロールでは、calleeを正確に指定してInvokeを許可しましょう。すべてのLambda関数に対してInvokeを許可すると、callee側アカウントの別のLambda関数に対する意図しない呼び出し許可を与えることになりかねません。

どちらを使うか: クロスアカウントではリソースポリシーが便利

次の2点が、クロスアカウント目的でリソースポリシーを使うメリットです。

  • 信頼ポリシーに別アカウントを含める必要がない
  • Lambda関数内で一時的な認証情報を明示的に扱う必要がないため、コードの分量が減る

クロスアカウントの設定をするなら、IAMポリシーを使う方法よりも、リソースポリシーを使うほうが便利だと思います。

おわりに

Lambda関数の権限設定をする時は、IAMポリシーとリソースポリシーの特徴を理解して、上手く使い分けましょう。

それでは、また。