CDK×Lambda×golang×Dynamoでアプリを作ってみる DynamoDB編（第二回）

さて、今回は以下の記事に続けて第二弾DynamoDBのスタックをCDKで追加していく。

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


まずは以下をimportに追加して`go mod tidy`を実行
```
"github.com/aws/aws-cdk-go/awscdk/v2/awsdynamodb"
```

さて、まずはコードの全体像からです。
今回は以前のコードの追加していますが、まあほとんど似たような箇所が多いので前回よりも理解は楽になりましたが、Dynamoの設定値があるのでそちらの方も踏まえて見ていこうと思います。追加などわかりやすいように改行や並び順が変だったりします。

ちなみに私のDynamoDBの知識は2年前にSAPを取得した時の枯れた知識と先日再度キャッチアップした時の以下の記事レベルです。ほとんど入門者です。

https://qiita.com/YokoYokoko/items/8f0a19b0da69dc28f512

```
package main

import (
	"log"
	"os"

	"github.com/aws/aws-cdk-go/awscdk/v2"
	"github.com/aws/aws-cdk-go/awscdk/v2/awsiam"
	"github.com/aws/aws-cdk-go/awscdk/v2/awslambda"
	"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 LambdaStackProps struct {
	awscdk.StackProps
}

type DynamoDBStackProps struct{
	awscdk.StackProps
}

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

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

	// Lambda関数の定義
	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), // Lambdaコードのパスを指定
	})

	return stack
}

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

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

	// DynamoDBテーブルの作成
	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,
	})

	tablerole := awsiam.NewRole(stack, jsii.String("dynamodb-role"), &awsiam.RoleProps{
		AssumedBy: awsiam.NewServicePrincipal(jsii.String("appsync.amazonaws.com"), nil),
	})
	tablerole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AmazonDynamoDBFullAccess")))

	return stack
}

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

	// で、その入れ物にLambdaを配置する。この時、idは"LambdaStack"
	NewLambdaStack(app, "LambdaStack", &LambdaStackProps{
		awscdk.StackProps{
			Env: env(),
		},
	})

	NewDynamoDBStack(app, "DynamoDBStack", &DynamoDBStackProps{
		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),
	}
}
```

上記で今回追加した部分を抜粋すると以下の関数です。Stackやmainでの呼び出しについては前回のlambdaと同じなので割愛します。


今回は以下関数を詳しく見ていきたいと思いますが、こちらも前回と重複している箇所があるので割愛する部分もありますのでご承知おきください。

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

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

	// DynamoDBテーブルの作成
	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,
	})

	tablerole := awsiam.NewRole(stack, jsii.String("dynamodb-role"), &awsiam.RoleProps{
		AssumedBy: awsiam.NewServicePrincipal(jsii.String("appsync.amazonaws.com"), nil),
	})
	tablerole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AmazonDynamoDBFullAccess")))

	return stack
}
```

まず、スタックの作成ですが、こちらは以前解説していますのでそちらをご参照ください。
どのスタックを配置する場所と一意の名前、そして設定値を渡しています。今回も以前と同様に設定値にはAccountとRegionを渡しています。
```
*DynamoDBStackProps) awscdk.Stack {
	var sprops awscdk.StackProps
	if props != nil {
		sprops = props.StackProps
	}

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

で、ここからが以前と異なるDynamoDBの設定です。
```
awsdynamodb.NewTable(stack, jsii.String("MyDynamoDB"), &awsdynamodb.TableProps{
	TableName: jsii.String("MyDynamoDB"),
	PartitionKey: &awsdynamodb.Attribute{
		Name: jsii.String("id"),
		Type: awsdynamodb.AttributeType_STRING,
	},
})
 ```

 以前の解説で、lambdaでは`&awslambda.FunctionProps`で設定値を渡していましたが、DynamoDBでは`&awsdynamodb.TableProps`を使用します。この構造体には以下の設定値があります。

色々あるけど必須なのは`PartitionKey`だけみたい。他の設定値もDynamoDBの基本を理解していれば大体わかると思うので、必要に応じて適用してみるとよさそうです。

まあ、今回はCDKの確認なので`PartionKey`と`BillingMode`をオンデマンドにしたいので、この2つだけ設定しようと思います。 

| フィールド名 | 型 | 説明 |必須|デフォルト|
|:-:|:-:|:-:|:-:|:-:|
| PartitionKey | *Attribute | パーティションキーの属性定義。このプロパティは必須です。 | ⭕️ | |
| SortKey | *Attribute | ソートキーの属性定義。 | | ソートキーなし |
| BillingMode | BillingMode | 読み取りおよび書き込みスループットの課金方法および管理方法を指定します。 | | PROVISIONED |
| ContributorInsightsEnabled | *bool | CloudWatch のコントリビュータインサイトを有効にするかどうか。 | | false |
| DeletionProtection | *bool | テーブルの削除保護を有効にするかどうか。 | | false |
| Encryption | TableEncryption | サーバーサイド暗号化を有効にするかどうか。 | | DynamoDB によって管理される暗号化キー |
| EncryptionKey | awskms.IKey | テーブルの暗号化に使用する外部の KMS キー。Encryption が CUSTOMER_MANAGED の場合にのみ設定可能です。 | | |
| ImportSource | *ImportSourceSpecification | S3 バケットからテーブルへのデータのインポートに関するプロパティ。 | | インポートなし |
| PointInTimeRecovery | *bool | ポイントインタイムリカバリを有効にするかどうか。 | | false |
| ReadCapacity | *float64 | テーブルの読み取り容量。BillingMode が PROVISIONED の場合にのみ設定可能です。 | | 5 |
| RemovalPolicy | awscdk.RemovalPolicy | DynamoDB テーブルに適用する削除ポリシー。 | | RETAIN |
| ReplicationRegions | *[]*string | レプリカテーブルを作成するリージョン。 | | レプリカなし |
| ReplicationTimeout | awscdk.Duration | 単一リージョンでのテーブルレプリケーション操作のタイムアウト。 | | 30分 |
| Stream | StreamViewType | テーブルのストリームに書き込まれる情報の種類を決定します。 | | ストリームなし |
| TableClass | TableClass | テーブルクラスを指定します。 | | STANDARD |
| TimeToLiveAttribute | *string | TTL 属性の名前。 | | TTL 無効 |
| WaitForReplicationToFinish | *bool | CloudFormation スタックがレプリケーションの完了を待つかどうか。 | | true |

ちなみに話が脱線しますがGSIやLSIの設定はどうやるんだ？と疑問に思っていたら、以下のように作成したtableに`AddLocalSecondaryIndex`で別途設定するっぽいです。いっそのことTablePropsに入れてくれたらいいのに。。。
```
    // ローカルセカンダリインデックスの追加
    table.AddLocalSecondaryIndex(&awsdynamodb.LocalSecondaryIndexProps{
        IndexName: jsii.String("LSI-Index"),
        SortKey: &awsdynamodb.Attribute{
            Name: jsii.String("LSI-SortKey"),
            Type: awsdynamodb.AttributeType_STRING,
        },
        ProjectionType: awsdynamodb.ProjectionType_ALL,
    })

    // グローバルセカンダリインデックスの追加
    table.AddGlobalSecondaryIndex(&awsdynamodb.GlobalSecondaryIndexProps{
        IndexName: jsii.String("GSI-Index"),
        PartitionKey: &awsdynamodb.Attribute{
            Name: jsii.String("GSI-PartitionKey"),
            Type: awsdynamodb.AttributeType_STRING,
        },
        SortKey: &awsdynamodb.Attribute{
            Name: jsii.String("GSI-SortKey"),
            Type: awsdynamodb.AttributeType_STRING,
        },
        ProjectionType: awsdynamodb.ProjectionType_ALL,
    })
```

さて、話を本筋に戻しましょう。
まず作成したstackに`MyDynamoDB`という名前をつけます。
そして、上記説明の通り、設定したい値を`awsdynamodb.TableProps`で付与していきます。
```
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,
})
```

まずPartitionKeyをどれにするか決めます。
`&awsdynamodb.Attribute`では以下のように`Name`と`Type`を設定できることがわかります。
Nameはカラムの名前、Typeは型ですね。（正確にはAttributeの名前かな。）
ちなみに、PKとSKの型には文字列、数値、またはバイナリのみ使用可能なので、このうちのいずれかを指定するように注意です。

今回は`文字列`の`id`属性をPKに指定します。
```Attribute.go
type Attribute struct {
	// The name of an attribute.
	Name *string `field:"required" json:"name" yaml:"name"`
	// The data type of an attribute.
	Type AttributeType `field:"required" json:"type" yaml:"type"`
}
```

次に`BillingMode`を設定します。デフォルトではプロビジョンドになるので、大量に使用する場合は割高になりますが、今回はほとんど使用しないためオンデマンドである`awsdynamodb.BillingMode_PAY_PER_REQUEST`と設定します。

とりあえず、Dynamoのテーブル設定自体はこれで完了です。
では、以下コマンドを実行しましょう。今回はスタックを2つ作成しているので`cdk deploy`だとエラーが出てしまいます。
```
cdk synth
cdk deploy --all
```

![スクリーンショット 2024-05-25 23.34.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/0afb26eb-9338-27a2-6663-e70f4bbc2ed9.png)

# ポリシーやロールの追加
今回はテーブルを試しに作成しただけなので細かい設定はしていませんが、本来であれば削除保護やロールなどを付与するかと思います。

その時は`awscdk.NewPolicy`や`awscdk.NewRole`として必要な情報を渡せば設定できるみたいです。

## ポリシーの追加
```
stackPolicy := awscdk.NewPolicy(stack, jsii.String("MyStackPolicy"), &awscdk.PolicyProps{
	Statements: []awscdk.PolicyStatement{
		awscdk.NewPolicyStatement(&awscdk.PolicyStatementProps{
			Effect:    awscdk.Effect_DENY,
			Actions:   []string{"dynamodb:DeleteTable"},
			Resources: []string{table.TableArn()},
		}),
	},
})

stack.AddPolicy(stackPolicy)
```
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.PolicyProps.html

`PolicyProps`では以下のような項目を設定できる模様。
| フィールド名 | 型 | 説明 |必須|デフォルト|
|:-:|:-:|:-:|:-:|:-:|
| Document | PolicyDocument | このポリシーに使用する初期のPolicyDocumentを指定します。 | |空のポリシー|
| force | boolean | AWS::IAM::Policyの作成を強制します。 | |false |
| PolicyName | *string | ポリシーの名前を指定します。 | |ポリシーリソースの論理ID（スタック内で一意）|
| Statements | *[]awsiam.PolicyStatement | ポリシーに含まれるポリシーステートメントのリストを指定します。 | |なし|
| Users | *[]awsiam.IUser | ポリシーを適用するユーザーのリストを指定します。 | |なし|
| Roles | *[]awsiam.IRole | ポリシーを適用するロールのリストを指定します。 | |なし|
| Groups | *[]awsiam.IGroup | ポリシーを適用するグループのリストを指定します。 | |なし|


## ロールの追加
ロールの追加の場合は`"github.com/aws/aws-cdk-go/awscdk/v2/awsiam"`でパッケージが必要となるので注意。
```
tablerole := awsiam.NewRole(stack, jsii.String("dynamodb-role"), &awsiam.RoleProps{
	AssumedBy: awsiam.NewServicePrincipal(jsii.String("appsync.amazonaws.com"), nil),
})
tablerole.AddManagedPolicy(awsiam.ManagedPolicy_FromAwsManagedPolicyName(jsii.String("AmazonDynamoDBFullAccess")))
```

`RoleProps`では以下のような項目を設定できる模様。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.RoleProps.html

| フィールド名 | 型 | 説明 | 必須 | デフォルト |
|:-:|:-:|:-:|:-:|:-:|
| assumedBy | IPrincipal | このロールを引き受けることができるIAMプリンシパル（例：`new ServicePrincipal('sns.amazonaws.com')`）。 | ⭕️ |  |
| description | string | ロールの説明。 | |  |
| externalIds | string[] | このロールを引き受ける際に提供する必要があるIDのリスト。 | |  |
| inlinePolicies | { [string]: PolicyDocument } | このロールにインライン化する名前付きポリシーのリスト。 | |  |
| managedPolicies | IManagedPolicy[] | このロールに関連付けられた管理ポリシーのリスト。 | |  |
| maxSessionDuration | Duration | 指定されたロールの最大セッション期間。 | |  |
| path | string | このロールに関連付けられたパス。 | |  |
| permissionsBoundary | IManagedPolicy | IAMエンティティ（ユーザーまたはロール）のための許可境界をサポートします。 | |  |
| roleName | string | IAMロールの名前。 | |  |

# まとめ
今回は前回作成したlambdaにdynamoを追加しました。
大枠を理解していればあとは設定項目の書き方を調べるだけという感じですね。

では、ここまででDynamoとLambdaの基本的な構成が完成したので次回は実際にlambda関数をgolangで記述してDynamoに書き込んだり読み込んだりしていきたいと思います！

その後は、色々と他の方のcdkの実装やドキュメントを見てみて、設定項目などで必要そうなものを追加していく予定です〜。