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

このシリーズでは、CDK*Lambdagolang*DynamoDBの構成で学習をしてみたい自分がその記録も残しつつ、私と同じような、とりあえずこのような構成の雰囲気を掴んでみたいという方向けに書いています。

# CDKとは
コードでクラウド インフラストラクチャを定義し、AWS CloudFormation を通じてプロビジョニングするためのオープンソース ソフトウェア開発フレームワークです。(公式)

まあ、簡単にいうとUI上でEC2とかS3とかをぽちぽち設定するんじゃなくて、コードで起動できるよってこと。管理もしやすいから便利だね。

# 何はともあれ触ってみよう。
とりあえずディレクトリを作成し、モジュールを初期化。
```
mkdir first_prac_go
cd first_prac_go
cdk init --language go
```
すると`go.mod`や他の必要ファイルが色々と作成される。
`cdk init --language go`では以下ファイルが生成されます。
```
.
├── README.md
├── cdk.json
├── directory_name_test.go
├── directory_name.go
├── go.mod
└── go.sum
```
で、ちょっと面倒ですが、cdkディレクトリにcdk.jsonを移動し、2行目を以下のように変更してください。
```
{
  "app": "go mod download && go run main.go",
```


`go.mod`の中身はmodule名とgoのversionが記載されているだけ。
```
module first_prac_go

go 1.21.6
```

::: note
もしcdkコマンドがnot foundで使えない方は以下コマンドでインストールしましょう。
```
npm install -g aws-cdk
```
その後`which cdk`でインストール先を確認します。
```
~/Desktop/.nodebrew/current/bin/cdk
```
次に、.zshrc ファイルに $PATH を永続的に追加します。
```
echo 'export PATH=$PATH:/Users/fukudanaoto/Desktop/.nodebrew/current/bin' >> ~/.zshrc
source ~/.zshrc
```
:::

次にCDKライブラリをインストール
```
go get github.com/aws/aws-cdk-go/awscdk/v2
go get github.com/aws/aws-cdk-go/awscdk/v2/awslambda
go get github.com/aws/constructs-go/constructs/v10
go get github.com/aws/jsii-runtime-go
```

`go mod tidy`は忘れずに。

無事インストールされたらCDK用に`main.go`を直下に作成してみる。
ちなみに今回は以下のようなディレクトリ構成で作成してみようと思う。
```
/first_prac_go
  /lambda
    /dynamoDBHandler
      main.go        # Lambda関数のソースコード
  /cdk
    main.go          # CDKデプロイメントコード　今はここを作成する。
  go.mod
  go.sum
  ```
```
touch main.go
```

### main.go　全体像
このmain.goに以下のように記述してみる。
これはlamdaで記述した関数をデプロイするためのコードとなっている。

```main.go
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/constructs-go/constructs/v10"
	"github.com/aws/jsii-runtime-go"
	"github.com/joho/godotenv"
)

type LambdaStackProps 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 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(),
		},
	})

	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),
	}
}
```

上記を分割して説明を加えていこうと思う。

### import
まず、ここだがこれは説明する程でもないが、packageの名前（通常はディレクトリ名）と先ほどインストールした外部ライブラリを読み込んでいる。反映されずエラ〜が出ている場合`go mod tidy`をしてみると解決するかも。
```
package main

import (
	"github.com/aws/aws-cdk-go/awscdk/v2"
	"github.com/aws/aws-cdk-go/awscdk/v2/awslambda"
	"github.com/aws/constructs-go/constructs/v10"
	"github.com/aws/jsii-runtime-go"
)
```
* `github.com/aws/aws-cdk-go/awscdk/v2`
AWS CDK（Cloud Development Kit）のGo言語版で、AWSリソースを定義してデプロイするためのライブラリです。CDKを使用することで、Go言語でAWSリソースの定義やデプロイを行うことができます。

* `github.com/aws/aws-cdk-go/awscdk/v2/awslambda`
AWS CDKのGo言語版で、AWS Lambda関数を定義してデプロイするためのライブラリです。Lambda関数はサーバーレスアプリケーションを構築する際に使用されます。

* `github.com/aws/constructs-go/constructs/v10`
AWS CDKのコンストラクツを定義するための基本ライブラリです。CDKではコンストラクツを使用してAWSリソースを構築しますが、このライブラリはその基本となるコンストラクツを提供します。

* `github.com/aws/jsii-runtime-go`
JSII（JavaScript Interoperability Interface）のランタイムライブラリで、AWS CDKのGo言語版で使用されます。JSIIは異なるプログラミング言語間での相互運用性を実現するための仕組みであり、AWS CDKはJSIIを使用して複数のプログラミング言語から利用することができます。

### Struct
次にStructだが、StackPropsは`StackProps.go`で定義されている。
```
type LambdaStackProps struct {
	awscdk.StackProps
}
```
このファイルを見ると色々と設定項目があることが分かる。
| フィールド | 説明 |デフォルト|
|:-:|---|---|
| AnalyticsReporting | ランタイムバージョン情報を含めるかどうか。デフォルトは`App`内の`analyticsReporting`設定または'aws:cdk:version-reporting'コンテキストキーの値。 ||
| CrossRegionReferences | ネイティブなクロスリージョンスタック参照を許可するかどうか。実験的な機能で、デフォルトはfalse。 |false|
| Description | スタックの説明 |未指定|
| Env | スタックのデプロイ先AWS環境（アカウント/リージョン）。具体的な値を設定するか、環境変数 `CDK_DEFAULT_REGION` / `CDK_DEFAULT_ACCOUNT` の値を使用。 ||
| PermissionsBoundary | ステージ内のすべてのIAMロールとユーザーに適用するアクセス許可境界。 |未指定の場合は適用されない|
| StackName | スタックのデプロイ名|構築パスから派生|
| SuppressTemplateIndentation | CloudFormationテンプレートのインデント抑制を有効にするかどうか |未指定時はデフォルト値が使用される|
| Synthesizer | スタックのデプロイ時に使用するシンセサイザーの合成方法。 |未指定時はデフォルト値が使用される|
| Tags | スタックとリソースに適用されるタグ|空|
| TerminationProtection | スタックの終了保護を有効にするかどうか|false|


### NewLambdaStack関数
次に`NewLambdaStack関数`を見てみよう。
```
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: a.Runtime_PROVIDED_AL2(),
		Handler: jsii.String("main"),
		Code:    awslambda.Code_FromAsset(jsii.String("dynamoDBHandler/main.go")), // Lambdaコードのパス
	})

	return stack
}
```
ここでは、Lamdaのスタックを作成する関数を定義している。
ちなみにスタックとは、一連のリソース（例えば、EC2インスタンス、S3バケット、RDSデータベースなど）をまとめてデプロイするために使用されるもので、スタックを使用することで、リソースの作成、更新、削除などの管理を一括して行うことができるものだ。

まず、NewLambdaStackは引数を`scope` `id` `props`の3つを取り、`awscdk.Stack`型を返す。

#### 引数
##### scope
scopeパラメータは新しいスタックやリソースが作成される親の構成要素を表します。スタックが存在するコンテキストや名前空間を定義します。

まあ分かりづらいけど、スタックを配置するための場所ってことかな。例えば、同じ場所に置くだけならあまり気にする必要はないが、あるスタックAの中にさらにスタックBを置きたいとなった場合、スタックBを作成する時にスタックAをスコープとすることで配置することができる。

例えば、バックエンドとフロントエンドでそれぞれスタックを分けたいが、最終的にはまとめてcdkとしてデプロイしたい場合、親のスタックを作り、それぞれ親のスタックをスコープとして使用したりとかも1つの方法かな。
```
// 親スタックを作成
parentStack := awscdk.NewStack(app, "ParentStack", nil)

// 子スタックを作成し、親スタック内に配置
childStack := NewChildStack(parentStack, "ChildStack")
```

ちょっと話が逸れてしまったが、こんな感じでどこに配置するかを渡している。

##### id
次に、`id`だが、これはそのまま。まあ、プロジェクトやチームの方針、開発者の好みによると思うが、説明的で一意性を保つ名前を付けることが一般的なベストプラクティスとされている。個人開発なら正直なんでもいいと思う。

##### props
引数の最後の`props`だが、これはスタック全体の設定で、さっき上で確認した`StackProps.go`ファイルの中にある`Env`や`StackName`などの値を渡すことができる。

今回は`env`関数でAccountとRegionを設定し、Envに渡している。

#### props初期化とスタック作成
次に以下の部分を見ていく。
```
var sprops awscdk.StackProps
if props != nil {
	sprops = props.StackProps
}

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

まず1行目で`awscdk.Stackprops`型の変数、propsを宣言している。これは当然、ゼロ値となる。まあ設定項目はnilかfalseになるっぽい。

で、引数の`props`がnilではない場合、引数`props`の`StackProps`がpropsに代入される。
引数`props`は`LambdaStackProps構造体`なので`props.StackProps`や`props.Env`でアクセスできる。

あとは、これらの引数を`awscdk.NewStack`の引数に渡せばスタックが完成する。

ざっくりまとめると、スタックを作成するために必要な情報を引数で渡して、それを使ってスタックを作成しているってこと。

で、ここまででは実は何も意味がなくてスタックはあくまでも`Lambda`や`DynamoDB`などのAWSリソースを配置するための場所である。

だから次のステップで以下で作成したスタックに必要なものを追加していく。


#### スタックにlambdaの配置
さて、NewLambdaStack関数の最後の説明となる。
先ほど作成したスタックにlambdaを配置していこう。

まず、`awslambda.Newfunction`関数があるが、その定義は以下の通り
```
func NewFunction(scope constructs.Construct, id *string, props *FunctionProps) Function {
	_init_.Initialize()

	if err := validateNewFunctionParameters(scope, id, props); err != nil {
		panic(err)
	}
	j := jsiiProxy_Function{}

	_jsii_.Create(
		"aws-cdk-lib.aws_lambda.Function",
		[]interface{}{scope, id, props},
		&j,
	)

	return &j
}
```

この引数に先ほど`awscdk.NewStack`で作成した`stack`を渡し、ここに作るよということを伝える。次に第二引数で一意となる`id`を渡す。第三匹数にpropsとして`&awslambda.FunctionProps`の構造体を渡している。

`awslambda.FunctionProps`構造体では、かなりの設定項目があるがマストは以下の通り。
| フィールド | 型 | 説明 |
|:-:|:-:|:-:|
|Code   |Code   |Lambda関数のソースコード。Amazon S3バケット内のファイルを指すか、インラインテキストとして指定。|
|Handler   |*string   |Lambdaが実行するコード内のメソッド名。ファイル名や名前空間などを含めた形式で指定。   |
|Runtime|Runtime   |Lambda関数の実行環境。Dockerイメージから定義する場合は Runtime.FROM_IMAGE を使用。   |

他にも数十個様々あるが、使ううちに知っていくしかない。以下はいくつかの例。
| 1 | 2 | 3 |
|:-:|:-:|:-:|
|MaxEventAge   |	awscdk.Duration   | Lambda関数にリクエストを送信する最大期間。最小60秒、最大6時間。デフォルトは6時間。  |
|OnFailure   |IDestination   |	失敗した呼び出しの送信先。デフォルトはなし。   |
|OnSuccess   |IDestination   |成功した呼び出しの送信先。デフォルトはなし。   |
|RetryAttempts   |*float64   |エラー時の再試行回数。最小0回、最大2回。デフォルトは2回。   |
|AdotInstrumentation   |AdotInstrumentationConfig   | 	AWS Distro for OpenTelemetry (ADOT) の設定。デフォルトはなし。  |
|AllowAllOutbound   |*bool   |	Lambdaがすべてのネットワークトラフィックを送信できるかどうか。デフォルトはtrue。   |
|AllowPublicSubnet   |*bool   |パブリックサブネットにLambdaを配置する場合の制限を承認。デフォルトはfalse。   |
|ApplicationLogLevel   |*string   |関数のアプリケーションログレベル。デフォルトは "INFO"。  |

ちなみに、ご存知の通りRunTimeについては、2023年の12月からgo1.xは廃止されAmazon Linux2ランタイムでのみサポートされている。

またちなみに、`jsii.String`としている理由として、CDKのコンストラクトやプロパティはTypeScriptのオブジェクトを期待しているため、Goの文字列型を直接渡すことはできないからである。そのため、jsii.Stringを使用して変換している。

jsii（JavaScript Interoperability Interface）は、異なるプログラミング言語間での相互運用性を提供するためのツールキットで、AWS Cloud Development Kit（CDK）のようなライブラリがTypeScriptで書かれていても、GoやPython、Javaなどの他の言語からも利用できるようにするために開発されたものらしい。

さて、話を戻すと`awslambda.NewFunction`では、CDKが理解できる形で、`どのスタックに`、`どんな名前で`、`どんな設定で突っ込むか`を決めるということだ。

`awslambda.Code_FromAsset`では、ExcludeやDeployTimeなどを以下のように渡せるが、今回はnilとする。
```
assetOptions := &awss3assets.AssetOptions{
    Exclude: jsii.Strings("node_modules", "*.md"),
}

Code: awslambda.Code_FromAsset(jsii.String("lambda/dynamoDBHandler"), assetOptions)
```

さて、これで完了した。
```
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), 
})
```

最後に、以下を実行して`.env`ファイルを作成しよう。
大丈夫だとは思いますが`.gitignore`への追加は忘れないようにしましょう。
```
go get github.com/joho/godotenv
```

```.env
AWS_ACCOUNT_ID=あなたのACCOUNT_ID
AWS_REGION=あなたのREGION
```

上が完了したらimportとmain関数に以下のように追加する。
```
package main

import (
	"os"
	"log"
    ...
	"github.com/joho/godotenv"
)
func main() {
    // .envファイルを読み込む
    err := godotenv.Load()
    if err != nil {
        log.Fatalf("Error loading .env file")
    }
```

そして、最後にmain関数の`app.Synth(nil)`で、定義されたスタックとその中のリソースをCloudFormationテンプレートに変換します。


# デプロイ
コマンドでデプロイするためにはCDK CLIが必要なので以下を実行する。
```
npm install -g aws-cdk
```

で、lambda関数も`go build`でビルドしておく。
次に`cdk synth`をfirst_prac_goディレクトリで実行する。

成功すると、AWS CDK CLI はコマンドプロンプトに YAML 形式の AWS CloudFormation テンプレートを出力します。JSON 形式のテンプレートもディレクトリにcdk.outが保存されます。

そして、以下コマンドを実行すればデプロイできるが、その前にブートスラップを1回実行する必要があるらしい。
```
cdk bootstrap aws://ACCOUNT-NUMBER/REGION
cdk deploy
```

これで無事デプロイ成功しました！
![スクリーンショット 2024-05-24 23.50.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/9c920e8a-4560-0131-6d9b-595138751df9.png)

CloudFormationの方にも追加されていることがわかります。
![スクリーンショット 2024-05-24 23.51.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3324306/bbdd1934-eb02-ec0e-0994-0e43bb231f0c.png)

最後に、お金がかかると怖いので（かからないはずだけど）、`cdk destroy`で削除しておきます。
実行するとLambdaとCloudFormationのどちらからも削除されていました。

# まとめ
とりあえず今回はCDKってどんな感じが雰囲気を掴むために簡単ではありましたが理解することができました。
色々と設定項目があるので、それを都度調べて知っていくしかないなって印象です。

次の記事では、DynamoDBをスタックに追加していきたいと思います！
で、それができたらlambdaの中の関数をDynamoに追加したり削除したりするHandlerを作成するという流れでやっていきます！