- 辻大志郎
- 約 5,800 文字
- 3,700 View
はじめに
こんにちは、TIGの辻です。
業務アプリケーションのビジネスロジックをサーバーレスで実装することも増えてきました。AWSサービストリガによるLambda関数起動の記事にもあるようにAPI GatewayとLambda関数を組み合わせてHTTPサーバを提供することも容易にできます。バックエンドのWeb APIをLambda関数で動かすユースケースがよくあるパターンです。Lambda関数とアプリケーションロジックの実装は密結合になりやすいですが、HTTPサーバで動作するように実装して、Lambda関数として組み込むと、従来のHTTPサーバを実装するノウハウを活かしつつ、サーバレスで動作するバックエンドのWeb APIを構築できます。
本記事ではGoを用いてサーバレスなWeb APIサーバをAWS上に1から構築するチュートリアルです。
go-swaggerに閉じた入門記事として、以下もおすすめです。
概要
AWS上に構築するインフラはこんな感じです。シンプル。
最初に本チュートリアルで使用しているバージョンを記載します。以下のとおりです。
- 各種ソフトウェアバージョン
| # | ソフトウェア/ライブラリ | バージョン |
|---|---|---|
| 1 | AWS CLI | 2.0.48 |
| 2 | LocalStack | 0.11.5 |
| 3 | Terraform | 0.13.2 |
- Goの主要ライブラリのバージョン
| # | ソフトウェア/ライブラリ | バージョン |
|---|---|---|
| 1 | go-swagger/go-swagger | 0.25.0 |
| 2 | guregu/dynamo | 1.19.1 |
- LocalStackの起動
ローカル開発にはDockerを使ってLocalStackを動かしていきます。LocalStackはバージョン 0.11.0 からすべてのサービスに 4566 ポートを使います。0.10.x 以下のバージョンと使用するポートが異なるため、古いバージョンを利用している方は注意してください。
docker run -it -p 4566:4566 -e SERVICES=dynamodb -e DEFAULT_REGION=ap-northeast-1 localstack/localstack:0.11.5 |
- AWS CLIのインストール、設定
また、Lambda関数のデプロイなどに aws コマンドを使いますので、AWS CLI バージョン 2 のインストールを参考にAWS CLIをインストールしましょう。使っているOSのインストール手順に従ってインストールしてください。
以下のように出力されれば成功です。(以下はWindowsの例)
aws --version |
CLIでAWSのリソースにアクセスできるように設定しておきましょう。デフォルトのプロファイルとして設定しています。必要に応じてプロファイル名を指定できます。
aws configure |
テスト用にダミー用の local プロファイルも設定しておきましょう。
aws configure --profile local |
インフラ構築
AWS上に構築するインフラはTerraformで使います。一時的な動作確認で使うリソースの場合GUIでポチポチリソースを作成しても問題ないですが、業務でインフラを構築する場合はAWS CloudFormationやTerraformを使うことが一般的です。GUIでポチポチする際に暗黙的に作成されるリソースもしっかり把握していきましょう。
Terraformに関しては…
の記事もあわせて見てみてください。その他にも技術ブログにTerraformに関する記事がたくさんあります。
またHashiCorp Learnのドキュメントも参考になります。
TerraformはInstall Terraformを参考に2020年9月にリリースしたv0.13.2をインストールしておきます。v0.12.xでも問題ないです。
terraform -v |
今回はローカルPCから terraform コマンドを実行してAWSにリソースを作っていきます。
最終的なファイル構成は以下のようになります。
. |
まずはプロバイダの設定をしておきます。
provider "aws" { region = "ap-northeast-1" alias = "ap-northeast-1" version = ">= 3.7.0" } |
provider.tf を記述したら terraform init をしましょう。
terraform init |
続いてAPI GatewayとLambda関数を実装します。まずはLambda関数とAPI Gatewayで必要なIAMを記述します。API GatewayはLambda関数を呼び出す操作、Lambda関数ではCloudWatch Logsにログを書き込む操作、DynamoDBを操作するIAMを定義します。
data "aws_iam_policy_document" "example_api_policy" { statement { effect = "Allow" principals { type = "*" identifiers = [ "*"] } actions = [ "execute-api:Invoke" ] resources = [ "arn:aws:execute-api:ap-northeast-1:*:*/*/*" ] } } data "aws_iam_policy_document" "example_lambda" { statement { effect = "Allow" actions = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] resources = [ "arn:aws:logs:*:*:*"] } } |
上記のポリシードキュメントをIAMポリシーとして定義します。
resource "aws_iam_policy" "example_lambda" { name = "example-lambda" policy = data.aws_iam_policy_document.example_lambda.json } |
IAMロールを定義します。
resource "aws_iam_role" "example_lambda" { name = "example-lambda" assume_role_policy = file("assume_role/lambda.json") } |
信頼ポリシーは以下のようになります。
{ |
先程記述したIAMロールにIAMポリシーをアタッチします。
resource "aws_iam_role_policy_attachment" "example_api" { role = aws_iam_role.example_lambda.name policy_arn = aws_iam_policy.example_lambda.arn } |
IAMの設定は以上で完了です。
続いてAPI GatewayのRESTのリソースを作っていきましょう。先程作成したIAMポリシードキュメントを使います。
resource "aws_api_gateway_rest_api" "example_api" { name = "example-api" description = "example serverless api" policy = data.aws_iam_policy_document.example_api_policy.json } |
resource "aws_api_gateway_resource" "example_api" { rest_api_id = aws_api_gateway_rest_api.example_api.id parent_id = aws_api_gateway_rest_api.example_api.root_resource_id path_part = "{proxy+}" } |
APIリクエストに対する認可はなしにします。必要な場合は authorization パラメータを用いて設定します。
resource "aws_api_gateway_method" "example_api_get" { authorization = "NONE" http_method = "GET" resource_id = aws_api_gateway_resource.example_api.id rest_api_id = aws_api_gateway_rest_api.example_api.id } resource "aws_api_gateway_method" "example_api_post" { authorization = "NONE" http_method = "POST" resource_id = aws_api_gateway_resource.example_api.id rest_api_id = aws_api_gateway_rest_api.example_api.id } |
Lambdaプロキシ統合のGETリクエストを実装する場合においても integration_http_method パラメータは POST と設定する必要があります。
resource "aws_api_gateway_integration" "example_api_get" { rest_api_id = aws_api_gateway_rest_api.example_api.id resource_id = aws_api_gateway_method.example_api_get.resource_id http_method = aws_api_gateway_method.example_api_get.http_method # "GET"ではなく"POST"にする必要がある integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.example_api.invoke_arn } resource "aws_api_gateway_integration" "example_api_post" { rest_api_id = aws_api_gateway_rest_api.example_api.id resource_id = aws_api_gateway_method.example_api_post.resource_id http_method = aws_api_gateway_method.example_api_post.http_method integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.example_api.invoke_arn } |
resource "aws_api_gateway_deployment" "example_api" { depends_on = [ aws_api_gateway_integration.example_api_get, aws_api_gateway_integration.example_api_post, ] rest_api_id = aws_api_gateway_rest_api.example_api.id stage_name = "test" stage_description = "test stage" } |
GETリクエストやPOSTリクエストを呼び出したときに起動するLambda関数のリソースを定義していないため、上記のtfファイルはエラーになります。Lambda関数のリソースを作ります。
Lambda関数はアプリケーション側からデプロイできるようにTerraform側ではLambda関数の初期構築時のみ使用するダミーのzipファイルを使って構築するのがおすすめです。
Lambdaの handler パラメータは、ビルドして生成した実行可能なファイル名と同じである必要があります。
resource "aws_lambda_function" "example_api" { filename = "dummy_function.zip" function_name = "example-api" role = aws_iam_role.example_lambda.arn handler = "lambda" runtime = "go1.x" memory_size = 128 timeout = 900 } |
Lambda関数をAPI Gatewayから呼び出せるように明示的に許可します。
resource "aws_lambda_permission" "example_apigateway_lambda" { action = "lambda:InvokeFunction" function_name = aws_lambda_function.example_api.function_name principal = "apigateway.amazonaws.com" source_arn = "${aws_api_gateway_rest_api.example_api.execution_arn}/*/*/*" } |
dummy_function.zip はビルド可能な適当な main.go を dummy_function に格納してzip化しておきます。ファイルが存在しないとエラーになります。
package main |
API GatewayとLambda関数のリソースを作成する準備が整いました。terraform plan terraform apply をしてリソースを作成します。
terraform apply |
Goのアプリケーション開発
Web API開発です。今回はサンプルアプリケーションなのでGETとPOSTだけ対応している以下の2つのパスを用意します。
| メソッド | パス | 説明 |
|---|---|---|
| GET | /v1/users |
登録されているユーザ一覧を返却します |
| POST | /v1/users |
ユーザを登録します |
それでは go mod init として開発を始めていきましょう。
go mod init example |
API定義
GoのWebアプリケーションフレームワークはEchoやGinやchiやgo-swaggerなどいろいろありますが、今回はgo-swaggerを用いることにします。どのWebアプリケーションフレームワークを使うかはGopherの間でも意見が分かれるところなので、使い慣れたWebアプリケーションフレームがあれば、それを使うのもよしです。上記のメソッドとパスをSwaggerで記述していきます。swagger.yaml にすると以下のようになります。ちなみにSwaggerの書き方・規約はスキーマファースト開発のためのOpenAPI(Swagger)設計規約 の記事がオススメです。
swagger.yaml
swagger: "2.0" |
go-swagger はInstallingを参考にインストールします。今回は2020/09/23現在の最新バージョンである 0.25.0 をインストールします。以下のように出力されていればOKです。
swagger version |
データストア
ユーザの情報を格納するDynamoDBのテーブル名は users としておきます。スキーマは以下です。
| 論理名 | 物理名 | キー |
|---|---|---|
| ユーザID | user_id | ハッシュキー |
| ユーザ名 | user_name | - |
ついでにTerraformを用いてAWS上にリソースを作成しましょう。キャパシティはオンデマンドモードにしておきます。
resource "aws_dynamodb_table" "example_users" { name = "example-users" billing_mode = "PAY_PER_REQUEST" hash_key = "user_id" attribute { name = "user_id" type = "S" } } |
Lambda関数の環境変数からDynamoDBのテーブル名を取得できるようにLambda関数の環境変数に追加しておきます。環境変数でDynamoDBのテーブル名を設定できるようにしておくと、ローカルでのテストする際にAWS上に構築するテーブル名と別の名前を指定でき、便利です。
resource "aws_lambda_function" "example_api" { |
data "aws_iam_policy_document" "example_lambda_policy" { |
新しいリソースを定義したら terraform apply しておきます。
パッケージ構成
ちょっとしたLambda関数であれば main.go の1ファイルで良い場合もありますが、バックエンドのWeb APIを提供するとなるとそうはいかないでしょう。Goのパッケージ構成は悩みポイントの1つです。プロジェクトの規模や開発メンバーのスキルセットなどにもよると思いますが、個人的にはフラットなパッケージ構成を導入することが多い気がします。あなたのGoアプリ/ライブラリのパッケージ構成もっとシンプルでよくない?やgo-swaggerを用いたWebアプリケーション開発Tips19選の記事を参考にしてみてください。今回は以下のようなフラットパッケージとします。
. |
以下のようにディレクトリを作っておきます。
mkdir -p %GOPATH%\src\github.com\d-tsuji\example |
ビルド
ビルドなどのタスクはMakefileに記述しておきます。
.PHONY: deps |
先程作成した swagger.yml のAPI定義を元に make generate-server で go-swagger でコードを生成します。
make generate-server |
go-swagger で生成したファイルでビルドに必要なモジュールを go.mod に追加します。
go get github.com/go-openapi/runtime |
ハンドラ実装
準備が整ったので、ハンドラの実装をしていきましょう。
まずは db.go を実装してDynamoDBに接続します。ローカルでの開発の場合は 4566 ポートで起動しているLocalStackに接続します。DynamoDBのGoのクライアントライブラリは…
などがあります。個人的なおすすめは guregu/dynamo です。本チュートリアルでは guregu/dynamo を利用することにします。guregu/dynamo の使い方については「DynamoDB×Go連載#1 GoでDynamoDBでおなじみのguregu/dynamoを利用する」の記事も見てみてください。
go get github.com/guregu/dynamo |
package example |
続いてハンドラの実装します。まずは以下の GET を扱うハンドラから実装していきます。
| メソッド | パス | 説明 |
|---|---|---|
| GET | /v1/users |
登録されているユーザ一覧を返却します |
DynamoDBから登録されているすべてのユーザを取得する処理を実装します。
DynamoDBとマッピングするモデルは以下です。
package example |
テーブルから全アイテム取得するためにScanを行います。
package example |
続いて上記を使ったハンドラを実装します。
package example |
ハンドラのテストも実装しましょう。テスト時はDynamoDBの接続先をLocalStackに上書きして、テスト用の設定にします。パッケージ構成によっては接続先の値は関数の引数で渡す、などが必要になるでしょう。
単体テストは実装の詳細をテストしないように、粒度を粗めにしておきます。ハンドラのリクエストに対して想定するJSONのレスポンスが取得できているかどうか確認します。テストファイルは want_get_users_1.json want_get_users_2.json としておきます。
package example |
[ |
[] |
Goのテストを実行するとPASSすることがわかります。ハンドラの実装ができました。
make test |
POSTのハンドラも同様に実装・テストできますが、本チュートリアルでは省略します。本チュートリアルの内容はGitHubにコミットしてあるので、そちらを参照ください。
go-swagger はハンドラの実装とHTTPリクエストのパスのマッピングを自動生成したファイルの中に記述します。今回の場合は configure_example_app.go です。
// This file is safe to edit. Once it exists it will not be overwritten |
Lambda関数インテグレーション
go-swaggerで実装したWebアプリケーションサーバをLambda関数として動かすようにします。GoのLambda関数として有効なシグネチャは以下の通りです。
- func () |
TIn や TOut は encoding/json でエンコード・デコードできる型を指定できます。
Lambda関数はAPI Gatewayのリクエストをトリガーに起動します。Lambda関数のAPI Gatewayのリクエスト events.APIGatewayProxyResponse をGoのHTTPサーバで扱えるような *http.Request に変換する必要があります。
を用いると簡単に変換できます。もちろん go-swagger だけでなく主要なGoのWebアプリケーションフレームに対応しています。
package main |
ビルド/デプロイ
準備は整いました! Goのファイルをビルドしzip化してAWS Lambda関数にデプロイしましょう。デプロイのコマンドはMakefileにタスクとして記述していました。
- Makefile
deploy: zip |
それではビルドしてデプロイします。
make deploy |
上記のように出力されていればデプロイは完了です。継続的なデプロイを実施したい場合はdevelopブランチなどにPRがマージされたタイミングで開発環境にLambda関数をデプロイするといった内容をGitHub ActionsやCircleCIの設定に組み込むとよいでしょう。
さてCLIでDynamoDBにデータをPutして、APIのレスポンスを確認してみましょう。
aws dynamodb put-item --table-name example-users --item '{"user_id": {"S": "001"}, "user_name": {"S": "Gopher"}}' |
今回はお手軽にcurlでAPIにリクエストしてレスポンスを確認します。
curl -i https://${rest-api-id}.execute-api.ap-northeast-1.amazonaws.com/test/v1/users |
API Gatewayから想定通りのレスポンスが返ってきました! 本チュートリアルでは /v1/users へのGETリクエストのサンプルだけでしたが、新しいAPI定義が必要な場合 swagger.yaml に追加して本チュートリアルのようにすすめていけば容易に拡張できます。
チュートリアルのすべてのサンプルはGitHubの以下のリポジトリにおいてあります。
| # | 項目 | リポジトリ |
|---|---|---|
| 1 | GoによるWeb APIの実装 | d-tsuji/serverless-api-go-tutorial |
| 2 | Terraformによるインフラ実装 | d-tsuji/serverless-api-infra-tutorial |