Amazon API Gatewayで地図データを配信するバックエンドをCDKで作る。

AwsGatewayで地図データ(PNG)を配信するAPIを作成し、S3に保存されたpngファイルを画像データとしてApiGatewayを使って配信したい。

:::note info
これは [MIERUNE AdventCalendar 2022](https://qiita.com/advent-calendar/2022/mierune) 13日目の記事です。
昨日は @chizutodesign さんによる [MapTilerの地図アイコンをデザインした話](https://qiita.com/chizutodesign/items/02d3498aa65f5efec00f) でした。
:::
APIのURLは以下を想定。ApiGateway+Lambdaで構築する。
```
(汎用例)https://hogehogeapi/raster/<バケット名>？key=<S3オブジェクトパス>.png
(具体例)https://hogehogeapi/raster/s3backetname？key=/10/20/30.png
```

### 実装方針
S3バケット名をパスパラメータに、オブジェクトパスをkey(クエリパラメータ)としてLambda関数に渡し、Lambda関数はS3よりオブジェクトを取得してBASE64にエンコードしApiGatewayに返却、ApiGatewayはBASE64をデコードしてクライアントへ返却する。
Lambdaに使用する言語はPythonで構築、以下のようなLambda関数を用意する。 (raster.py)
**BASE64にエンコード後にApiGatewayでバイナリに戻して上げる点が注意点となる。**

``` python
# raster.py
import base64
from pathlib import Path
import boto3

s3 = boto3.resource("s3")
def get_raster(event, context):
    try:
        bucket = s3.Bucket(event["bucket"])
        path = event["path"]
        if Path(path).suffix == ".png":
            response = bucket.Object(path).get()
            data = response["Body"].read()
            return base64.b64encode(data).decode("utf-8")
        else:
            return "Invalid path"
    except Exception as e:
        print(f"Exception..{e}")
        return "Invalid Request"
```

## AWSコンソールからのApiGatewayの設定

APIGatewayにおいて**Lambdaプロキシ統合の使用**を選択すると、文字列(主にJSONなど)を返却するようになるため、バイナリで返却したい場合以下の設定をApiGatewayで行う必要がある。

- バイナリタイプとして扱うメディアタイプを指定して、APIのバイナリサポートを設定有効にする。
![スクリーンショット 2022-11-30 14.51.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810146/1013d6f1-6183-9f2d-c250-e2dd8d155564.png)

- メソッドリクエストの設定にURLクエリ文字列パラメータを設定する。
![スクリーンショット 2022-11-30 14.52.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810146/7396c4c5-168a-cc24-665b-232c2466a2fa.png)


- 統合レスポンスの設定を行いたいため、統合リクエストの**Lambdaプロキシ統合の使用**のチェックを外す。
上記の設定によりLambdaにわたすリクエストのマッピングは自動で設定されないので、**マッピングテンプレートを設定する。**
![スクリーンショット 2022-11-30 14.53.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810146/390837b8-4442-eaa0-8815-49a088a7c12e.png)


- 統合レスポンスの設定。
**コンテンツの処理**の設定を行うとApiGatewayはLambda関数から渡されたレスポンス(BASE64)を統合レスポンスでバイナリ化してメソッドレスポンスとしてクライアント側へ返却する。
![スクリーンショット 2022-11-30 14.54.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810146/655e2e8e-1982-9ce1-1f9c-9d8f46446a7d.png)

- メソッドレスポンスの設定。
![スクリーンショット 2022-11-30 14.55.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810146/6b921813-ec16-4aa9-1bae-c9351a174048.png)


上記の設定を行うことにより、https://hogehogeapi/raster/<バケット名>？key=<S3オブジェクトパス>.pngでこのAPIを呼び出すと、S3に保存されている  /<バケット名>/< S3オブジェクトパス>.png がクライアント側にコンテンツタイプ 
 image/png として呼び出され、PNG画像が表示される。

## CDKでの実装例
AWSコンソール画面からの設定では毎回同じことを繰り返さなければならないのでCDKで作成しておく。
以下がその実装例。

肝となる点は以下

``` typescript
    const lambdaIntegration = new LambdaIntegration(
        .
        .
        .
        proxy: false,
        passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
        requestTemplates: { 'application/json': requestTemplates },
        integrationResponses: [
          {
            statusCode: '200',
            contentHandling: ContentHandling.CONVERT_TO_BINARY,
            responseParameters: { 'method.response.header.Content-Type': "'image/png'" }
          },
        ]

```



全てのコード

``` typescript
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { ContentHandling, LambdaIntegration, PassthroughBehavior, RestApi } from 'aws-cdk-lib/aws-apigateway';

export class hogehogeapi extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const requestTemplates = `
      {
        "bucket": "$input.params(\'bucket\')", 
        "key": "$input.params(\'key\')",
      }`;

    const lambdaIntegration = new LambdaIntegration(
      new lambda.Function(
        this,
        `get_raster`,
        {
          functionName: "funcName",
          runtime: lambda.Runtime.PYTHON_3_8,
          code: lambda.Code.fromAsset('./handler'),
          memorySize: 128,
          handler: "handlerName"
        }),
      {
        proxy: false,
        passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH,
        requestTemplates: { 'application/json': requestTemplates },
        integrationResponses: [
          {
            statusCode: '200',
            contentHandling: ContentHandling.CONVERT_TO_BINARY,
            responseParameters: { 'method.response.header.Content-Type': "'image/png'" }
          },
        ]
      }
    );

    // APIGateway関連の設定
    const api = new RestApi(this, `api`, {
      restApiName: `hogehogeapi`,
      description: `hogehogeapi backend Service`,
      binaryMediaTypes: ['image/png'],
      deployOptions: {
        stageName: 'dev'
      },
    });

    const CorsOptions = {
      statusCode: 200,
      allowOrigins: ['*'],
      allowMethods: ['OPTIONS', 'GET']
    };

    const resource = api.root.addResource('raster')
    const resourcePath = resource.addResource('{bucket}');
    resourcePath.addMethod('GET', lambdaIntegration, {
      requestParameters: {
        'method.request.querystring.key': false,
      },
      methodResponses: [{
        statusCode: '200',
        responseParameters: {
          'method.response.header.Content-Type': true
        },
        responseModels: {
          'image/png': { modelId: 'Empty' }
        }
      }]
    });
    resourcePath.addCorsPreflight(CorsOptions);
  }
}
```
:::note info
明日は @dayjournal さんによる [QGISのプロセッシングを色々とためしてみた](https://qiita.com/dayjournal/items/dc2009cf6c30aa7f978c)です！お楽しみにー
:::