TerraformでAPI Gateway + Lambdaの構成テンプレート

API Gateway + LambdaでのAPIをTerraformで実装するためのテンプレートです。

API GatewayとLambdaにはそれぞれIAM Roleを作成してアタッチしています。

```main.tf
variable aws_profile {}
variable aws_region {}
variable resource_prefix {}

provider "aws" {
   profile = var.aws_profile
   region = var.aws_region
}

################################
# LambdaにアタッチするIAM Role
################################

resource "aws_iam_role" "lambda_role" {
  name               = "${var.resource_prefix}-lambda-role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}

resource "aws_iam_role_policy_attachment" "lambda_policy" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

data "aws_iam_policy_document" "lambda_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

################################
# API GatewayにアタッチするIAM Role
################################

resource "aws_iam_role" "api_gateway_role" {
  name               = "${var.resource_prefix}-apigateway-role"
  assume_role_policy = data.aws_iam_policy_document.api_gateway_assume_role.json
}

resource "aws_iam_role_policy_attachment" "api_gateway_policy_logs" {
  role       = aws_iam_role.api_gateway_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
}

resource "aws_iam_role_policy_attachment" "api_gateway_policy_lambda" {
  role       = aws_iam_role.api_gateway_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaRole"
}

data "aws_iam_policy_document" "api_gateway_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      type        = "Service"
      identifiers = ["apigateway.amazonaws.com"]
    }
  }
}

################################
# Lambda
################################

# apiディレクトリにLambdaのソースコードがある前提
# apiディレクトリを api.zip という名前に固めて resource "aws_lambda_function" "api" から参照できるようにする
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "api"
  output_path = "api.zip"
}

resource "aws_lambda_function" "api" {
  depends_on       = [aws_iam_role.lambda_role]
  filename         = data.archive_file.lambda_zip.output_path
  function_name    = "${var.resource_prefix}-api"
  role             = aws_iam_role.lambda_role.arn
  handler          = "index.lambda_handler"
  runtime          = "nodejs14.x"
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256
}

################################
# API Gateway
################################

resource "aws_api_gateway_rest_api" "api" {
  name = "${var.resource_prefix}-api"

  body = jsonencode({
    openapi = "3.0.1"
    info = {
      title   = "api"
      version = "1.0"
    }
    paths = {
      "/path1" = {
        get = {
          x-amazon-apigateway-integration = {
            httpMethod           = "POST"  # LambdaへのアクセスはPOSTでないといけないらしい
            payloadFormatVersion = "1.0"
            type                 = "AWS_PROXY"
            uri                  = aws_lambda_function.api.invoke_arn
            credentials          = aws_iam_role.api_gateway_role.arn
          }
        }
      }
    }
  })
}

resource "aws_api_gateway_deployment" "deployment" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  depends_on  = [aws_api_gateway_rest_api.api]
  stage_name  = "prod"
  triggers = {
    # resource "aws_lambda_function" "api" の内容が変わるごとにデプロイされるようにする
    redeployment = sha1(jsonencode(aws_api_gateway_rest_api.api))
  }
}

data "aws_iam_policy_document" "api_gateway_policy" {
  statement {
    effect = "Allow"
    principals {
      type = "*"
      identifiers = ["*"]
    }
    actions   = ["execute-api:Invoke"]
    resources = ["${aws_api_gateway_rest_api.api.execution_arn}/*"]
  }
}

resource "aws_api_gateway_rest_api_policy" "policy" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  policy = data.aws_iam_policy_document.api_gateway_policy.json
}
```

次のファイルはLambdaのソースコードのサンプルです。 `api/` ディレクトリに入れておきます。Node.js です。

```api/index.js
exports.lambda_handler = async (event, context) => {
  return new Promise(function(resolve, reject) {
    resolve({
        "statusCode": 200,
        "headers": {
            "Content-Type": "application/json",
        },
        "body": JSON.stringify({message: "OK", event: event}),
    });
    return;
  });
};
```

`main.tf` から参照されている変数の定義例です。

```terraform.tfvars
aws_profile = "default"
aws_region = "ap-northeast-1"
resource_prefix = "APINAME"
```

`terraform apply` を実行するとAPI Gateway、Lambda、IAM Roleが作成されます。

以下はAPIの実行結果例です。

```plain
$ curl 'https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/path1?q=abc' | jq .
```

```json
{
  "message": "OK",
  "event": {
    "resource": "/path1",
    "path": "/path1",
    "httpMethod": "GET",
    "headers": {
      "Accept": "*/*",
      ...
    },
    "multiValueHeaders": {
      "Accept": [
        "*/*"
      ],
      ...
    },
    "queryStringParameters": {
      "q": "abc"
    },
    "multiValueQueryStringParameters": {
      "q": [
        "abc"
      ]
    },
    ...,
    "requestContext": {
      ...
    },
    "body": null,
    "isBase64Encoded": false
  }
}
```

# 追記 2023/06/22

API Gatewayの代わりにCloudFrontを使うバージョンも次の記事で書きました。

https://qiita.com/suzuki-navi/items/cfd054b931ff3cb12cdf