JAWS-UG CLI専門支部 #171R S3基礎 通知 (Lambdaの自動実行) に参加したので、そのレポートです。

connpass のイベントページはこちら。

これまでの CLI 専門支部参加レポートはこちら。

目次

概要

Amazon S3 はただのストレージではなく、非常に機能が多いストレージになってます。どこが Simple やねん というツッコミもあるくらいです。

今回は S3 の機能の中の一つである イベント通知 に関する内容でした。イベント通知については下記の記事内でも書いてますが、S3 バケットに対する色んなイベントをトリガーにして他のサービスと連携することができる機能です。

イベントの内容としては、次のようなものです。

  • オブジェクトの作成 : s3:ObjectCreated:*
  • オブジェクトの削除 : s3:ObjectRemoved:*
  • オブジェクトの復元 : s3:ObjectRestore:Post, s3:ObjectRestore:Completed

その他のイベントについては下記公式ドキュメントの サポートされるイベントタイプ を参照してください。

Amazon S3 イベント通知の設定 - Amazon Simple Storage Service

そして、これらのイベントをトリガーにして連携できるサービスとしては

  • Amazon SNS のトピック
  • Amazon SQS のキュー
  • AWS Lambda の関数

を指定することができます。

今回のハンズオンでは、オブジェクトの作成 (s3:ObjectCreated:*) イベントをトリガーにして Lambda 関数を起動し、その関数内では S3 から渡されたイベントを print() で出力し、その内容を CloudWatch Logs で確認するという一連の流れを試しました。

S3 のイベントをトリガーに Lambda 関数を実行するというのは以前に JAWS-UG 初心者支部のハンズオンでもやった内容なので、ここではそのときにやったハンズオンを AWS CLI を使ってやってみることにします。

音声ファイルから文字起こし

初心者支部でやったハンズオンは、 S3 バケットに音声ファイル (.mp3) をアップロードし、そのイベントをトリガーに Lambda 関数を実行。その Lambda 関数では Amazon Transcribe を使ってアップロードされた音声ファイルを文字起こしして、その結果を再度 S3 バケットに出力するという処理をしています。(実際に結果を S3 バケットに出力するのは Amazon Transcribe)

さらに、文字起こしした結果が S3 バケットに出力されたことをトリガーにして、今度は Amazon Translate を使って翻訳をする Lambda 関数を実行するところまで実装する宿題もありました。が、今回はそこまでやりません。ちなみにその宿題を普通にやってみた話はこちらです。

また、その後の復習イベントで、この内容と同じものを AWS CDK を使って構築してみた話を LT で紹介しました。

やってみる

前置きが長くなりましたが、早速 CLI でやっていきます。手順としては下記のとおりです。

  1. 音声ファイルアップロード用の S3 バケットを作成
  2. 文字起こし結果を出力する S3 バケットを作成
  3. Amazon Transcribe を使って文字起こしする Lambda 関数を作成
    • Lambda 関数用の IAM ロールの作成
    • 関数本体の作成
    • S3 から Lambda を実行する権限を付与
  4. 1 で作成したバケットにイベント通知設定を追加
  5. 確認
    • 1 のバケットに音声ファイルをアップロード
    • 2 のバケットに文字起こし結果が出力されることを確認

これで構築されるのは、次のような構成です。

S3 event notification

では、順番にやっていきます。

1. 音声ファイルアップロード用の S3 バケットを作成

まずは音声ファイルをアップロードする S3 バケットを作成します。 s3api create-bucket コマンドを使います。

S3 バケット名を変数に設定します。

$ INPUT_BUCKET="s3-handson-input-$(aws sts get-caller-identity \
--query 'Account' \
--output text)" && echo ${INPUT_BUCKET}

s3-handson-input-************

(************ はアカウント ID とします)

対象のリージョンは、東京 (ap-northeast-1) とします。

$ S3_BUCKET_LOCATION="ap-northeast-1"

バケットを作成します。

$ aws s3api create-bucket \
--bucket ${INPUT_BUCKET} \
--create-bucket-configuration "LocationConstraint=${S3_BUCKET_LOCATION}"

{
    "Location": "http://s3-handson-input-************.s3.amazonaws.com/"
}

2. 文字起こし結果を出力する S3 バケットを作成

1 と同様に、 Amazon Transcribe で文字起こしされた結果が出力される S3 バケットを作成します。

$ OUTPUT_BUCKET="s3-handson-output-$(aws sts get-caller-identity \
--query 'Account' \
--output text)" && echo ${OUTPUT_BUCKET}

s3-handson-output-************

$ aws s3api create-bucket \
--bucket ${OUTPUT_BUCKET} \
--create-bucket-configuration "LocationConstraint=${S3_BUCKET_LOCATION}"

{
    "Location": "http://s3-handson-output-************.s3.amazonaws.com/"
}

3. Amazon Transcribe を使って文字起こしする Lambda 関数を作成

S3 のイベントによって起動される Lambda 関数を作成します。

関数の実体となる .py ファイルを作成

関数の処理内容としては、S3 から渡されるイベントの内容から対象のオブジェクトのキーを取得し、そのキーをもとに Amazon Transcribe の文字起こし処理を呼ぶ というものです。今回は Python で実装した下記のような関数を使います。

import json
import urllib.parse
import boto3
import datetime

s3 = boto3.client('s3')
transcribe = boto3.client('transcribe')

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    media_uri = f'https://{bucket}.s3-ap-northeast-1.amazonaws.com/{key}'
    try:
        transcribe.start_transcription_job(
            TranscriptionJobName= datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '_Transcription',
            LanguageCode='en-US',
            Media={
                'MediaFileUri': media_uri
            },
            OutputBucketName='s3-handson-output-************'
        )
    except Exception as e:
        print(e)
        print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
        raise e

OutputBucketName には、 2 で作成したバケット名を指定します。

これを transcribe.py として保存し、下記のコマンドを実行して ZIP ファイルを生成します。

$ LAMBDA_ZIP="function.zip"
$ FUNCTION_FILE="transcribe.py"
$ zip -j ${LAMBDA_ZIP} ${FUNCTION_FILE}

adding: transcribe.py (deflated 46%)

Lambda 関数にアタッチする IAM ロールを作成

続いて、 Lambda 関数にアタッチする IAM ロールを作成します。今回の Lambda 関数で必要になる権限は、 CloudWatch Logs へのログの書き込み、 Amazon Transcribe の実行権限、 S3 へのアクセス権限です。ということで、次のような IAM ポリシードキュメントを policy.json という名前で作成します。

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

このポリシードキュメントを使って、 s3_handson_transcribe_policy という名前の IAM ポリシーを作成します。コマンドは iam create-policy です。ポリシーを作成する際には、その後の管理がしやすくなるように --path パラメータを指定します。

$ LAMBDA_IAM_POLICY_NAME="s3_handson_transcribe_policy"
$ aws iam create-policy \
--policy-name ${LAMBDA_IAM_POLICY_NAME} \
--path "/michimani/sample/" \
--policy-document file://policy.json

{
    "Policy": {
        "PolicyName": "s3_handson_transcribe_policy",
        "PolicyId": "ANPA2FQKQ3TUYPLJBDU4T",
        "Arn": "arn:aws:iam::************:policy/michimani/sample/s3_handson_transcribe_policy",
        "Path": "/michimani/sample/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2020-11-12T12:59:08+00:00",
        "UpdateDate": "2020-11-12T12:59:08+00:00"
    }
}

この IAM ポリシーを用いて、 s3_handson_transcribe_role という名前の IAM ロールを作成します。まずは、 Lambda がロールを使用するための信頼ポリシードキュメントを作成します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}

これを assume_role.json として保存しておきます。

ロール名を変数に設定して、 IAM ロールを作成します。コマンドは iam create-role です。

$ LAMBDA_IAM_ROLE_NAME="s3_handson_transcribe_role"
$ aws iam create-role \
--role-name ${LAMBDA_IAM_ROLE_NAME} \
--assume-role-policy-document file://assume_role.json

{
    "Role": {
        "Path": "/",
        "RoleName": "s3_handson_transcribe_role",
        "RoleId": "AROA2FQKQ3TUU6KJ3TVDJ",
        "Arn": "arn:aws:iam::************:role/s3_handson_transcribe_role",
        "CreateDate": "2020-11-12T13:17:00+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Action": "sts:AssumeRole",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Effect": "Allow",
                    "Sid": ""
                }
            ]
        }
    }
}

IAM ポリシーの ARN を変数に設定して、上で作成した IAM ロールにアタッチします。コマンドは iam attach-policy です。

$ LAMBDA_IAM_POLICY_ARN=$( \
  aws iam list-policies \
    --scope Local \
    --path-prefix "/michimani/sample/" \
    --max-items 1000 \
    --query "Policies[?PolicyName==\`${LAMBDA_IAM_POLICY_NAME}\`].Arn" \
    --output text \
) && echo ${LAMBDA_IAM_POLICY_ARN}

arn:aws:iam::************:policy/michimani/sample/s3_handson_transcribe_policy

$ aws iam attach-role-policy \
--role-name ${LAMBDA_IAM_ROLE_NAME} \
--policy-arn ${LAMBDA_IAM_POLICY_ARN}

作成した IAM ロールを Lambda 関数にアタッチする際に ARN が必要になるので、変数に設定しておきます。

$ LAMBDA_IAM_ROLE_ARN=$( \
  aws iam get-role \
    --role-name ${LAMBDA_IAM_ROLE_NAME} \
    --query 'Role.Arn' \
    --output text \
) && echo ${LAMBDA_IAM_ROLE_ARN}

arn:aws:iam::************:role/s3_handson_transcribe_role

Lambda 関数を作成

諸々準備が整ったので、 Lambda 関数を作成します。コマンドは lambda create-function です。

$ LAMBDA_FUNCTION_NAME="s3-handson-tanscribe"
$ aws lambda create-function \
--function-name ${LAMBDA_FUNCTION_NAME} \
--description "S3 イベント通知テスト用関数" \
--runtime "python3.8" \
--role ${LAMBDA_IAM_ROLE_ARN} \
--handler "transcribe.lambda_handler" \
--zip-file fileb://${LAMBDA_ZIP}

{
    "FunctionName": "s3-handson-tanscribe",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:************:function:s3-handson-tanscribe",
    "Runtime": "python3.8",
    "Role": "arn:aws:iam::************:role/s3_handson_transcribe_role",
    "Handler": "transcribe.lambda_handler",
    "CodeSize": 680,
    "Description": "S3 イベント通知テスト用関数",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2020-11-12T13:43:35.679+0000",
    "CodeSha256": "rrjImL4le+s5Hyz8rPlapksfo1DmUqpZnOzpYwfRWtc=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "1e1fcdd5-ba7f-4495-a8ee-7d7b71c4e3ee",
    "State": "Active",
    "LastUpdateStatus": "Successful"
}

S3 バケットに Lambda の実行権限を付与

作成した Lambda 関数を 1 で作成したバケットが実行できるように権限を付与します。コマンドは lambda add-permission を使用しますが、事前に必要なパラメータの値を変数に設定しておきます。

  • S3 バケットの ARN

    $ INPUT_BUCKET_ARN="arn:aws:s3:::${INPUT_BUCKET}" \
    && echo ${INPUT_BUCKET_ARN}
      
    arn:aws:s3:::s3-handson-input-************
    

実行権限を付与します。

$ aws lambda add-permission \
--function-name ${LAMBDA_FUNCTION_NAME} \
--statement-id "s3-invoke-lambda-function" \
--action "lambda:InvokeFunction" \
--principal "s3.amazonaws.com" \
--source-arn ${INPUT_BUCKET_ARN}

4. 1 で作成したバケットにイベント通知設定を追加

1 で作成したバケットに対して、イベント通知の設定を追加します。まずはイベント通知設定用のドキュメントを作成します。その際、実行する Lambda 関数の ARN が必要なので、事前に取得しておきます。

$ aws lambda get-function \
--function-name ${LAMBDA_FUNCTION_NAME} \
--query 'Configuration.FunctionArn' \
--output text

arn:aws:lambda:ap-northeast-1:************:function:s3-handson-tanscribe

今回はオブジェクトの作成 (MP3 ファイル) をトリガーにして、 Lambda 関数を実行するので、次のような次のようなドキュメントになります。イベントの発生条件を制御するために、 Filter でサフィックスが .mp3 のオブジェクトのみをイベント発生の対象としています。

{
  "LambdaFunctionConfigurations": [
    {
      "Id": "s3:ObjectCreated-lambda",
      "LambdaFunctionArn": "arn:aws:lambda:ap-northeast-1:************:function:s3-handson-tanscribe",
      "Events": [
        "s3:ObjectCreated:*"
      ],
      "Filter": {
        "Key": {
          "FilterRules": [
            {
              "Name": "suffix",
              "Value": ".mp3"
            }
          ]
        }
      }
    }
  ]
}

これを config.json として保存しておきます。その他、 Lambda 関数との連携設定については下記のドキュメントを参照してください。

LambdaFunctionConfiguration - Amazon Simple Storage Service

この JSON をもとに、 1 で作成した S3 バケットに対してイベント通知設定を追加します。コマンドは s3api put-bucket-notification-configuration を使います。

$ aws s3api put-bucket-notification-configuration \
--bucket ${INPUT_BUCKET} \
--notification-configuration file://config.json

ちなみに、該当のバケットに Lambda の実行権限 (前項で設定した権限) がない場合、 s3api put-bucket-notification-configration を実行しても下記のようなエラーとなり、イベント通知の設定が追加できません。

An error occurred (InvalidArgument) when calling the PutBucketNotificationConfiguration operation: Unable to validate the following destination configurations

5. 確認

すべての準備が整ったので、 INPUT_BUCKET に MP3 ファイルをアップロードしてみます。使用する MP3 ファイルは、 Amazon Poly のページで用意されているサンプルファイルから HelloEnglish-Joanna.mp3 をダウンロードして使います。

Amazon Polly(深層学習を使用したテキスト読み上げサービス)| AWS

$ aws s3 cp ./HelloEnglish-Joanna.mp3 s3://${INPUT_BUCKET}/HelloEnglish-Joanna.mp3
upload: ./HelloEnglish-Joanna.mp3 to s3://s3-handson-input-************/HelloEnglish-Joanna.mp3

しばらくすると OUTPUT_BUCKET に文字起こし結果が出力されるので、確認します。

$ aws s3 ls s3://${OUTPUT_BUCKET}
2020-11-13 00:12:51          2 .write_access_check_file.temp
2020-11-13 00:13:16       1878 20201112151225_Transcription.json

JSON ファイルをダウンロードして中身を確認してみます。

$ aws s3 cp s3://${OUTPUT_BUCKET}/20201112151225_Transcription.json ./res.json \
&& cat res.json \
| jq ."results.transcripts[0]"

{
  "transcript": "Hello. Do you speak a foreign language? One language is never enough."
}

文字起こしの結果が取得できました。

まとめ

JAWS-UG CLI専門支部 #171R S3基礎 通知 (Lambdaの自動実行) に参加したので、そのレポートでした。
というか、今回はレポートではなく以前にマネジメントコンソールや CDK で作成した構成を AWS CLI でやってみた内容になってしまいました。

やってみた感想としては、正直 AWS CDK で構築するのが一番楽だなという思いです。どのあたりが辛かったかと言うと、 IAM ポリシー、 IAM ロールまわりの設定です。マネジメントコンソールや CDK でよしなにやってくれている部分が大きいんだなと、あらためて感じました。

逆に言うと、 AWS CLI でやろうとするとそのあたりをしっかり理解していないと設定できないので、 IAM の各要素 (ポリシー/ロール/ユーザー…) に関して理解を深めるには一番良い方法だなと思います。 これまでの CLI 専門支部のハンズオンの中でも IAM による権限設定は毎回のように出てきますが、なんとなくコマンドのコピペで終わっていた部分も大いにあるので、しっかり理解しながら進めないといけないなと感じました。

今回のように扱うサービスや機能 (S3 + Lambda) 自体に馴染みがある場合には、 IAM の権限設定まわりを注意しながらハンズオンやっていこうと思います。


以上、よっしー (michimani) でした。

Share to ...

0

Follow on Feedly