Techtouch Developers Blog

テックタッチ株式会社の開発チームによるテックブログです

Go で IaC してみる / AWS CDK・CDK for Terraform・Pulumi

adventCalendar2021-day12

テックタッチアドベントカレンダー 13 日目を担当する taisa です。少しずつ減らしていった体重が 2 ヶ月で 6 kg リバウンドして完全に元に戻りました。

さて今年、AWS CDK v2 の開発者プレビューで Go を使えるようになり、CDK for Terraform でも Go を実験的(experimental)に使えるようになりました。

これらは、まだ開発者プレビューや実験的であるため本番においては投入できませんが、アドベントカレンダーをきっかけに触ってみました。また他にも Go が利用できる IaC(Infrastructure as Code) プラットフォームとして Pulumi があるので合わせて触ってみました。

aws.amazon.com

www.hashicorp.com

www.pulumi.com

各フレームワーク・ライブラリの概要

本記事で取り上げる各フレームワーク・ライブラリの概要について簡単に説明します。特に比較の考察はないので予めご容赦ください。下記は比較記事のリンクとなります。

pilotcoresystems.com

www.libhunt.com

AWS CDK

AWS Cloud Development Kit (AWS CDK) は、AWS が提供するオープンソースの IaC フレームワークです。AWS CloudFormation を通じてデプロイします。現在使える言語は TypeScript、Python、Java、C# に加えて AWS CDK v2 開発者プレビューで Go があります。

docs.aws.amazon.com

CDK for Terraform

CDK for Terraform は、HashiCorp 社が提供するオープンソースの IaC ソフトウェアです。AWS CDK のコンセプトとライブラリを活用しながら、AWS だけでなく様々なプロバイダーに対応しています。

CDK for Terraform を使用すると、HashiCorp構成言語(HCL)を学習せず、Terraform エコシステム全体にアクセスでき、既存のツールチェーンの機能をテストや依存関係の管理などに活用できます。現在、CDK for Terraform 自体がベータ版で、現在使える言語は、TypeScript、Python、Java、C# に加えて実験的として Go があります。

www.terraform.io

Pulumi

Pulumi は、Pulumi 社が提供するオープンソースの IaC ソフトウェアで、様々なプロバイダーに対応しています。現在利用できる言語は、TypeScript、Python、C#、Go で、今後のロードマップに Java、Ruby、PowerShell サポートが予定されています。

Pulumi は今年、クラウドベンダ自身が Pulumi のクラウド対応機能のメンテナンスを担当することで、常に最新のクラウド対応が即時に行われる「Native Providers for Azure and Google Cloud」という新機能をリリースし、後程 AWS 対応のものもリリースしました。

また、プラットフォーム機能を提供していて、チームで利用する場合は有償ですが、今年その価格改定も行われ、以前より利用しやすくなったようです。

www.publickey1.jp

www.pulumi.com

www.pulumi.com

実際に動かしてみる

前置きが長くなりましたが、実際に動かしてみます。本記事では、API Gateway 経由で Lambda Function を実行し、POST した内容がレスポンスとして返ってくる簡単なサンプルアプリを構築します。

# 実行例
% curl -X POST -H "Content-Type: application/json" -d '{"name":"World!"}' https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello
Hello World!

Lambda Function サンプルコード

実行する Lamba Function サンプルコードは下記を利用します。

package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "net/http"

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

type MyEvent struct {
    Name string
}

func main() {
    lambda.Start(HandleRequest)
}

func HandleRequest(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    event := MyEvent{}
    if err := json.Unmarshal([]byte(req.Body), &event); err != nil {
        return events.APIGatewayProxyResponse{
            StatusCode: http.StatusBadRequest,
        }, errors.New("error")
    }

    return events.APIGatewayProxyResponse{
        StatusCode: http.StatusOK,
        Body:       fmt.Sprintf("Hello %s!", event.Name),
    }, nil
}

Lambda Function は下記のように handler 配下へ配置する構成で進めます。

.

├── go.mod
├── go.sum
├── handler 
│   ├── main
│   └── main.go # lambda function
└── main.go # IaC コード

AWS CDK

ではまず AWS CDK を使って構築します。下記コマンドにてインストールしプロジェクトのセットアップを行います。なお、あらかじめ AWSの設定ファイルと認証情報ファイルの設定 が行われている前提で進めます。

# インストール
% npm install -g aws-cdk

# プロジェクトセットアップ
% mkdir sandbox-aws-cdk && cd sandbox-aws-cdk
% cdk init --language=go
% go mod tidy

プロジェクトを作成しcdk init するとコードが自動生成されるので、Lambda Function 設定と API Gateway 設定の記述を追記します。

// sandbox-aws-cdk/sandbox-aws-cdk.go
package main

import (
    "github.com/aws/aws-cdk-go/awscdk/v2"
    "github.com/aws/aws-cdk-go/awscdk/v2/awsapigateway"
    "github.com/aws/aws-cdk-go/awscdk/v2/awslambda"
    "github.com/aws/aws-cdk-go/awscdk/v2/awss3assets"
    "github.com/aws/constructs-go/constructs/v10"
    "github.com/aws/jsii-runtime-go"
)

type SandboxAwsCdkStackProps struct {
    awscdk.StackProps
}

func NewSandboxAwsCdkStack(scope constructs.Construct, id string, props *SandboxAwsCdkStackProps) awscdk.Stack {
    var sprops awscdk.StackProps
    if props != nil {
        sprops = props.StackProps
    }
    stack := awscdk.NewStack(scope, &id, &sprops)

    // lambda function 設定
    lambdaFn := awslambda.NewFunction(stack, jsii.String("my-lambda-aws-cdk"), &awslambda.FunctionProps{
        FunctionName: jsii.String("my-lambda-aws-cdk-func"),
        Runtime:      awslambda.Runtime_GO_1_X(),
        Code:         awslambda.AssetCode_FromAsset(jsii.String("handler"), &awss3assets.AssetOptions{}),
        Handler:      jsii.String("main"),
    })

    // api gateway 設定
    apiGW := awsapigateway.NewRestApi(stack, jsii.String("my-api-gw-aws-cdk"), nil)
    apiGW.Root().
        AddResource(jsii.String("hello"), nil).
        AddMethod(jsii.String("POST"), awsapigateway.NewLambdaIntegration(lambdaFn, nil), nil)

    return stack
}

func main() {
    app := awscdk.NewApp(nil)

    NewSandboxAwsCdkStack(app, "SandboxAwsCdkStack", &SandboxAwsCdkStackProps{
        awscdk.StackProps{
            Env: env(),
        },
    })

    app.Synth(nil)
}

// env determines the AWS environment (account+region) in which our stack is to
// be deployed. For more information see: https://docs.aws.amazon.com/cdk/latest/guide/environments.html
func env() *awscdk.Environment {
    // If unspecified, this stack will be "environment-agnostic".
    // Account/Region-dependent features and context lookups will not work, but a
    // single synthesized template can be deployed anywhere.
    //---------------------------------------------------------------------------
    return nil
}

デプロイするために、まず下記コマンドを実行する必要があるので実行します。

% cdk bootstrap

cdk bootstrap実行後、下記コマンドで Lambda Function をビルドしデプロイします。

# ビルド
% GOARCH=amd64 GOOS=linux go build -o handler/main handler/main.go
# デプロイ
% cdk deploy
SandboxAwsCdkStack: deploying...
・・・
Outputs:
SandboxAwsCdkStack.myapigwawscdkEndpoint987172E9 = https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/

デプロイ後コンソール画面確認

f:id:techtouch:20211212142040p:plain

デプロイができたら下記リクエストを実行して、動作確認をします。

% curl -X POST -H "Content-Type: application/json" -d '{"name":"World!"}' https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/hello
Hello World!!

Hello World!! が出力されました。動作確認ができたら下記コマンドで削除しておきます。

% cdk destroy
SandboxAwsCdkStack: destroying...

 ✅  SandboxAwsCdkStack: destroyed

CDK for Terraform

続いて CDK for Terraform で構築します。下記コマンドでインストールしプロジェクトのセットアップをします。

# インストール
% brew install hashicorp/tap/terraform

# プロジェクトセットアップ
% mkdir sandbox-terraform-cdk && cd sandbox-terraform-cdk
% npm install --global cdktf-cli
% cdktf init --template="go" --local

今回は AWS を利用するので、cdkft.json に aws を追加します。

{
     "language": "go",
     "app": "go run main.go",
     "codeMakerOutput": "generated",
-    "terraformProviders": [],
+    "terraformProviders": [
+        "hashicorp/aws@~> 3.42"
+    ],
     "terraformModules": [],
     "context": {
         "excludeStackIdFromLogicalIds": "true",
         "allowSepCharsInLogicalIds": "true"
     }
 }

cdkft.json を編集後、下記コマンドを実行し AWS 用ライブラリをダウンロードします。これにはめちゃくちゃ時間がかかるので、気長に待ちましょう。ダウンロードが終わると generated ディレクトリにライブラリが大量に入ってきます。

% cdktf get
# めちゃくちゃ時間かかる..
downloading and generating modules and providers...

% go mod tidy

cdktf initを実行するとコードが自動生成されるので、必要な実装を追加します。ダウンロードした AWS ライブラリを利用するには"cdk.tf/go/stack/generated/hashicorp/aws"を import します。

// sandbox-terraform-cdk/main.go
package main

import (
    "path/filepath"
    "time"

    "cdk.tf/go/stack/generated/hashicorp/aws"
    "cdk.tf/go/stack/generated/hashicorp/aws/apigateway"
    "cdk.tf/go/stack/generated/hashicorp/aws/iam"
    "cdk.tf/go/stack/generated/hashicorp/aws/lambdafunction"
    "github.com/aws/constructs-go/constructs/v10"
    "github.com/aws/jsii-runtime-go"
    "github.com/hashicorp/terraform-cdk-go/cdktf"
)

func NewMyStack(scope constructs.Construct, id string) cdktf.TerraformStack {
    stack := cdktf.NewTerraformStack(scope, &id)

    // AWS 利用宣言
    aws.NewAwsProvider(stack, jsii.String("aws"), &aws.AwsProviderConfig{
        Region: jsii.String("ap-northeast-1"),
    })

    // role 作成
    role := iam.NewIamRole(stack, jsii.String("my-role-tf-cdk"), &iam.IamRoleConfig{
        AssumeRolePolicy: jsii.String(`{
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": "lambda.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        }`),
    })

    absPath, _ := filepath.Abs("handler/main.zip")

    // lambda function 設定
    lambdaFn := lambdafunction.NewLambdaFunction(stack, jsii.String("my-Lambda-tf-cdk"), &lambdafunction.LambdaFunctionConfig{
        FunctionName: jsii.String("my-lambda-tf-cdk-func"),
        Runtime:      jsii.String("go1.x"),
        Filename:     jsii.String(absPath),
        Handler:      jsii.String("main"),
        Role:         role.Arn(),
    })

    // api gateway 設定
    apiRest := apigateway.NewApiGatewayRestApi(stack, jsii.String("my-api-rest-tf-cdk"), &apigateway.ApiGatewayRestApiConfig{
        Name: jsii.String("my-api-gw-tf-cdk-name"),
    })

    apiResource := apigateway.NewApiGatewayResource(stack, jsii.String("my-api-resource-tf-cdk"), &apigateway.ApiGatewayResourceConfig{
        PathPart:  jsii.String("hello"),
        ParentId:  apiRest.RootResourceId(),
        RestApiId: apiRest.Id(),
    })

    apiMethod := apigateway.NewApiGatewayMethod(stack, jsii.String("my-api-method-tf-cdk"), &apigateway.ApiGatewayMethodConfig{
        ResourceId:    apiResource.Id(),
        RestApiId:     apiRest.Id(),
        HttpMethod:    jsii.String("POST"),
        Authorization: jsii.String("NONE"),
    })

    apiIntegration := apigateway.NewApiGatewayIntegration(stack, jsii.String("my-api-integration-tf-cdk"), &apigateway.ApiGatewayIntegrationConfig{
        RestApiId:             apiRest.Id(),
        ResourceId:            apiResource.Id(),
        HttpMethod:            apiMethod.HttpMethod(),
        IntegrationHttpMethod: jsii.String("POST"),
        Type:                  jsii.String("AWS_PROXY"),
        Uri:                   lambdaFn.InvokeArn(),
    })

    apigateway.NewApiGatewayDeployment(stack, jsii.String("my-api-dep-tf-cdk"), &apigateway.ApiGatewayDeploymentConfig{
        StageName:        jsii.String("dev"),
        RestApiId:        apiRest.Id(),
        StageDescription: jsii.String(time.Now().String()),
        Lifecycle: &cdktf.TerraformResourceLifecycle{
            CreateBeforeDestroy: jsii.Bool(true),
        },
        DependsOn: &[]cdktf.ITerraformDependable{
            apiIntegration,
        },
    })

    lambdafunction.NewLambdaPermission(stack, jsii.String("my-api-per-tf-cdk"), &lambdafunction.LambdaPermissionConfig{
        Action:       jsii.String("lambda:InvokeFunction"),
        FunctionName: lambdaFn.FunctionName(),
        Principal:    jsii.String("apigateway.amazonaws.com"),
    })

    return stack
}

func main() {
    app := cdktf.NewApp(nil)

    NewMyStack(app, "sandbox-terraform-cdk")

    app.Synth()
}

下記コマンドでビルドしデプロイします(zip ファイルが必要です)。

# ビルド
% GOARCH=amd64 GOOS=linux go build -o handler/main handler/main.go
% zip -j ./handler/main.zip ./handler/main
# デプロイ
% cdktf deploy

下記リクエストを実行して動作確認をします。

% curl -X POST -H "Content-Type: application/json" -d '{"name":"World!"}' https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello 
Hello World!!

Hello World!! が出力されました。下記コマンドで削除できると思ったのですが、やり方が悪いのか削除されず、手動で削除しました。

% cdktf destroy

CDK for Terraform は慣れていないからか、実験的だからか分かりませんが、動かすのに時間がかかりました。

Pulumi

最後に Pulumi を確認します。下記コマンドでインストールし、プロジェクトをセットアップします。

# インストール
% brew install pulumi

# AWSアカウントセットアップ
% pulumi config set aws:profile {profile-name}

or

% export aws_access_key_id=xxxxxxxxxxxxxxxxxxx
% export aws_secret_access_key=xxxxxxxxxxxxxxxxxxx

# プロジェクトセットアップ
% mkdir sandbox-pulumi && cd sandbox-pulumi
% pulumi new aws-go

# 下記ルールでインフラと言語のテンプレート指定が可能
# pulumi new <infra>-<language>

pulumi newすると下記コードが自動生成されます。

// sandbox-pulumi/main.go
package main

import (
    "github.com/pulumi/pulumi-aws/sdk/v4/go/aws/s3"
    "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
    pulumi.Run(func(ctx *pulumi.Context) error {
        // Create an AWS resource (S3 Bucket)
        bucket, err := s3.NewBucket(ctx, "my-bucket", nil)
        if err != nil {
            return err
        }

        // Export the name of the bucket
        ctx.Export("bucketName", bucket.ID())
        return nil
    })
}

このまま進めてもよいですが、Pulumi は GitHub の examples リポジトリにサンプルがたくさん用意されているので、ここでは example コードをそのまま利用します。下記コマンドを実行すると examples の aws-go-lambda-gateway のコードがそのまま実行できます。

% git clone https://github.com/pulumi/examples
% cd examples/aws-go-lambda-gateway

# Lambda Function ビルド
% make build

# pulumi セットアップ
% pulumi stack init
% pulumi config set aws:region ap-northeast-1

# デプロイ
% pulumi up
Previewing update (dev)

View Live: https://app.pulumi.com/taisa831/go-lambda-gateway/dev/previews/e0cd482e-b3d7-41c4-9c23-8041fd6a249c

     Type                           Name                   Plan
     pulumi:pulumi:Stack            go-lambda-gateway-dev
 +   ├─ aws:iam:Role                task-exec-role         create
 +   ├─ aws:apigateway:RestApi      UpperCaseGateway       create
 +   ├─ aws:apigateway:Resource     UpperAPI               create
 +   ├─ aws:iam:RolePolicy          lambda-log-policy      create
 +   ├─ aws:apigateway:Method       AnyMethod              create
 +   ├─ aws:lambda:Function         basicLambda            create
 +   ├─ aws:lambda:Permission       APIPermission          create
 +   ├─ aws:apigateway:Integration  LambdaIntegration      create
 +   └─ aws:apigateway:Deployment   APIDeployment          create

Outputs:
  + invocation URL: output<string>

Resources:
    + 9 to create
    1 unchanged

Do you want to perform this update? yes
Updating (dev)

View Live: https://app.pulumi.com/taisa831/go-lambda-gateway/dev/updates/4

     Type                           Name                   Status
     pulumi:pulumi:Stack            go-lambda-gateway-dev
 +   ├─ aws:iam:Role                task-exec-role         created
 +   ├─ aws:apigateway:RestApi      UpperCaseGateway       created
 +   ├─ aws:apigateway:Resource     UpperAPI               created
 +   ├─ aws:apigateway:Method       AnyMethod              created
 +   ├─ aws:iam:RolePolicy          lambda-log-policy      created
 +   ├─ aws:lambda:Function         basicLambda            created
 +   ├─ aws:apigateway:Integration  LambdaIntegration      created
 +   ├─ aws:lambda:Permission       APIPermission          created
 +   └─ aws:apigateway:Deployment   APIDeployment          created

Outputs:
  + invocation URL: "https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/{message}"

Resources:
    + 9 created
    1 unchanged

Duration: 21s

AWS CDK と CDK for Terraform で確認した Lambda Function とは少し違いますが、下記リクエストで動作確認ができます。実行すると {message} の内容が大文字に変換されて返ってきます。

# 例)https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/{message}
# MESSAGE

% curl https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/Hello,World!
HELLO,WORLD!

下記コマンドで削除できます。

% pulumi destroy
Destroying (dev)

View Live: https://app.pulumi.com/taisa831/go-lambda-gateway/dev/updates/8

     Type                           Name                   Status      
 -   pulumi:pulumi:Stack            go-lambda-gateway-dev  deleted     
 -   ├─ aws:apigateway:Deployment   APIDeployment          deleted     
 -   ├─ aws:lambda:Permission       APIPermission          deleted     
 -   ├─ aws:apigateway:Integration  LambdaIntegration      deleted     
 -   ├─ aws:apigateway:Method       AnyMethod              deleted     
 -   ├─ aws:lambda:Function         basicLambda            deleted     
 -   ├─ aws:apigateway:Resource     UpperAPI               deleted     
 -   ├─ aws:iam:RolePolicy          lambda-log-policy      deleted     
 -   ├─ aws:iam:Role                task-exec-role         deleted     
 -   └─ aws:apigateway:RestApi      UpperCaseGateway       deleted     
 
Outputs:
  - invocation URL: "https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/{message}"

Resources:
    - 10 deleted

Duration: 9s

また、pulumi stack historyコマンドで実行履歴を確認したり、管理画面が提供されるので、管理画面からアクティビティの詳細を確認したりいろいろできるようです。この辺りはまた別の機会に触ってみようと思います。

実行履歴

% pulumi stack history
Version: 11
UpdateKind: destroy
Status: succeeded
Message: Add Crosswalk API Gateway multi-language examples (#1122)
+0-10~0 0 Updated 20 hours ago took 9s
    exec.kind: cli
    git.author: Daniel Bradley
    git.author.email: daniel@pulumi.com
    git.committer: GitHub
    git.committer.email: noreply@github.com
    git.dirty: true
    git.head: 5b1b42e6bd2da3461e36630965f343e365ea5ff3
    git.headName: refs/heads/master
    vcs.kind: github.com
    vcs.owner: pulumi
    vcs.repo: examples

Version: 10
UpdateKind: update
Status: succeeded
Message: Add Crosswalk API Gateway multi-language examples (#1122)
+9-0~0 1 Updated 20 hours ago took 21s
    exec.kind: cli
    git.author: Daniel Bradley
    git.author.email: daniel@pulumi.com
    git.committer: GitHub
    git.committer.email: noreply@github.com
    git.dirty: true
    git.head: 5b1b42e6bd2da3461e36630965f343e365ea5ff3
    git.headName: refs/heads/master
    vcs.kind: github.com
    vcs.owner: pulumi
    vcs.repo: examples

アクティビティの詳細画面

f:id:techtouch:20211213113610p:plain

Pulumi は他と違い Go がサポート対象なのもあり、ドキュメントやサンプルが充実していて、使っていて安定感を感じました。

まとめ

AWS CDK・CDK for Terraform・Pulumi それぞれを実際に触ってみました。個人的に AWS CDK と Pulumi は比較的すんなり使えてよかったです。特に Pulumi に関してはドキュメントの充実さやデプロイの速さなど好感触だったので今後も動向を追ってみたいと思います。

明日は @macchiitaka の「React Query のレンダリング最適化を目指した話」です!