Terraformを使ってAPI Gatewayとlambda(aws-serverless-express)でAPIを構築する


はじめに

最近サーバーサイドやインフラを勉強中の新米エンジニアです。
勉強のためにnodejsとexpressでWebAPIを構築してローカルでモックとして利用することが結構あるのですが、お盆休みを使ってデプロイ方法を勉強しました。
AWSのAPI GatewayとLambdaを利用します。仕事でALBとLambdaの組み合わせは使っているのですが、個人でやるならコスト的にAPI Gatewayかなと思い選定しました。

前提

  • terraform&aws cliの環境構築済み
  • nodejsの環境構築済み

API Gateway

フルマネージド型サービスの Amazon API Gateway を利用すれば、開発者は規模にかかわらず簡単に API の作成、公開、保守、モニタリング、保護を行えます。API は、アプリケーションがバックエンドサービスからのデータ、ビジネスロジック、機能にアクセスするための「フロントドア」として機能します。API Gateway を使用すれば、リアルタイム双方向通信アプリケーションを実現する RESTful API および WebSocket API を作成することができます。API Gateway は、コンテナ化されたサーバーレスのワークロードやウェブアプリケーションをサポートします。

APIの作成や管理が容易に行えるAWSのマネージドなサービスです。ECSやEC2を使わなくてもAPIの構築ができて、APIのコール数や転送データでの従量課金なのでコストも低く抑えられます。API構築するならEC2やECSよりもなるべくこちらを利用したいですね。どちらでも構築できそうな場合にAPI Gatewayを利用するかどうかはタイムアウト設定が上限29秒までっていうところがポイントとなりそうです。
色んな使い方がありそうですが、今回はパスやメソッドごとの処理はLambda側で行うので、API Gatewayでは入り口を1つ用意してあげるだけになります。

Lambda

https://aws.amazon.com/jp/lambda/
言わずもがなですね。
サーバーの管理が不要でコードを実行できて、実行時間とリクエスト数で課金ですが100万リクエスト/月の無料枠があります。個人で使う分にはコストを意識する必要はほぼないと思います。

今回は実行環境をnodejs12.xにして、aws-serverless-expressを利用して作成したLambda関数を使います。

aws-serverless-express

https://github.com/awslabs/aws-serverless-express
webアプリケーションフレームワークであるexpressがLambdaで動くすごいやつです。expressを使ったnodejsのプロジェクトを少しいじる以外はほぼそのままでLambdaで動かせます。
リポジトリのREADMEに書かれている通りにやれば実はコマンド叩くだけでデプロイまでできちゃうのですが、terraformの勉強を兼ねていたのでterraformで書いてます。

アーキテクチャ

ざっくり下の図のような感じになります。
API Gatewayのエンドポイントは1つで、そこに来たリクエストをaws-serverless-expressとexpressで実装したLambdaにプロキシする。

実装

作業ディレクトリの作成

$ mkdir ./serverless-express-app
$ cd serverless-express-app

まずはlambdaで動かすnodejsのコードを準備していきます。
lambda用のnodejsプロジェクトディレクトリをserverless-express-app配下に作成

$ mkdir ./lambda
$ cd lambda

プロジェクトを作成。全部Enterで進めます。

$ npm init
$ npm i express
$ npm i aws-serverless-express

エントリポイントとなるindex.jsを作成

index.js
const awsServerlessExpress = require('aws-serverless-express');
const app = require('./app');
const server = awsServerlessExpress.createServer(app);

exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context);

index.jsの2行目でrequireしているapp.jsを作成する。ほぼ普通にexpress書く感じですね。

app.js
const serverlessExpress = require('aws-serverless-express/middleware');
var express = require('express');
var app = express();

app.use(serverlessExpress.eventContext());

app.get('/', (req, res) => {
    res.send({message: "Hello World"});
});

module.exports = app

ここまでで今回必要なlambdaのコードは完成です。
気になる方はlambdaディレクトリをzip化してマネジメントコンソールからlambda関数を作成してテストしてみてください。うまくいってれば{\"message\":\"Hello World\"}が返ってきているはずです。

それではlambdaのコードができたので、terraformを書いていきます。
tfファイルはserverless-express-appディレクトリ配下に作成してください。

aws.tf
provider "aws" {
  version = "~> 3.0"
  region = "ap-northeast-1"
}
lambda.tf
data "archive_file" "your_function_name" {
  type        = "zip"
  source_dir  = "lambda"
  output_path = "./your_function_name.zip"
}

resource "aws_lambda_function" "your_function_name" {
  filename         = data.archive_file.your_function_name.output_path
  function_name    = "your_function_name"
  role             = aws_iam_role.your_lambda_role.arn
  handler          = "index.handler"
  source_code_hash = data.archive_file.your_function_name.output_base64sha256
  runtime          = "nodejs12.x"

  memory_size = 128
  timeout     = 60
}

resource "aws_iam_role" "your_lambda_role" {
  name = "your_lambda_role"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
POLICY
}

resource "aws_lambda_permission" "apigw_lambda" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.your_function_name.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn = "${aws_api_gateway_rest_api.your_api.execution_arn}/*/*/*"
}
api.tf
resource "aws_api_gateway_rest_api" "your_api" {
  name = "your_api"
  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

resource "aws_api_gateway_method" "your_api_method_root" {
  rest_api_id   = aws_api_gateway_rest_api.your_api.id
  resource_id   = aws_api_gateway_rest_api.your_api.root_resource_id
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_resource" "your_api_resource" {
  path_part   = "{proxy+}"
  parent_id   = aws_api_gateway_rest_api.your_api.root_resource_id
  rest_api_id = aws_api_gateway_rest_api.your_api.id
}

resource "aws_api_gateway_method" "your_api_method" {
  rest_api_id   = aws_api_gateway_rest_api.your_api.id
  resource_id   = aws_api_gateway_resource.your_api_resource.id
  http_method   = "ANY"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "your_api_proxy" {
  rest_api_id             = aws_api_gateway_rest_api.your_api.id
  resource_id             = aws_api_gateway_resource.your_api_resource.id
  http_method             = aws_api_gateway_method.your_api_method.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.your_function_name.invoke_arn
}

resource "aws_api_gateway_integration" "your_api_proxy_root" {
  rest_api_id             = aws_api_gateway_rest_api.your_api.id
  resource_id             = aws_api_gateway_rest_api.your_api.root_resource_id
  http_method             = aws_api_gateway_method.your_api_method_root.http_method
  integration_http_method = "POST"
  type                    = "AWS_PROXY"
  uri                     = aws_lambda_function.your_function_name.invoke_arn
}

resource "aws_api_gateway_deployment" "your_deployment" {
  depends_on = [aws_api_gateway_integration.your_api_proxy]

  rest_api_id = aws_api_gateway_rest_api.your_api.id
  stage_name  = "dev"
}

aws_api_gateway_integrationのtypeを"AWS_PROXY"に設定した場合、integration_http_methodは"POST"にしないといけないみたいですね。結構はまりました。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/api_gateway_integration#argument-reference

tfファイルが準備できたのでデプロイします。

$ terraform init
$ terraform apply

AWSマネジメントコンソールからAPI GatewayとLambdaを確認しましょう。
確認できたら、APIのステージからURLを確認し、アクセスしてみます。

下のスクリーンショットのようにレスポンスが返ってきていればOKです。

あとは、expressの書き方で好きなようにAPIを作っていくだけです。この記事ではHello Worldまでに留めます。

最後に

terraformの勉強と思ってやってみたのですが、最終的なコード量の割にかなり時間がかかりました。まだまだ勉強する必要がありますね。これくらいはさくっと書けるようになりたいです。
指摘などあればコメントよろしくお願いします!