はじめに
以前から気になっていた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で開発しています。
手順
事前にAWSのアカウントを用意して、
aws configure
コマンドで設定をしてください。
もしくは、以下の環境変数をセットしておいてください。
export AWS_ACCESS_KEY_ID={ACCESS_KEY}
export AWS_SECRET_ACCESS_KEY={SECRET_ACCESS_KEY}
まずpulumi-cli
のインストールをします。
brew install pulumi/tap/pulumi
次に作業用ディレクトリを作成して、以下のコマンドを実行してください。
pulumi new aws-go
環境の作成が完了したら、適当にLambda関数(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)
}
続いてgoファイルをビルドしたら準備は完了です。
GOOS=linux GOARCH=amd64 go build -ldflags=$(BUILD_LDFLAGS) -o ./bootstrap ./...
zip ../function.zip ./bootstrap
ここで、紹介にもあったPulumi AIを活用していきます。
チャットで作成したい構成を指示しましょう。
ちなみに私は「API Gateway + LambdaでサーバレスなAPIを作成してください。」と指示しました。
生成が完了したらコードをコピー&ペーストして、以下のコマンドを実行してください。
pulumi up
私は1回目で実行した時にエラーが発生してデプロイできませんでしたが、
チャット上でエラーのコードを送付することで解決策の提示してくれて無事デプロイすることができました。
参考程度に手直し済みのコード
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
が返ってきたら成功です!
リソースの削除を忘れずに。
pulumi destroy
以上です。
全体的なコードはGithubに載せているので、必要であればご参照ください。
さいごに
TerraformやCloudFormationを使ってYAMLで構築することは今までもありましたが、プログラミング言語で記述したことがなかったので良い経験になりました。