aws-sdkでAPIGatewayとLambdaの統合をセットアップする

# 概要

APIGatewayからLambdaを呼び出せるようにするまでの設定を`aws-sdk`を使って自動化したかったのでコードを書いてみました。
特に CORS の有効化の部分はなかなか資料がなく苦労しました。。。

TypeScriptで書いているので型定義不要な方は適時読み替えてください。
上から順につなげれば動きます。

# 実装コード

## REST API の作成

```typescript
import AWS from "aws-sdk";

AWS.config.update({
  region: "ap-northeast-1",
  accessKeyId: "XXXXXXXXXXXXXXXXXXX",
  secretAccessKey: "XXXXXXXXXXXXXXXXXXXXXX",
});

// APIGateway
const apiGateway = new AWS.APIGateway();

/**
 * REST APIを作成する
 * @param apiName ステージ名
 * @return REST APIのID
 */
const createRestApi = async (apiName: string): Promise<string> => {
  // REST APIを作成する
  const result = await apiGateway.createRestApi({ name: apiName }).promise();
  // REST APIのIDを記録しておく
  const restApiId = result.id;
  return restApiId;
};
```

## リソースの作成

```typescript
/**
 * REST APIのリソースを作成する
 * @param restApiId REST APIのID
 * @param parentPath 親リソースのパス
 * @param pathPart リソースパス
 * @return Lambda設定
 */
const createRestApiResource = async (restApiId: string, parentPath: string, pathPart: string): Promise<string> => {
  // 親リソースのID
  let parentResourceId: string | undefined = "";
  // リソース一覧を取得する
  const getResourcesResult = await apiGateway.getResources({ restApiId }).promise();
  if (getResourcesResult.items) {
    // 見つかったリソースでループする
    for (const resource of getResourcesResult.items) {
      if (resource.path === parentPath) {
        // 親リソースのIDを取得する
        parentResourceId = resource.id;
        break;
      }
    }
  }
  if (!parentResourceId) {
    // 親リソースが見つからなければエラー
    throw new Error(`親リソース:${parentPath} が見つかりませんでした。`);
  }
  // 親リソースが見つかれば、そこにリソースを追加する
  const result = await apiGateway
    .createResource({
      restApiId: restApiId,
      parentId: parentResourceId,
      pathPart: pathPart,
    })
    .promise();
  // 作成したリソースのIDを取得する
  const resourceId = result.id;
  return resourceId;
};
```

## CORS の有効化 (OPTION メソッドの追加)

```typescript
/**
 * REST APIのリソースにOPTIONメソッドを作成する
 * @param restApiId REST APIのID
 * @param resourceId リソースのID
 */
export const createRestApiOptionMethod = async (restApiId: string, resourceId: string) => {
  // OPTIONSメソッドを作成する
  await apiGateway
    .putMethod({
      restApiId: restApiId,
      resourceId: resourceId,
      httpMethod: "OPTIONS",
      authorizationType: "NONE",
    })
    .promise();
  // メソッドレスポンスを作成する
  await apiGateway
    .putMethodResponse({
      restApiId: restApiId,
      resourceId: resourceId,
      httpMethod: "OPTIONS",
      statusCode: "200",
      responseParameters: {
        "method.response.header.Access-Control-Allow-Headers": false,
        "method.response.header.Access-Control-Allow-Methods": false,
        "method.response.header.Access-Control-Allow-Origin": false,
      },
    })
    .promise();
  // 統合リクエストを作成する
  await apiGateway
    .putIntegration({
      restApiId: restApiId,
      resourceId: resourceId,
      httpMethod: "OPTIONS",
      integrationHttpMethod: "OPTIONS",
      type: "MOCK",
      contentHandling: "CONVERT_TO_TEXT",
      requestTemplates: {
        "application/json": '{"statusCode": 200}',
      },
    })
    .promise();
  // 統合レスポンスを作成する
  await apiGateway
    .putIntegrationResponse({
      restApiId: restApiId,
      resourceId: resourceId,
      httpMethod: "OPTIONS",
      statusCode: "200",
      responseParameters: {
        "method.response.header.Access-Control-Allow-Headers": "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'",
        "method.response.header.Access-Control-Allow-Methods": "'DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT'",
        "method.response.header.Access-Control-Allow-Origin": "'*'",
      },
    })
    .promise();
};
```

## メソッドを追加して Lambda を呼び出すようにする

```typescript
/**
 * REST APIのリソースにPOSTメソッドを作成する
 * @param restApiId REST APIのID
 * @param resourceId リソースのID
 * @param httpMethod メソッド名
 * @param lambdaArn 呼び出すLambdaのARN
 */
export const createRestApiPostMethod = async (restApiId: string, resourceId: string, httpMethod: string, lambdaArn: string) => {
  // メソッドを作成する
  await apiGateway.putMethod({ restApiId, resourceId, httpMethod: httpMethod, authorizationType: "NONE" }).promise();
  // 統合リクエストを作成する
  await apiGateway
    .putIntegration({
      restApiId: restApiId,
      resourceId: resourceId,
      httpMethod: httpMethod,
      integrationHttpMethod: httpMethod,
      type: "AWS_PROXY", // Lambdaプロキシ統合の使用
      contentHandling: "CONVERT_TO_TEXT",
      uri: `arn:aws:apigateway:${AWS.config.region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations`,
    })
    .promise();
};
```

## デプロイする

```typescript
/**
 * REST APIをデプロイする
 * @param restApiId APIのID
 * @param stageName ステージ名
 */
const deployRestApi = async (restApiId: string, stageName: string) => {
  await apiGateway.createDeployment({ restApiId, stageName: stageName }).promise();
};
```

# 使用例

```typescript
(async () => {
  // LambdaのARNは事前に分かっているものとする
  const lambdaArn = "arn:aws:lambda:ap-northeast-1:XXXXXXXXX:function:YYYYYYYYYYY";
  // REST APIを作成する
  const restApiId = await createRestApi("test-api");
  // hogeリソースを作成する
  const resourceId = await createRestApiResource(restApiId, "/", "hoge");
  // hogeリソースのCORSを有効化する (OPTIONSメソッドを作成)
  await createRestApiOptionMethod(restApiId, resourceId);
  // hogeリソースにPOSTメソッドを作成し、Lambdaを呼び出すようにする
  await createRestApiPostMethod(restApiId, resourceId, "POST", lambdaArn);
  // ステージ:dev としてデプロイする
  await deployRestApi(restApiId, "dev");
})();
```