S3にファイルがSFTPアップロードされたことを検知してLambdaを起動する( `aws-cli` で構築)

2019-04-26Amazon,AWS,Bash

以下のような仕組みを作る必要がありました。

  1. ユーザにSFTPプロトコルを使って、S3にファイルをアップロードしてもらう。
  2. S3にファイルがアップロードされたことを検知して、何らかの処理を行う。

AWSのサービスを組みわせて、かつできる限り aws-cli のみを使ってコマンドラインで仕組みを構築してみました。

今回は、 「何らかの処理」 として Lambdaファンクションを使ってCloudWatchLogにログを書き出させてみます

※注意:IAM周りの設定はかなり大雑把に設定しているので、参考する場合にはもっと考慮が必要!

簡易構成図

設定パラメータを整理

AWSの各種サービス設定時に必要となる情報を事前にまとめておきました。

設定項目設定値
AWS Transfer 実行ユーザ名(SFTPログインユーザ)g-trigger-bucket-transfer-user
AWS Transfer 実行ロール名g-trigger-bucket-transfer-role
S3 バケット名g-trigger-bucket
S3 バケット イベント名g-trigger-bucket-event
Lambda ファンクション名g-trigger-function
Lambda 実行ロール名g-trigger-function-role

事前準備

aws-cli (AWSコマンドの利用可能な環境づくり)

aws-cli の利用可能環境は、以下のエントリを参考に構築済み。

AWSサービスの設定開始

AWS S3バケットの作成

S3バケットの作成やファイルのアップロードをコマンドラインから操作する方法は、 以下のエントリで整理済み。

参考にしながら設定を進める。

# バケットを作成
$ aws s3 mb s3://g-trigger-bucket

# バケットができたかを確認
$ aws s3 ls
2019-02-25 07:32:57 g-trigger-bucket

AWS Lambdaファンクションの作成

AWS Lambda ファンクションの作成や呼び出しをコマンドラインから操作する方法は、 以下のエントリで整理済み。

参考にしながら設定を進める。

実行ロールの作成

# まずは初期ポリシーファイルを作成
$ cat <<'EOF' >g-trigger-function-role-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# 作成したポリシーファイルを元に、ロールを作成。
### 実行時に出力されたJSONメッセージのうち、 **Arn の部分は後々必要となるためメモしておく** 。
$ aws iam create-role \
  --role-name g-trigger-function-role \
  --assume-role-policy-document file://g-trigger-function-role-policy.json

# 実行結果を CloudWatch Log として保存したい場合は、以下の既存ポリシーを追加で付与してやる。
### arn:aws:iam::aws:policy/CloudWatchLogsFullAccess が "CloudWatchLogsFullAccess" のリソース識別子を新たしている
$ aws iam attach-role-policy \
  --role-name g-trigger-function-role \
  --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess

実行時に出力されたJSONメッセージのうち、 Arn の部分は後々必要となるためメモしておく

ファンクションを作成

CloudWatch Logs にログを記録するLambdaファンクションをコーディングし、デプロイします。

# index.js を作成
$ cat <<'EOF' >index.js
exports.handler = function (event, context, callback) {
  console.log("===START===")
  console.log(event = ${JSON.stringify(event)})
  console.log(context = ${JSON.stringify(context)})
  callback(null, "Success")
  console.log("===END===")
}
EOF

# jsファイルには忘れずに読み取り可能権限を付与しておく
$ chmod 644 index.js

# デプロイパッケージを作成
$ zip function.zip index.js

# create-function コマンドを使用して Lambda ファンクションを作成
## 先程ロール作成時にメモしておいた **Arn** 値を指定する
$ aws lambda create-function \
  --function-name g-trigger-function \
  --zip-file fileb://function.zip \
  --handler index.handler \
  --runtime nodejs8.10 \
  --role "arn:aws:iam::642942901536:role/g-trigger-function-role"

最後のコマンド実行後には、以下のようなJSON情報が出力されます。
出力内容のうち、 "FunctionArn"の値はこの後使用するため、メモしておきましょう。

{
    "FunctionName": "g-trigger-function",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:642942901536:function:g-trigger-function",
    "Runtime": "nodejs8.10",
    "Role": "arn:aws:iam::642942901536:role/g-trigger-function-role",
    "Handler": "index.handler",
    "CodeSize": 307,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2019-02-27T03:58:19.128+0000",
    "CodeSha256": "se58QQr0WdxVCQ4ltQqra10h4tKtGzxJRUXJmeFFBvk=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "d1205975-56ff-42fa-a2f4-7eb3bbd4bd29"
}

AWS S3バケットのイベントフックを設定

S3にファイルがアップロードされたタイミングでLambdaファンクションを起動させる設定を行います。

まずは先程作成したLambdaファンクションに対して、S3イベントフック可能な権限を与えておきましょう。

$ aws lambda add-permission \
  --function-name "g-trigger-function" \
  --statement-id "s3-put-event" \
  --action "lambda:InvokeFunction" \
  --principal "s3.amazonaws.com" \
  --source-arn "arn:aws:s3:::g-trigger-bucket"

S3にイベントの設定を行います。
ファイルが作成されたタイミングでイベントが発火するように設定。

# まずは設定内容を記述したファイルを作成
$ cat <<'EOF' >g-trigger-bucket-event.json
{
  "LambdaFunctionConfigurations": [
    {
      "LambdaFunctionArn": "arn:aws:lambda:ap-northeast-1:642942901536:function:g-trigger-function",
      "Events": ["s3:ObjectCreated:*"]
    }
  ]
}
EOF

# イベント作成
$ aws s3api put-bucket-notification-configuration \
  --bucket g-trigger-bucket \
  --notification-configuration file://g-trigger-bucket-event.json

AWS Transferを設定

最後に S3 に対して SFTP プロトコルを使ってファイルをアップロードするための口を設定します。
( この手順に関しては、今までブログで整理していなかったので初めて設定してみました。 )

サーバの作成

# サーバの作成
$ aws transfer create-server --identity-provider-type SERVICE_MANAGED
{
    "ServerId": "s-40bb881352ed42cb8"
}

実行結果のJSONに出力されている ServerId の値は後に必要となるためメモしておきます。

SFTP用ロール作成

# SFTP用ロール作成
$ cat <<'EOF' >g-trigger-bucket-transfer-role-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "transfer.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# ポリシーファイルを使ってロールを作成
$ aws iam create-role \
  --role-name g-trigger-bucket-transfer-role \
  --assume-role-policy-document file://g-trigger-bucket-transfer-role-policy.json

最後のコマンド実行後には、以下のようなJSON情報が出力されます。
出力内容のうち、 "Arn"の値はこの後使用するため、メモしておきます。

{
  "Role": {
    "Path": "/",
    "RoleName": "g-trigger-bucket-transfer-role",
    "RoleId": "AROAJR26G65QLOYSINCNK",
    "Arn": "arn:aws:iam::642942901536:role/g-trigger-bucket-transfer-role",
    "CreateDate": "2019-02-27T22:41:09Z",
    "AssumeRolePolicyDocument": {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Service": "transfer.amazonaws.com"
          },
          "Action": "sts:AssumeRole",
          "Condition": {}
        }
      ]
    }
  }
}

SFTPユーザの作成・公開鍵登録

  • 最後に登録した サーバ に対して、 SFTPロール を持つユーザを作成します。
  • 作成後はSSH公開鍵を登録
    • 端折ってしまったが、次のファイルは事前に ssh-keygen コマンドなどを使って作成しておく必要があります。
    • ~/.ssh/id_rsa.pub
# SFTPログインユーザの作成
$ aws transfer create-user \
  --role="arn:aws:iam::642942901536:role/g-trigger-bucket-transfer-role" \
  --server-id="s-40bb881352ed42cb8" \
  --user-name="g-trigger-bucket-transfer-user"

# SFTPログインユーザはS3へのアクセス権限が必要なためひとまずフルアクセスを付与
$ aws iam attach-role-policy \
  --role-name g-trigger-bucket-transfer-role \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess

# 公開鍵の登録
## 端折ってしまったが、次のファイルは事前に ssh-keygen コマンドなどを使って作成しておく。`
$ aws transfer import-ssh-public-key \
  --server-id="s-40bb881352ed42cb8" \
  --ssh-public-key-body="$(cat ~/.ssh/id_rsa.pub)" \
  --user-name="g-trigger-bucket-transfer-user"

動作確認

ファイルアップロード

SFTPでログインしてみます。
先程作成した AWS Transfer For SFTP サーバだが、ホスト名は以下のルールで付与されていました。

  • <サーバID>.server.transfer.<リージョンID>.amazonaws.com

今回は、 s-40bb881352ed42cb8.server.transfer.ap-northeast-1.amazonaws.com となる。

※ちなみに、SFTPサーバの起動までには5分程度かかるので、接続失敗しても気長に待ってみることをおすすめします。

$ sftp -i ~/.ssh/id_rsa g-trigger-bucket-transfer-user@s-40bb881352ed42cb8.server.transfer.ap-northeast-1.amazonaws.com

正常にログインできたので、ファイルをアップロードしてみます。

# 一旦SFTPからでて、ファイルを作成
$ echo 'HELLO' > test.txt

$ sftp -i ~/.ssh/id_rsa g-trigger-bucket-transfer-user@s-40bb881352ed42cb8.server.transfer.ap-northeast-1.amazonaws.com
sftp> put test.txt /g-trigger-bucket
Uploading test.txt to /g-trigger-bucket/test.txt
test.txt                                            100%    6     0.2KB/s   00:00

CloudWatchLogが正しく書き出されているかを確認

以下のページにログインしてみて、ログが出力されているかを確認してみたところ、無事書き出しされていました。

https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logs:

正しくログが出力された。
Lambdaファンクションの第一引数 event の情報から、アップロードされたS3バケット上のファイルパスを特定することができます。

以下のようにすれば、JavaScriptでS3上のURLが生成できます。

let url = s3://${event.Records[0].s3.bucket.name}/${event.Records[0].s3.object.key};

ゴミ掃除

検証が終わったらゴミ掃除。

# Lambdaファンクションの削除
$ aws lambda delete-function --function-name g-trigger-function

# IAMロールの削除
$ aws iam detach-role-policy \
  --role-name g-trigger-function-role \
  --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess

$ aws iam delete-role \
  --role-name g-trigger-function-role

$ aws iam delete-role \
  --role-name g-trigger-bucket-transfer-role

# S3バケットの削除
$ aws s3 rb s3://g-trigger-bucket --force

2019-04-26Amazon,AWS,Bash