PulumiをGoで書いてみる

やろうとは思っていたものの、手を出さずにいたのでこれを機にやってみよう。

# Pulumiってなに？

https://www.pulumi.com/
Pulumiとは様々なプログラミング言語から好きなものを使ってインフラのプロビジョニングやデプロイができるツールです。
昨今界隈で使われているTerraformみたいなものですね。

TerraformがHCLという独自の言語を利用しているのに対し、Pulumiはjavascript/typescript/python/C#/F#/VB/Go （今現在pulumi new -hで確認できる言語) が利用できる模様。
AWSはもちろんGCP/AzureやAliCloud、K8s、OpenStackまでサポートされているようです。
最近の流行りとしてコンテナ化が進んでいるので、EKS/GKE/AKSあたりに対応されているのはすばらしい。

普段私はRails(Ruby)での開発が多く、たまにtypescriptを使うくらい。
なので、今回はGoで書いてみたいと思います。

## 準備
macosで開発しているので、goとpulumiは両方ともbrewでインストールします。
macosはBig Sur(11.1)です。

```zsh
$ brew install go
$ brew install pulumi
```

ちなみに、私がpulumiの噂を聞いたのは去年くらいで、その時のバージョンが1.0が出たとかどうとか。
これを書くにあたってpulumiコマンドを実行したところ2.16.0へのアップグレードを促されました。（なのでアップグレード）
リリースサイクルがとても早いみたいですね。
アップグレードした次の日に2.16.1へのアップグレードを促され、今日(12/25)は2.16.2へのアップグレードを促されました。。すごい。
Goのバージョンは1.15.6です。

インストールしたらAWS関連の環境変数を設定しておきます。

```zsh
$ export AWS_REGION=ap-northeast-1
$ export AWS_PROFILE=<YOUR_PROFILE>
$ export AWS_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>
$ export AWS_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>
```

これ、`$ pulumi config set aws:profile hogehoge`でprofileを指定できるのですが、のちの`pulumi up`時に毎回keyが設定されていないエラーが発生していたので環境変数を設定しています。

## 構成
今回はGoで書いてみたかっただけなので、構成は簡素なものになります。
AWSにそれぞれ

- VPC
- EC2
- ALB
- S3

をデプロイする流れです。

## コードを書く

最初の雛形を作成します。

```zsh
$ mkdir pulumi-go
$ cd pulumi-go
$ pulumi new aws-go
```
ズラズラーと流れてきますが大体はそのままEnterでOK。

スタックはdev/prodを設定しておき、devを選択しておきます。

```zsh
$ pulumi stack init dev
$ pulumi stack init prod
$ pulumi stack select dev
```

最終的なファイル構成はこんな感じ。

```zsh
.
└── pulumi-go
   ├── alb.go
   ├── ec2.go
   ├── go.mod
   ├── go.sum
   ├── main.go
   ├── network.go
   ├── Pulumi.dev.yaml
   ├── Pulumi.yaml
   ├── README.md
   ├── s3.go
   ├── security.go
   └── setup.go
```

### Main
main.goが実行されるのでそこにコードを書いていきます。

```go:main.go
package main

import (
	"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		err := setup(ctx)
		if err != nil {
			return err
		}

		return nil
	})
}
```

```go:setup.go
package main

import (
	"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)

func setup(ctx *pulumi.Context) error {
	// VPC
	opt, err := createVpc(ctx)
	if err != nil {
		return err
	}

	// SecurityGroup
	sg, err := createSg(ctx, opt)
	if err != nil {
		return err
	}

	// S3
	err = createS3(ctx)
	if err != nil {
		return err
	}

	// EC2
	err = createEc2(ctx, opt, sg)
	if err != nil {
		return err
	}

	// ALB
	err = createAlb(ctx, opt, sg)
	if err != nil {
		return err
	}

	return nil
}
```

### VPC
デフォルトのVPCは利用せずに新しく作成します。
あとで利用するので構造体に入れておきます。

```go:network.go
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v3/go/aws/ec2"
	"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)

type Opt struct {
	vpc     interface{}
	subnet  interface{}
	subnet2 interface{}
	err     interface{}
}

func createVpc(ctx *pulumi.Context) (*Opt, error) {
	vpc, err := ec2.NewVpc(ctx, "app-dev-vpc", &ec2.VpcArgs{
		CidrBlock: pulumi.String("10.0.0.0/16"),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("app-dev-vpc1"),
		},
	})
	if err != nil {
		return nil, err
	}

	gateway, err := ec2.NewInternetGateway(ctx, "app-dev-gw", &ec2.InternetGatewayArgs{
		VpcId: vpc.ID(),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("app-dev-gw1"),
		},
	})
	if err != nil {
		return nil, err
	}

	_, err = ec2.NewRouteTable(ctx, "app-dev-rt", &ec2.RouteTableArgs{
		VpcId: vpc.ID(),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("app-dev-rt1"),
		},
		Routes: ec2.RouteTableRouteArray{
			&ec2.RouteTableRouteArgs{
				CidrBlock: pulumi.String("0.0.0.0/0"),
				GatewayId: gateway.ID(),
			},
		},
	})
	if err != nil {
		return nil, err
	}

	subnet, err := ec2.NewSubnet(ctx, "app-dev-subnet1", &ec2.SubnetArgs{
		VpcId:            vpc.ID(),
		CidrBlock:        pulumi.String("10.0.2.0/24"),
		AvailabilityZone: pulumi.String("ap-northeast-1b"),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("app-dev-subnet1"),
		},
	})
	if err != nil {
		return nil, err
	}

	subnet2, err := ec2.NewSubnet(ctx, "app-dev-subnet2", &ec2.SubnetArgs{
		VpcId:            vpc.ID(),
		CidrBlock:        pulumi.String("10.0.3.0/24"),
		AvailabilityZone: pulumi.String("ap-northeast-1c"),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("app-dev-subnet2"),
		},
	})
	if err != nil {
		return nil, err
	}

	opt := new(Opt)
	opt.vpc = vpc
	opt.subnet = subnet
	opt.subnet2 = subnet2
	opt.err = err

	return opt, nil
}
```

### SecurityGroup
EC2とALB用に作成します。
こちらも構造体に入れておきます。

```go:security.go
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v3/go/aws/ec2"
	"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)

type Sg struct {
	SecurityGroupEc2 interface{}
	SecurityGroupAlb interface{}
}

func createSg(ctx *pulumi.Context, opt *Opt) (*Sg, error) {
	// EC2 SecurityGroup
	sg1, err := ec2.NewSecurityGroup(ctx, "app-dev-ec2-sg", &ec2.SecurityGroupArgs{
		VpcId: opt.vpc.(*ec2.Vpc).ID(),
		Ingress: ec2.SecurityGroupIngressArray{
			ec2.SecurityGroupIngressArgs{
				CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				FromPort:   pulumi.Int(22),
				ToPort:     pulumi.Int(22),
				Protocol:   pulumi.String("TCP"),
			},
			ec2.SecurityGroupIngressArgs{
				CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				FromPort:   pulumi.Int(80),
				ToPort:     pulumi.Int(80),
				Protocol:   pulumi.String("TCP"),
			},
		},
		Egress: ec2.SecurityGroupEgressArray{
			ec2.SecurityGroupEgressArgs{
				CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				FromPort:   pulumi.Int(0),
				ToPort:     pulumi.Int(0),
				Protocol:   pulumi.String("-1"),
			},
		},
		Tags: pulumi.StringMap{
			"Name": pulumi.String("app-dev-sg1"),
		},
	})
	if err != nil {
		return nil, err
	}

	// ALB SecurityGroup
	sg2, err := ec2.NewSecurityGroup(ctx, "app-dev-lb-sg", &ec2.SecurityGroupArgs{
		VpcId: opt.vpc.(*ec2.Vpc).ID(),
		Ingress: ec2.SecurityGroupIngressArray{
			ec2.SecurityGroupIngressArgs{
				CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				FromPort:   pulumi.Int(80),
				ToPort:     pulumi.Int(80),
				Protocol:   pulumi.String("TCP"),
			},
		},
		Egress: ec2.SecurityGroupEgressArray{
			ec2.SecurityGroupEgressArgs{
				CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				FromPort:   pulumi.Int(0),
				ToPort:     pulumi.Int(0),
				Protocol:   pulumi.String("-1"),
			},
		},
		Tags: pulumi.StringMap{
			"Name": pulumi.String("app-dev-sg2"),
		},
	})
	if err != nil {
		return nil, err
	}

	sg := new(Sg)
	sg.SecurityGroupEc2 = sg1
	sg.SecurityGroupAlb = sg2

	return sg, nil
}
```

### S3
細かいポリシー等はいったんなしで構築します。

```go:s3.go
package main

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

func createS3(ctx *pulumi.Context) error {
	_, err := s3.NewBucket(ctx, "app-dev-bucket", nil)
	if err != nil {
		return err
	}

	return nil
}
```

### EC2
公式のAMIからインスタンスを立ち上げ、サブネットとセキュリティグループの指定をします。

```go:ec2.go
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v3/go/aws/ec2"
	"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)

func createEc2(ctx *pulumi.Context, opt *Opt, sg *Sg) error {
	// Instance
	_, err := ec2.NewInstance(ctx, "app-dev-web", &ec2.InstanceArgs{
		Ami:                      pulumi.String("ami-01748a72bed07727c"), // Amazon Linux 2 AMI
		InstanceType:             pulumi.String("t3.micro"),
		SubnetId:                 opt.subnet.(*ec2.Subnet).ID(),
		VpcSecurityGroupIds:      pulumi.StringArray{sg.SecurityGroupEc2.(*ec2.SecurityGroup).ID()},
		KeyName:                  pulumi.String("app-dev"),
		AssociatePublicIpAddress: pulumi.Bool(true),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("app-dev-web1"),
		},
	})
	if err != nil {
		return err
	}

	return nil
}
```

ここで少しハマったのがSubnetIdとVpcSecurityGroupIdsの指定方法です。
基本Pulumiは各リソースの依存関係をうまいこと解決してくれるのですが、なにも考えずにたとえば

```go
...(省略
		SubnetId:                 pulumi.String(opt.SubnetID),
		VpcSecurityGroupIds:      pulumi.StringArray{sg.SecurityGroupID},
```
みたいな記述をしてもうまく依存を解決してくれませんでした。
（先にインスタンスを作成してあとからvpcを作成しようとするのでエラーが発生）
なので、構造体には作成したオブジェクトを`interface{}`に入れ、利用する際に`.(*ec2.Vpc)`のように指定することで依存関係を解決できました。
正直これであっているのかわからない。。

### ALB
単純に80番で受けて80番へ流す設定です。

```go:alb.go
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v3/go/aws/ec2"
	"github.com/pulumi/pulumi-aws/sdk/v3/go/aws/lb"
	"github.com/pulumi/pulumi/sdk/v2/go/pulumi"
)

func createAlb(ctx *pulumi.Context, opt *Opt, sg *Sg) error {
	// alb(subnet)
	alb, err := lb.NewLoadBalancer(ctx, "app-dev-alb", &lb.LoadBalancerArgs{
		SecurityGroups: pulumi.StringArray{sg.SecurityGroupAlb.(*ec2.SecurityGroup).ID()},
		Subnets: pulumi.StringArray{
			opt.subnet.(*ec2.Subnet).ID(),
			opt.subnet2.(*ec2.Subnet).ID(),
		},
	})
	if err != nil {
		return err
	}

	// tg
	tg, err := lb.NewTargetGroup(ctx, "app-dev-tg", &lb.TargetGroupArgs{
		Port:     pulumi.Int(80),
		Protocol: pulumi.String("HTTP"),
		VpcId:    opt.vpc.(*ec2.Vpc).ID(),
	})
	if err != nil {
		return err
	}

	// listener
	_, err = lb.NewListener(ctx, "app-dev-listener", &lb.ListenerArgs{
		LoadBalancerArn: alb.Arn,
		Port:            pulumi.Int(80),
		Protocol:        pulumi.String("HTTP"),
		DefaultActions: lb.ListenerDefaultActionArray{
			&lb.ListenerDefaultActionArgs{
				Type:           pulumi.String("forward"),
				TargetGroupArn: tg.Arn,
			},
		},
	})
	if err != nil {
		return err
	}

	return nil
}
```

## ビルド/デプロイ
Goをビルドしてチェックします。

```zsh
$ go get .
```

`pulumi up`してAWSに構築してみます。
途中でてくるyes/no/detailsはyesを選択することで実際にAWSへのデプロイが発生します。

```zsh
$ pulumi up -d -r
Previewing update (dev)

（省略）
     Type                        Name              Plan       Info
 +   pulumi:pulumi:Stack         pulumi-go-dev     create     3 debugs
 +   ├─ aws:ec2:Vpc              app-dev-vpc       create
 +   ├─ aws:s3:Bucket            app-dev-bucket    create
 +   ├─ aws:ec2:Subnet           app-dev-subnet2   create
 +   ├─ aws:ec2:Subnet           app-dev-subnet1   create
 +   ├─ aws:ec2:SecurityGroup    app-dev-lb-sg     create
 +   ├─ aws:lb:TargetGroup       app-dev-tg        create
 +   ├─ aws:ec2:SecurityGroup    app-dev-ec2-sg    create
 +   ├─ aws:ec2:InternetGateway  app-dev-gw        create
 +   ├─ aws:lb:LoadBalancer      app-dev-alb       create
 +   ├─ aws:ec2:Instance         app-dev-web       create
 +   ├─ aws:ec2:RouteTable       app-dev-rt        create
 +   └─ aws:lb:Listener          app-dev-listener  create

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

（省略）
```

必要なくなったら`pulumi destroy`します。

```zsh
$ pulumi destroy
Previewing destroy (dev)

（省略）

     Type                        Name              Plan
 -   pulumi:pulumi:Stack         pulumi-go-dev     delete
 -   ├─ aws:lb:Listener          app-dev-listener  delete
 -   ├─ aws:lb:LoadBalancer      app-dev-alb       delete
 -   ├─ aws:ec2:Subnet           app-dev-subnet2   delete
 -   ├─ aws:ec2:Instance         app-dev-web       delete
 -   ├─ aws:ec2:Subnet           app-dev-subnet1   delete
 -   ├─ aws:ec2:RouteTable       app-dev-rt        delete
 -   ├─ aws:ec2:InternetGateway  app-dev-gw        delete
 -   ├─ aws:ec2:Vpc              app-dev-vpc       delete
 -   ├─ aws:ec2:SecurityGroup    app-dev-lb-sg     delete
 -   ├─ aws:lb:TargetGroup       app-dev-tg        delete
 -   ├─ aws:s3:Bucket            app-dev-bucket    delete
 -   └─ aws:ec2:SecurityGroup    app-dev-ec2-sg    delete

Do you want to perform this destroy? yes
Destroying (dev)

（省略）
```

一通りの流れができるようになりました。

## 所感
PulumiはTerraformにくらべアップデート速度が早く、普段利用しているプログラミング言語をつかって書くことができるのは好印象です。
またWebUIも存在していて、見た目もさることながらこれまでのアクティビティがログとして残っているのも良い感じ。
ドキュメントも整備されていてよかったです。ただGoはまだ新しいらしくComing soon!がちらほら。

コードの記述量はTerraformのほうが簡素かなといった印象。とはいえtypescriptやpythonで書くならそこまで差はないかと。

惜しむらくは、Ruby/Rustの対応がされていないこと。コミュニティを盛り上げていけばいずれは・・・！
なんにせよ今日からインフラのことを聞かれたら、**時代はPulumiですよ！**と応えていくことにしよう。

_Infrastructure as Code!!_