「Amazon API Gateway」から「Lambda」を通さずに「S3」へアクセスできるようにしてみる( `aws-cli` で構築)

2019-03-20Amazon,AWS

はじめに

AWSを利用したサーバレスな事例が増えてきました。
例えば以下のような構成の事例がWeb上で掲載されています。

「API Gateway」 から 「Lambda」 を経由して 「DynamoDB」あるいは「S3」 にアクセスする例は、様々な方がWeb上に掲載しています。
「API Gateway」 から 「S3」 へ直接アクセスするための例はあまり見つからず、またあったとしても AWS-CLI を使って設定している例が見つからなかったため、整理してみました。

ちなみに設定した内容はAmazonが提供している以下のドキュメントを参考にしています。こちらのドキュメントは「AWS マネージメントコンソール」からポチポチ操作していく内容となっています。

作成する「API」概要

  • S3にアクセスできる。(今回は読み取りのみ)
  • アクセス用URLは https://<host>/<S3-BUCKET-NAME>/<S3-OBJECT-KEY> の形式とする。

準備

  • aws-cli コマンドが利用できる
  • jq コマンドが利用できる

ja はなくてもよいのですが、今回各AWS構成要素を作成した際のJSONレスポンスをパースしてできる限りコマンドラインのみの操作としたかったので利用してみました。

検証環境

$ bash -version
GNU bash, バージョン 5.0.2(1)-release (x86_64-apple-darwin18.2.0)

$ aws --version
aws-cli/1.16.120 Python/3.7.2 Darwin/18.2.0 botocore/1.12.110

$ jq --version
jq-1.6

手順

デプロイ直前までの流れは以下の通り。
(デプロイは次回実施してみる。)

  1. IAMの作成
  2. S3バケットの作成
  3. 「REST API」の作成
  4. 「REST API リソース」の作成
  5. 「REST API リソース」に対するメソッドの作成
  6. リソースに対してリクエスト、レスポンス設定を行う
  7. テスト

セットアップ時に事前に決めておかないといけない内容がいくつかあったため表にまとめておきました。
セットアップ手順は非常に長いですが 事前に決めておかないといけない情報は意外と少ないです。

設定項目(当エントリで登場する環境変数名)目的設定値
IAM_ROLE_NAME「API Gateway」に設定するロール名g-test-role-s3-full-access
S3_BUCKET_NAMEアクセス対象の「S3バケット名」g-test-bucket
APIGATEWAY_RESTAPI_NAME「REST API」名g-test-restapi
AWS_DEFAULT_REGION各種設定を行う「リージョン」名ap-northeast-1

IAMの作成

まずは 「API Gateway」 に割り当てるIAMロールを作成します。
「API Gateway」かS3にアクセスできるようにします。

# IAMロール名は以下の名前で作成します。
$ IAM_ROLE_NAME='g-test-role-s3-full-access'

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

# 作成したポリシーファイルを元に、ロールを作成。
# 同時にIAM ROLE IDを保存しておきます。
$ IAM_ROLE_ID=$(
  aws iam create-role \
    --role-name "${IAM_ROLE_NAME}" \
    --assume-role-policy-document file://${IAM_ROLE_NAME}-policy.json | jq -r '.Role.RoleId'
)

# 動作確認
$ echo ${IAM_ROLE_ID}

加えて、「S3」へのフルアクセスを付与しておきます。 (実サービスに適用する際には注意してください!)

$ aws iam attach-role-policy \
  --role-name "${IAM_ROLE_NAME}" \
  --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess

S3バケットの作成

アクセスする対象の「S3バケット」を作成します。

$ S3_BUCKET_NAME=g-test-bucket

# S3バケット作成
$ aws s3 mb s3://${S3_BUCKET_NAME}

「REST API」の作成

いよいよ「REST API」を作成していきます。

$ APIGATEWAY_RESTAPI_NAME=g-test-restapi

# REST API IDを保存しておきます
$ APIGATEWAY_REST_API_ID=$(
  aws apigateway create-rest-api \
    --name "${APIGATEWAY_RESTAPI_NAME}" | jq -r '.id'
)

# 動作確認
$ echo "${APIGATEWAY_REST_API_ID}"

「REST API リソース」の作成

次に、作成した「REST API」に 「リソース」 を割り当てます

「リソース」 とはURLのパスのことです。(僕はそのように解釈しました。)
URLのパスですので、階層構造があります。
予めルートリソース( = "/" ) は作成されています。
自分でリソースを作成する際には親となるリソースを指定しなければなりません。

はじめに記載していたとおり、今回作成したいパスは

  • アクセス用URLは https://<host>/<S3-BUCKET-NAME>/<S3-OBJECT-KEY> の形式とする。

としていたので、ルートリソースの他に2つ登録が必要です。

# 「ルート リソース ID」を保存しておきます
$ APIGATEWAY_RESOURCE_ID_ROOT=$(
  aws apigateway get-resources \
    --rest-api-id "$APIGATEWAY_REST_API_ID" | jq -r '.items[0].id'
)

# 「リソース ID」を保存しておきます
$ APIGATEWAY_RESOURCE_ID_S3_BUCKET_NAME=$(
  aws apigateway create-resource \
    --rest-api-id "${APIGATEWAY_REST_API_ID}" \
    --parent-id "${APIGATEWAY_RESOURCE_ID_ROOT}" \
    --path-part '{s3_bucket_name}' | jq -r '.id'
)

# 「リソース ID」を保存しておきます
$ APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY=$(
  aws apigateway create-resource \
    --rest-api-id "${APIGATEWAY_REST_API_ID}" \
    --parent-id "${APIGATEWAY_RESOURCE_ID_S3_BUCKET_NAME}" \
    --path-part '{s3_object_key}' | jq -r '.id'
)

# 動作確認
$ echo "${APIGATEWAY_RESOURCE_ID_S3_BUCKET_NAME}"
$ echo "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}"

「REST API リソース」に対するメソッドの作成

作成した「リソース」がどんな 「HTTPリクエストメソッド」 を受け付けられるかを設定していきます。
今回は読み取りのみを設定したいので、 GET メソッドだけ作成します。

GET メソッドの設定対象のリソース(パス)は /{s3_bucket_name}/{s3_object_key} です。

メソッド作成時には --request-parameters を設定しないとパスを分解したときのパラメータ値が正しく取得できないことに注意。僕はこれでまる一日近くハマりました。
( AWS-CLI上はエラーが出ないのですが、後々「統合リクエスト」設定でCUI/GUIいずれでも操作できなくなってしまいます。仕様? )

# REST API リソース に対してGET可能とする。Gateway外からCallする場合にはAPIキーが必須とする。
aws apigateway put-method \
  --rest-api-id "${APIGATEWAY_REST_API_ID}" \
  --resource-id "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}" \
  --http-method GET \
  --authorization-type NONE \
  --request-parameters "method.request.path.s3_bucket_name=true,method.request.path.s3_object_key=true" \
  --api-key-required \
  ;

リソースに対してリクエスト、レスポンス設定を行う

もう一息。

GET メソッド」が実行されたときに、「API Gateway」がS3と通信する方法について設定を行います。

パスとして受け取った s3_bucket_name / s3_object_key の値を S3 に対して引き渡すための設定も行っています。


aws apigateway put-method-response \
  --rest-api-id "${APIGATEWAY_REST_API_ID}" \
  --resource-id "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}" \
  --http-method GET \
  --status-code 200 \
  --response-models '{"application/json": "Empty"}'

aws apigateway update-method-response \
  --rest-api-id "${APIGATEWAY_REST_API_ID}" \
  --resource-id "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}" \
  --http-method GET \
  --status-code 200 \
  --patch-operations op="add",path="/responseParameters/method.response.header.Content-Type",value="false"

aws apigateway put-method-response \
  --rest-api-id "${APIGATEWAY_REST_API_ID}" \
  --resource-id "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}" \
  --http-method GET \
  --status-code 400 \
  --response-models '{"application/json": "Empty"}'

aws apigateway put-method-response \
  --rest-api-id "${APIGATEWAY_REST_API_ID}" \
  --resource-id "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}" \
  --http-method GET \
  --status-code 500 \
  --response-models '{"application/json": "Empty"}'

aws apigateway put-integration \
  --rest-api-id "${APIGATEWAY_REST_API_ID}" \
  --resource-id "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}" \
  --http-method GET \
  --type AWS \
  --integration-http-method GET \
  --uri "arn:aws:apigateway:${AWS_DEFAULT_REGION}:s3:path/{s3_bucket_name}/{s3_object_key}" \
  --credentials $(
    aws iam get-role \
      --role-name "${IAM_ROLE_NAME}" | jq -r '.Role.Arn'
  ) \
  --request-parameters 'integration.request.path.s3_bucket_name=method.request.path.s3_bucket_name,integration.request.path.s3_object_key=method.request.path.s3_object_key'

aws apigateway put-integration-response \
  --rest-api-id "${APIGATEWAY_REST_API_ID}" \
  --resource-id "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}" \
  --http-method GET \
  --status-code 200 \
  --response-templates '{"application/json": ""}'

テスト

テストデータの作成

対象のバケットに、 test.json というテストファイルを作成しておいておきます。

$ echo '{ "msg": "hello" }' > test.json

$ aws s3 cp test.json s3://${S3_BUCKET_NAME}
upload: ./test.json to s3://g-test-bucket/test.json

REST API コール

APIのコールが正しく行えるかを「AWS マネージメントコンソール」からテストしてみます。

「クエリ文字列」の部分の表示が気持ち悪い。何かミスっていそうな気がするが先に進む。

正しく動作していることがわかりました。

AWS-CLIを使ってコールしてみる

「AWS マネージメントコンソール」を使わずに、「AWS-CLI」を使ってコールすることもできます。

$ aws apigateway test-invoke-method \
  --rest-api-id "${APIGATEWAY_REST_API_ID}" \
  --resource-id "${APIGATEWAY_RESOURCE_ID_S3_OBJECT_KEY}" \
  --http-method GET \
  --path-with-query-string "${S3_BUCKET_NAME}/test.json" \
  ;

{
    "status": 200,
    "body": "{ \"msg\": \"hello\" }\n",
    "headers": {
        "Content-Type": "application/json",
        "X-Amzn-Trace-Id": "Root=1-5c8c8f47-bd5e7301b636a01d36c505ce"
    },
    "multiValueHeaders": {
        "Content-Type": [
            "application/json"
        ],
        "X-Amzn-Trace-Id": [
            "Root=1-5c8c8f47-bd5e7301b636a01d36c505ce"
        ]
    },
...(省略)...

HTTPレスポンスコードが 200 、 "body" にもJSONが正しく出力されています。

【2019-03-16 追記】

一連の処理をシェルスクリプト化して Github で公開しました。

ひとこと

一部表示が怪しい部分がありましたがなんとか設定できました。
次回やり残しは以下の通り。

  • デプロイ
  • POST メソッドの設定

2019-03-20Amazon,AWS