【Go, AWS】低予算APIサーバー構築を最初から最後まで【Golang, Lambda, DynamoDB, API Gateway, Cognito】

:::note
* `Go` -> `Python`差し替え編
* クライアント（`Vue.js`）編

執筆中です。
:::

:::note warn
価格・安全性など重要事項は随時ご自分で調べ、自己責任で作業をしてください。
筆者はいかなる損害について責任を負いかねます。
:::

## はじめに
低予算（規模が十分小さければ無料）で[REST APIサーバー](https://qiita.com/masato44gm/items/dffb8281536ad321fb08)を作りましょう。

DBあるよ！

## 概要
まず、低予算で推奨されるのが [**サーバーレス**](https://business.ntt-east.co.jp/content/cloudsolution/column-414.html) です。
厳密性を無視して簡単に言うと、 **常時待機するのではなく、必要な時だけ動作するサーバー** というイメージです。低予算でいける理由がこれでわかりますね。

思ったよりやること、というよりかは扱わないといけない機能が多いです。

![AWS_Rest.drawio (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/4c681a27-d252-1d4f-f7ec-336c27a23b58.png)

* Cognito
    ユーザー管理を全部代行してくれる[神機能の王、神機能キング](https://qiita.com/uts1_6/items/11e348ead4bae71571b8)です。
    **ログイン画面** のフロントエンドすら用意してくれます。[めっちゃ安いというかだいたい無料です。](https://aws.amazon.com/jp/cognito/pricing/)

    もしユーザー認証が必要なければ、Cognitoを使わず、一個下に書いてある`API Gateway`で全員共通のAPIキーを作り、それで認証することもできます。

* API Gateway
    その名の通り、API通信の「門」です。
    **アクセス負荷**, **アクセス制限**, **バリデーション** など防衛的なお話はだいたいここの機能で行います。

* AWS Lambda
    **サーバーレスAWSの肝要です**
    Lambdaという名の通り、ラムダ関数を用意するというコンセプトです。
    `Go`なり`Python`なり`C#`なり各々の言語で、
    * 引数: リクエスト
    * 返り値: レスポンス

    の関数を作るというわけです。

* Dynamo DB
    AWSが用意しているDBの中で唯一無料枠が提供されている種類です。

    非リレーショナルで、データは`JSON`で扱われ、`SQL`は使えません。

:::note
参考
https://qiita.com/omo_taku/items/b1c076cf6aa1b8badb33
https://aws.amazon.com/jp/dynamodb/pricing/
:::

## 実装
### Lambda関数をGolangで書く

:::note warn
中級以上のプログラマー向けにコーディングの概形や雰囲気を把握してもらうための記述をしていまして、各記述の詳細は各自お調べください。
:::

まずは、通常のGolang書きと同じように、モジュール作るべく`go mod init`などをしてください。

#### POSTリクエストを受け取り、レスポンスを返す
下記のパッケージが必要になります。
```
go get "github.com/aws/aws-lambda-go/events"
go get "github.com/aws/aws-lambda-go/lambda"
```

まず、大枠から。

```go:main.go
package main

import (
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

//リクエストJSON
type Request struct {
	vari   string `json:"vari"`
}

//レスポンスJSON
type Response struct {
	vari   string `json:"vari"`
}

func requestHandler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	// リクエストを受け取る
	request := Request{}
	err := json.Unmarshal([]byte(req.Body), &request)
	if err != nil {
		//エラーログ
		fmt.Println("Error parsing request: ", err, "; ", req.Body)	

      //HTTPステータス400のレスポンスを返す
		return events.APIGatewayProxyResponse{StatusCode: 400}, fmt.Errorf("error parsing request: %v", err)
	}

     //...いろいろな、処理...

	// レスポンス作成
	response := Response{Token: token}
	responseJson, err := json.Marshal(response)
	if err != nil {
		// エラーログ
		fmt.Println("Error marshalling response: ", err)

          //HTTPステータス500のレスポンスを返す
		return events.APIGatewayProxyResponse{StatusCode: 500}, fmt.Errorf("error marshalling response: %v", err)
	}
	return events.APIGatewayProxyResponse{StatusCode: 200, Body: string(responseJson)}, nil
}

func main() {
	lambda.Start(requestHandler)
}
```

こうやってリクエストを受け取り、レスポンスを返します。（不親切な説明）

#### CognitoユーザーID取得
もし`Cognito`のユーザーIDを取得したい場合は、

```go:CognitoIdGetter.go
func getUserId(req events.APIGatewayProxyRequest) (string, error) {
	cognitoAttributes, ok := req.RequestContext.Authorizer["claims"].(map[string]interface{})
	if !ok {
		return "", fmt.Errorf("couldn't get cognito attributes")
	}
	
	userId, ok := cognitoAttributes["sub"].(string)

	if !ok {
		return "", fmt.Errorf("couldn't get userId")
	} else {
		return userId, nil
	}
}
```

ちょっと大変ですがこんな感じ。
（要するに`Authorizer` -> `claims` -> `sub`からCognito IDを取得できます）

#### Dynamo DBとのデータ出し入れ
[github.com/guregu/dynamo](https://github.com/guregu/dynamo)を使うととても便利です。

こちら参照（パッケージ作成者本人による解説記事）: [気楽にDynamoDBを使おう](https://qiita.com/guregu/items/2e9ac305791935b86f5d)

#### Zipにする
AWS Lambdaに投稿するには、zipにしなければなりません。

筆者はWindowsのPower Shellを使っておりますが、こちらの呪文をコピペして
```
cd (main.goがあるディレクトリ)
$env:GOOS = "linux"
$env:GOARCH = "amd64"
$env:CGO_ENABLED = "0"
go build -o bootstrap main.go
```
当該ディレクトリに生成された`bootstrap`ファイル **一つだけ** を`.zip`に圧縮しています。
**この`zip`ファイルをAWS Lambdaにアップロードします**

----------------------
いよいよマウスでカチカチするノーコードタイムです。この設定作業が楽しい人がいるか分かりませんが、プログラミングだけが好きな筆者にとっては苦痛ですね。

もちろんですが、AWSのユーザー登録をして、AWSコンソールにアクセスしてくださいね。

### Cognito設定
**ユーザープール** を作ります。IDプールは使えなかったです（泣）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/55e6d483-f061-4585-19a8-dc80d36c64f1.png)

サインインオプション。ログイン画面で入力するアレを指定できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/23052c4e-9d06-5ecd-b38d-d7d5652afcc9.png)

次のステップに移りましてセキュリティ要件。
デフォルトのパスワード要件がけっこうガッチリしていますので、必要にあわせて調整。
二段階認証の可否も必要に合わせて調整（デフォルトは「必須」になっています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/740f8986-5b7d-4fae-8ce4-9a5358daac12.png)

次のステップに移りましてサインアップエクスペリエンス
* **自己登録**とは **ユーザー登録機能** を指します。 **オフにすると、管理者が一人ひとり手動で登録する、という仕様になります**
* **検証と確認** とは、ユーザー登録の際のメアド認証ないし電話番号認証のメールを飛ばすやつです。必要に応じて。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/39e73d4d-0548-282c-dd0e-a1cab062dfe7.png)

次のステップに移りましてアプリケーション設定
* **ホストされたUI** は **Cognitoがログイン画面を代わりに提供してくれる神機能** です。後述参照。
* 身内だけでユーザー管理をする場合は **クライアントシークレット** を設定するべきだそうですが、公開APIを作成する場合は **生成しない** に設定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/99f7da6b-19e1-4d02-4c95-da0027d780bd.png)

**ホストされたUI** にチェック入れると、こんな設定画面が。
そのCognitoドメインってのが、Cognitoが提供してくれるログイン画面になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/639cb1f6-288b-01b4-3ba5-835f5c5c7d85.png)

これでCognitoの設定完了です。

-------------------

### Dynamo DB
左のメニューから「テーブル」を選んで、「テーブルの作成」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/5a0e30d7-b285-819f-1101-791ef342dfa2.png)

テーブル作成の設定はこれだけ。
**パーティションキー** は、RDBでいうところの「 **主キー** 」です。
まあ各データのどの項目を「ID」にするか、ということです。だいたい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/8a716ba8-097e-7a2c-7db2-5fbc947aff70.png)

これでDB設定は終わりです。

JSON管理DBですので(?)、パーティションキー以外に **列名などの事前登録はせず、Lambda側に委ねられています**

------------
### IAM
現状のままでは、 **LambdaがDynamo DBを編集する権限がありません**

AWSのサービス一覧に「IAM」というのがありまして、そこから設定しないといけません。

「**ロール**」へ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/67894446-4593-7091-fe06-49bc934de4d4.png)

**ロールを作成**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/870697cc-e406-4e14-6d26-bed89500c7d6.png)

* **信頼されたエンティティタイプ**は「AWSのサービス」（Lambdaのロールを作っているので）
* **ユースケース** は **Lambda** を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/3f093380-ba7a-dd60-d8ab-bb0af0bccad2.png)

具体的な権限を付与します。
「dynamo」と検索すると4つ出てきますね。
* AmazonDynamoDB**FullAcccess**
* AmazonDynamoDB**Execution**

を選択するとデータの出し入れができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/ebbc68c6-3777-81e0-1875-a7cb27f020b8.png)

これでDynamoDBデータ出し入れロールが作成できました。

---------------
### Lambda
「関数を作成」を選択。
* 「一から作成」
* **Golang** の場合は、**ランタイム** を **「Amazon Linux」** に
* **実行ロール** は、**IAMで作成したDynamoアクセスロール** をセット。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/addf5129-269b-43ec-3415-b0773251c233.png)

Zipをアップロード。
Golangのところで作成したzipファイルをドラッグアンドドロップでアップロード。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/7a91fb32-029a-f552-0665-43378c374931.png)

終わりです。

テストしたい場合は、「テスト」から **テンプレート** から 「**apigateway-aws-proxy**」を選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/0e020cef-1629-f2f7-b9a5-06b21cca3a8f.png)

リクエストボディはどうしてかこんな感じでJSONをエスケープ付きの文字列にしないといけないようです。
[文字列化はこのサイトが便利](https://atwata.com/json-stringify/)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/5c21ce94-c881-20a8-56b3-967ed7a0514e.png)

-----------------------
### API Gateway
ここの設定が複雑で苦痛です。

#### APIを作成
APIを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/0fcda8a9-451b-509e-788d-31ba8a1d5d9e.png)

REST APIを作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/84c0b2b7-0db4-7063-8c84-494c4ffee386.png)

「リソース」を作成でURIの末端部分を指定できます。
[話によると](https://qiita.com/naka345/items/98582cfb45ab09c3df9f)、`/` 自体にメソッド(機能)を持たせるとDDos攻撃を受けやすくなるらしいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/f7761a40-29ee-59b2-de79-50ca7c31f29a.png)

Webサイトからのアクセスを想定する場合、 **CORS** にチェックつけるのをオススメします、というかチェックしないとCORS制限にひっかかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/19ddce9c-18c7-3195-0d34-ddfe252a034e.png)

「メソッドを作成します」
**OPTIONはそのまま** にしましょう。それはCORSに使われるのでいじらない方がいいです。「プリフライトリクエスト」ってやつです[[参考]](https://future-architect.github.io/articles/20200717/)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/d1e2c262-c105-1f7e-ebaf-b58e824f4d74.png)

* **メソッドタイプ** を欲しいものに選択
* **統合タイプ** はもちろん **Lambda**
* **Lambdaプロキシ統合** にチェック（するとGo書きで使ったパッケージが使えます）
* 作ったLambda関数を指定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/b582c739-9775-1d58-bdff-82bc78b1d02d.png)

メソッド作成はこれで完了

#### 認証機能をつける
Cognitoの認証機能を付与します。

「オーソライザー」へ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/077cec13-6ce5-3e77-5a45-5c0ce8dcb43d.png)


「作成」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/a58ee77c-6683-1f9d-6924-d47fae54dadb.png)

* Cognitoを指定
* ユーザープールは作成したものを指定
* **トークンのソース** はよきように。`Authorization`が推奨。 (リクエストのヘッダーのやつですよ！！）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/06425a32-7078-8b6a-4aad-2794942d9175.png)

さあ「リソース」へ戻って「メソッドリクエストの設定」から「編集」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/5845aa73-1d9d-f926-71bd-ec3ece92670b.png)

さっき作ったものを指定。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/df2c2f01-7de9-3442-8cc1-3e8cb9f6e331.png)

これで、リクエストヘッダーにCognitoから与えられたトークンを入れてないと弾かれる、という **ユーザー認証** ができます。

#### バリデーション
「モデル」へ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/a6fef7b4-cfe2-4318-fe7c-6bc1dd429573.png)

「モデル」を作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/dc37f8ff-333d-f551-f8a8-23e0abfc450d.png)

モデルの記法はこんな感じ。[AWS 開発者ガイドより引用。](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-request-validation-set-up.html)
```json
{
  "type" : "object",
  "required" : [ "name", "price", "type" ],
  "properties" : {
    "id" : {
      "type" : "integer"
    },
    "type" : {
      "type" : "string",
      "enum" : [ "dog", "cat", "fish" ]
    },
    "name" : {
      "type" : "string"
    },
    "price" : {
      "type" : "number",
      "minimum" : 25.0,
      "maximum" : 500.0
    }
  }
}
```

前項と同じくメソッドリクエストを編集から登録。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/8498b21a-371a-9634-e19e-1604ddb6265e.png)

#### デプロイ
デプロイで初めて使えるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/c57bf19c-a648-94e2-6418-c45eca631397.png)

ステージは、`(ドメイン)/(ステージ名)/(リソース名)`というような感じでURIに現れます。
慣習的には、テスト段階では`beta`、本番では`v1`と指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/8151f1e6-feba-b0bd-ea23-5af5a52775b7.png)


デプロイ完了！！

URIを確認するには

「ステージ」へ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/846a53b1-3902-f6a1-b6f0-11f804f7a8cf.png)

この部分です！！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/36754b53-482c-818a-54df-9156f21a4749.png)

## 末筆
以上です。
あまり丁寧に記述しなかった点は多々ありますが（というか全部？）、情報不足などあれば積極的にコメントに書いていただければ、随時追記します。

:::note
この記事に「いいね」をくだされば、泣いて喜びます！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！
:::