PulumiでAPI Gateway + Lambda(Go)をデプロイしてみた

# はじめに
以前から気になっていたIaCツールであるPulumiでAPI Gateway + Lambda(Go)のサーバレスなAPIを作成してみたので記事にしたいと思います。

### Pulumiとは
TerraformやCloudFormationなどと同じくIaCツールとなっていて、YAMLだけでなくGoやPython、TypeScriptなどで記述できることやAWS、Google Cloud、Azureなど様々なクラウドサービスに対応してることも特徴です。
さらにPulumiには**Pulumi AI**というChat GPTのように自然言語でチャットするだけでIaCのコードを生成してくれる機能があります。

### そもそもIaCとは
IaC(Infrastructure as Code)はインフラ(サーバー、ネットワーク、ストレージ）をコードで定義して管理する手法のことです。手動での管理と違ってコードで記述するので、手作業でのミスがなくなることやGitなどを用いることで管理の効率化が可能になっています。

# 開発環境
M1 MacBookで開発しています。

# 手順

:::note info
事前にAWSのアカウントを用意して、
`aws configure`コマンドで設定をしてください。

もしくは、以下の環境変数をセットしておいてください。
```bash
export AWS_ACCESS_KEY_ID={ACCESS_KEY}
export AWS_SECRET_ACCESS_KEY={SECRET_ACCESS_KEY}
```
:::

まず`pulumi-cli`のインストールをします。

```bash
brew install pulumi/tap/pulumi
```

https://www.pulumi.com/docs/install/

次に作業用ディレクトリを作成して、以下のコマンドを実行してください。

```bash
pulumi new aws-go
```

環境の作成が完了したら、適当にLambda関数(Go)のプログラムを用意します。

```go:main.go
package main

import (
	"context"

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

func handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	return events.APIGatewayProxyResponse{
		Body:       "hello",
		StatusCode: 200,
	}, nil
}

func main() {
	lambda.Start(handler)
}
```
https://github.com/tkhs1121/pulumi-apigateway-lambda/blob/master/cmd/main.go


続いてgoファイルをビルドしたら準備は完了です。

```bash
GOOS=linux GOARCH=amd64 go build -ldflags=$(BUILD_LDFLAGS) -o ./bootstrap ./...
zip ../function.zip ./bootstrap
```

---

#### ここで、紹介にもあったPulumi AIを活用していきます。

https://www.pulumi.com/ai

チャットで作成したい構成を指示しましょう。

ちなみに私は「API Gateway + LambdaでサーバレスなAPIを作成してください。」と指示しました。

生成が完了したらコードをコピー&ペーストして、以下のコマンドを実行してください。

```bash
pulumi up
```

私は1回目で実行した時にエラーが発生してデプロイできませんでしたが、
チャット上でエラーのコードを送付することで解決策の提示してくれて無事デプロイすることができました。

#### 参考程度に手直し済みのコード

```go
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/apigateway"
	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam"
	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {

		// Create an IAM role that AWS Lambda will use
		lambdaRole, err := iam.NewRole(ctx, "lambdaRole", &iam.RoleArgs{
			AssumeRolePolicy: pulumi.String(`{
				"Version": "2012-10-17",
				"Statement": [{
					"Action": "sts:AssumeRole",
					"Effect": "Allow",
					"Principal": {
						"Service": "lambda.amazonaws.com"
					}
				}]
			}`),
		})
		if err != nil {
			return err
		}

		// Attach the AWSLambdaBasicExecutionRole policy to the IAM role
		_, err = iam.NewRolePolicyAttachment(ctx, "lambdaPolicyAttachment", &iam.RolePolicyAttachmentArgs{
			Role:      lambdaRole.Name,
			PolicyArn: pulumi.String("arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"),
		})
		if err != nil {
			return err
		}

		// Define the AWS Lambda resource
		lambdaFunc, err := lambda.NewFunction(ctx, "helloLambda", &lambda.FunctionArgs{
			Code:    pulumi.NewFileArchive("function.zip"),
			Role:    lambdaRole.Arn,
			Handler: pulumi.String("bootstrap"),
			Runtime: pulumi.String("provided.al2"), 
		})
		if err != nil {
			return err
		}

		// Create an API Gateway Rest API
		api, err := apigateway.NewRestApi(ctx, "api", &apigateway.RestApiArgs{
			Description: pulumi.String("Example API"),
		})
		if err != nil {
			return err
		}

		_, err = lambda.NewPermission(ctx, "apiGatewayInvoke", &lambda.PermissionArgs{
			Action:    pulumi.String("lambda:InvokeFunction"),
			Function:  lambdaFunc.Name,
			Principal: pulumi.String("apigateway.amazonaws.com"),
		})
		if err != nil {
			return err
		}

		// Create an API Gateway Resource
		resource, err := apigateway.NewResource(ctx, "resource", &apigateway.ResourceArgs{
			RestApi:  api.ID(),
			ParentId: api.RootResourceId,
			PathPart: pulumi.String("hello"),
		})
		if err != nil {
			return err
		}

		// Create an API Gateway Method for the 'GET' HTTP verb
		_, err = apigateway.NewMethod(ctx, "getMethod", &apigateway.MethodArgs{
			RestApi:       api.ID(),
			ResourceId:    resource.ID(),
			HttpMethod:    pulumi.String("GET"),
			Authorization: pulumi.String("NONE"),
		})
		if err != nil {
			return err
		}

		// Create an API Gateway Integration to connect the 'GET' method to the Lambda function
		integration, err := apigateway.NewIntegration(ctx, "getIntegration", &apigateway.IntegrationArgs{
			RestApi:               api.ID(),
			ResourceId:            resource.ID(),
			HttpMethod:            pulumi.String("GET"),
			IntegrationHttpMethod: pulumi.String("POST"),      // Lambda functions are invoked with POST
			Type:                  pulumi.String("AWS_PROXY"), // Use the Lambda proxy integration
			Uri:                   lambdaFunc.InvokeArn,
		})
		if err != nil {
			return err
		}

		// Create a deployment to enable the API Gateway
		deployment, err := apigateway.NewDeployment(ctx, "deployment", &apigateway.DeploymentArgs{
			RestApi: api.ID(),
		}, pulumi.DependsOn([]pulumi.Resource{
			integration,
		}))
		if err != nil {
			return err
		}

		// Create an API Gateway Stage which acts as an environment
		_, err = apigateway.NewStage(ctx, "stage", &apigateway.StageArgs{
			Deployment: deployment.ID(),
			RestApi:    api.ID(),
			StageName:  pulumi.String("prod"), // Name the stage as 'prod'
		})
		if err != nil {
			return err
		}

		// Output the invocation URL of the stage
		ctx.Export("invokeUrl", pulumi.Sprintf("https://%s.execute-api.%s.amazonaws.com/prod/hello", api.ID(), "ap-northeast-1"))

		return nil
	})
}
```

OutputされたURLにアクセスして、`hello`が返ってきたら成功です！

:::note warn
リソースの削除を忘れずに。
```
pulumi destroy
```
:::

以上です。

全体的なコードはGithubに載せているので、必要であればご参照ください。

https://github.com/tkhs1121/pulumi-apigateway-lambda

# さいごに
TerraformやCloudFormationを使ってYAMLで構築することは今までもありましたが、プログラミング言語で記述したことがなかったので良い経験になりました。