CDK×Lambda×golang×Dynamoでアプリを作ってみる APIGATEWAY＋スタック統合編（第三回）

さて、第一回と第二回でLambdaとDynamoDBのスタックをそれぞれ作成しデプロイまでやりました。

https://qiita.com/YokoYokoko/items/c1fecc3278197ca43050

https://qiita.com/YokoYokoko/items/da70a46737cd9db1de7f

ただ、このままではアクセスポイントがないため若干不便なので、APIgatewayを準備したいと思います。
また、現状ではスタックを別々に分けていますが、アプリケーション自体が大きくないので、扱いやすくなるためいっそのこと1つのスタックに統合しようと思います。

記述内容自体はほとんど変わりませんが、関数名をNewAppStackと変更しその中にNewDynamoDBStack関数とNewLambdaStack関数をまとめました。これで重複コードも削除できるのですっきりしました。
```
package main

import (
	"log"
	"os"
	"github.com/aws/aws-cdk-go/awscdk/v2"
	"github.com/aws/aws-cdk-go/awscdk/v2/awslambda"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsapigatewayv2"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsapigatewayv2integrations"
	"github.com/aws/constructs-go/constructs/v10"
	"github.com/aws/jsii-runtime-go"
	"github.com/joho/godotenv"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsdynamodb"
)

type AppStackProps struct {
	awscdk.StackProps
}

func NewAppStack(scope constructs.Construct, id string, props *AppStackProps) awscdk.Stack {
	var sprops awscdk.StackProps
	if props != nil {
		sprops = props.StackProps
	}

	stack := awscdk.NewStack(scope, &id, &sprops)

	// DynamoDBテーブルの作成
	table := awsdynamodb.NewTable(stack, jsii.String("MyDynamoDB"), &awsdynamodb.TableProps{
		TableName: jsii.String("MyDynamoDB"),
		PartitionKey: &awsdynamodb.Attribute{
			Name: jsii.String("id"),
			Type: awsdynamodb.AttributeType_STRING,
		},
		BillingMode: awsdynamodb.BillingMode_PAY_PER_REQUEST,
	})

	// Lambda関数の定義
	lambdaFunction := awslambda.NewFunction(stack, jsii.String("MyFunction"), &awslambda.FunctionProps{
		Runtime: awslambda.Runtime_PROVIDED_AL2(),
		Handler: jsii.String("main"),
		Code:    awslambda.Code_FromAsset(jsii.String("lambda/dynamoDBHandler"), nil),
		Environment: &map[string]*string{
			"DYNAMODB_TABLE_NAME": table.TableName(),
		},
	})

	// テーブルへの権限付与
	table.GrantReadWriteData(lambdaFunction)

	// API Gatewayの定義
	api := awsapigatewayv2.NewHttpApi(stack, jsii.String("MyApi"), &awsapigatewayv2.HttpApiProps{
		ApiName: jsii.String("MyApi"),
	})

	// Lambda関数の統合
	lambdaIntegration := awsapigatewayv2integrations.NewHttpLambdaIntegration(jsii.String("MyIntegration"), lambdaFunction, nil)
	
	// API Gatewayの定義
	api.AddRoutes(&awsapigatewayv2.AddRoutesOptions{
		Path: jsii.String("/test"),
		Methods: &[]awsapigatewayv2.HttpMethod{
			awsapigatewayv2.HttpMethod_GET,
			awsapigatewayv2.HttpMethod_POST,
			awsapigatewayv2.HttpMethod_PUT,
			awsapigatewayv2.HttpMethod_DELETE,
		},
		Integration: lambdaIntegration,
	})

	return stack
}


func main() {
	// .envファイルを読み込む
	err := godotenv.Load()
	if err != nil {
		log.Fatalf("Error loading .env file")
	}
	// スタックの入れ物を作るイメージ
	app := awscdk.NewApp(nil)

	NewAppStack(app, "AppStack", &AppStackProps{
		awscdk.StackProps{
			Env: env(),
		},
	})

	app.Synth(nil)
}

func env() *awscdk.Environment {
	awsAccountId := os.Getenv("AWS_ACCOUNT_ID")
	awsRegion := os.Getenv("AWS_REGION")

	return &awscdk.Environment{
		Account: jsii.String(awsAccountId),
		Region:  jsii.String(awsRegion),
	}
}
 ```

今回、追加した内容としては以下の2点です。
- lambda関数に作成するテーブルへの読み書き操作権限を与えること
- apigatewayを作成しパスを設定し、lambdaと統合すること

## テーブルへの権限付与
テーブルへの権限付与については以下の箇所です。`GrantWriteData`や`GrantReadData`など他にも様々な権限を与えることができます。

今回は作成したlambda関数にテーブルの全ての読み書き操作を許可します。
```
table.GrantReadWriteData(lambdaFunction)
```
| メソッド名 | 説明 |
|:-:|:-:|
| GrantFullAccess | DynamoDB テーブルに対するすべての操作を許可します。 | ⭕️ |
| GrantReadData | BatchGetItem、GetRecords、GetShardIterator、Query、GetItem、Scan、DescribeTable など、すべての読み取り操作を許可します。 | ⭕️ |
| GrantReadWriteData | BatchGetItem、GetRecords、GetShardIterator、Query、GetItem、Scan、BatchWriteItem、PutItem、UpdateItem、DeleteItem、DescribeTable など、すべての読み書き操作を許可します。 | ⭕️ |
| GrantWriteData | BatchWriteItem、PutItem、UpdateItem、DeleteItem、DescribeTable など、すべての書き込み操作を許可します。 | ⭕️ |
| GrantStreamRead | DescribeStream、GetRecords、GetShardIterator、ListStreams など、テーブルのストリームに対するすべての読み取り操作を許可します。 | ⭕️ |
| GrantStream | テーブルのストリームに関連する指定されたアクションを許可します。 | ⭕️ |
| GrantTableListStreams | 現在の DynamoDB テーブルに接続されているストリームの一覧を IAM プリンシパルに許可します。 | ⭕️ |
| Grant | 特定のアクションのセットを IAM プリンシパルに対して許可します。 | ⭕️ |
| GrantCrud | テーブルに対する CRUD（作成、読み取り、更新、削除）操作を許可します。 | ⭕️ |
| GrantManagement | テーブルの管理操作（例: DescribeTable や ListTables）を許可します。 | ⭕️ |

## APIgateway
### 続いてapigatewayを追加します。
以下をimportに追加して、`go mod tidy`を実行しましょう。
```	
"github.com/aws/aws-cdk-go/awscdk/v2/awsapigateway"
```

次にapiを定義して、そのapiとlambda関数を統合しましょう。
```
api := awsapigatewayv2.NewHttpApi(stack, jsii.String("MyApi"), &awsapigatewayv2.HttpApiProps{})
```
この関数は、新しいHttpApiオブジェクト（API Gateway HTTP API）を作成するためのコンストラクタです。
まず、awaspigatewayv2.NewHttpApiの定義は`scope`と`id`と`*HttpApiProps`構造体を渡します。
scopeとidはこれまでに出てきたものと同じ概念なので割愛しますが、第三引数にはlambdaやdynamoと同様に設定値を渡すことができます。今回は試しにApiNameを渡してみます。

| フィールド名 | 型 | 説明 | 必須 | 具体的な値 |
|:-:|:-:|:-:|:-:|:-:|
| ApiName | *string | APIの名前を指定します。指定しない場合は自動生成されます。 | | `"MyCustomApi"` |
| CorsPreflight | *CorsPreflightOptions | CORS (Cross-Origin Resource Sharing) 設定を指定します。 | | `&awsapigatewayv2.CorsPreflightOptions{ AllowOrigins: jsii.Strings("*"), AllowMethods: jsii.Strings("GET", "POST") }` |
| CreateDefaultStage | *bool | デフォルトのステージを作成するかどうかを指定します（デフォルトはtrue）。 | | `jsii.Bool(true)` |
| DefaultDomainMapping | *DomainMappingOptions | デフォルトのドメインマッピングを設定します。 | | `&awsapigatewayv2.DomainMappingOptions{ DomainName: domainName }` |
| DefaultIntegration | IHttpRouteIntegration | デフォルトの統合を設定します。 | | `lambdaIntegration` |
| Description | *string | APIの説明を設定します。 | | `"This is my custom API"` |
| DisableExecuteApiEndpoint | *bool | `execute-api`エンドポイントを無効にするかどうかを指定します。 | | `jsii.Bool(false)` |
| FailOnWarnings | *bool | 警告がある場合にデプロイを失敗させるかどうかを指定します。 | | `jsii.Bool(true)` |

これらの設定項目を使って、API Gateway HTTP APIの動作を細かく制御できます。具体的な値の例も含めているので、必要に応じてこれらを参考に設定してください。

### Lambda関数の統合
次にAPI Gatewayのルートが呼び出されたときに、指定されたLambda関数が実行されるように設定します。
```
lambdaIntegration := awsapigatewayv2integrations.NewHttpLambdaIntegration(jsii.String("MyIntegration"), lambdaFunction, &awsapigatewayv2integrations.HttpLambdaIntegrationProps{})
```

`awsapigatewayv2integrations.NewHttpLambdaIntegration`の引数には以下の関数の定義から分かる通り、一意の名前と`awslambda.NewFunction`で作成したlambda関数、そして最後に`HttpLambdaIntegrationProps`で設定値を渡します。
```HttpLambdaIntegration.go
func NewHttpLambdaIntegration(id *string, handler awslambda.IFunction, props *HttpLambdaIntegrationProps) HttpLambdaIntegration {
```

パッケージは以下の`awsapigatewayv2integrations`をimportして使用します。こちらも`go mod tidy`しましょう。
```
"github.com/aws/aws-cdk-go/awscdk/v2/awsapigatewayv2integrations"
```

`HttpLambdaIntegrationProps`は`HttpLambdaIntegrationProps.go`で定義されていますが、`ParameterMapping`と`PayloadFormatVersion`だけのようです。

| フィールド名 | 型 | 説明 | 必須 | 具体的な値 |
|:-:|:-:|:-:|:-:|:-:|
| ParameterMapping | ParameterMapping | API GatewayのリクエストパラメータをLambda関数に渡す際のマッピングを指定します。 |  | `awsapigatewayv2.ParameterMapping{QueryStrings: map[string]string{"originalParamName": "newParamName"}}` |
| PayloadFormatVersion | PayloadFormatVersion | API GatewayがLambda関数に渡すペイロードのフォーマットバージョンを指定します。 |  | `awsapigatewayv2.PayloadFormatVersion_VERSION_2_0` |

今回は特に必要ないですが、以下のようにマッピングの設定などをしておくと、例えば外部パラメータ名userIdが内部パラメータ名 user_idに変換されるみたいです。ヘッダーも同様。
```
lambdaIntegration := awsapigatewayv2integrations.NewHttpLambdaIntegration(jsii.String("MyIntegration"), lambdaFunction, &awsapigatewayv2integrations.HttpLambdaIntegrationProps{
    ParameterMapping: awsapigatewayv2.ParameterMapping{
        QueryStrings: map[string]string{
            "userId": jsii.String("user_id"),
        },
        Headers: map[string]string{
            "externalHeader": "internalHeader",
        },
    },
    PayloadFormatVersion: awsapigatewayv2.PayloadFormatVersion_VERSION_2_0,
})
```


まあ今回は設定しないのでnilとしておきます。とりあえずここまででapiGatewayとそれにlambda関数を統合するための定義ができました。

残すところは`api.AddRoutes(&awsapigatewayv2.AddRoutesOptions`の箇所ですね。
```
api.AddRoutes(&awsapigatewayv2.AddRoutesOptions{
	Path: jsii.String("/test"),
	Methods: &[]awsapigatewayv2.HttpMethod{
		awsapigatewayv2.HttpMethod_GET,
		awsapigatewayv2.HttpMethod_POST,
		awsapigatewayv2.HttpMethod_PUT,
		awsapigatewayv2.HttpMethod_DELETE,
	},
	Integration: lambdaIntegration,
})
```
ここでは、先ほど`awsapigatewayv2.NewHttpApi`で定義したapiに設定を追加していきます。

今回は`AddRoutes`を使用します。この関数は`AddRoutesOptions`を引数にとります。
```HttpApi.go
AddRoutes(options *AddRoutesOptions) *[]HttpRoute
```

さらに`AddRoutesOptions`を見てみると`AddRoutesOptions.go`に以下のように定義されています。
| フィールド名     | 型    | 説明 | 必須  | デフォルト  |
|:-:                      |:-:                         |:-:                                                                                                        |:-:  |:-:                                                                       |
| Integration             | HttpRouteIntegration       | このルートで構成される統合を指定します。                                                                  | ⭕️   |                                                                            |
| Path                    | *string                    | これらのルートが構成されるパスを指定します。                                                              | ⭕️   |                                                                            |
| AuthorizationScopes     | *[]*string                 | 認証に含めるOIDCスコープのリスト。これらのスコープは、ゲートウェイのデフォルトの認証スコープを上書きします。   |      | APIで構成されている場合はデフォルトの認証スコープを使用、そうでない場合はなし |
| Authorizer              | IHttpRouteAuthorizer       | これらのルートに関連付けられるオーソライザー。                                                            |      | デフォルトのオーソライザーを使用                                            |
| Methods                 | *[]HttpMethod              | 構成するHTTPメソッドのリスト。                                                                             |      | HttpMethod.ANY                                                             |

今回は必須である`Path`と`Methods`に加えて、先ほど定義したlambdaIntegrationを`Integration`に設定します。

具体的には、/testパスへのGET、POST、PUT、DELETEリクエストを指定されたLambda関数にルーティングします。この設定により、指定したパスにリクエストが来た場合、対応するLambda関数が実行されます。

さて、これで完成したので`cdk deploy`を実行してみましょう。
すると無事、`✅  AppStack✨  Deployment time: 62.46s`と表示されデプロイに成功しました。

AWSのUIで確認してみるときちんとLambdaにAPI Gatewayが統合されてトリガーとして設定されていることが分かります。
![スクリーンショット 2024-05-26 23.53.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/10f55c88-f048-f8d3-1e16-2ada4117a939.png)

DynamoDBについても以前と同様に正常に作成されています。
![スクリーンショット 2024-05-26 23.57.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/b26e56f4-2542-198e-1a69-774188cf38b2.png)

これで`/test`パスへAPIを投げるとトリガーになり、lambda関数に飛ばすことができるようになりました🙌
したがって次はこのDynamoDBに読み書き権限を与えたlambda関数の中でmethodによる条件分岐でテスト処理を書いて、CRUD処理をできるように実装してみたいと思います！