やろうとは思っていたものの、手を出さずにいたのでこれを機にやってみよう。
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)です。
$ 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関連の環境変数を設定しておきます。
$ 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
をデプロイする流れです。
コードを書く
最初の雛形を作成します。
$ mkdir pulumi-go
$ cd pulumi-go
$ pulumi new aws-go
ズラズラーと流れてきますが大体はそのままEnterでOK。
スタックはdev/prodを設定しておき、devを選択しておきます。
$ pulumi stack init dev
$ pulumi stack init prod
$ pulumi stack select dev
最終的なファイル構成はこんな感じ。
.
└── 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が実行されるのでそこにコードを書いていきます。
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
})
}
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は利用せずに新しく作成します。
あとで利用するので構造体に入れておきます。
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用に作成します。
こちらも構造体に入れておきます。
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
細かいポリシー等はいったんなしで構築します。
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からインスタンスを立ち上げ、サブネットとセキュリティグループの指定をします。
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は各リソースの依存関係をうまいこと解決してくれるのですが、なにも考えずにたとえば
...(省略
SubnetId: pulumi.String(opt.SubnetID),
VpcSecurityGroupIds: pulumi.StringArray{sg.SecurityGroupID},
みたいな記述をしてもうまく依存を解決してくれませんでした。
(先にインスタンスを作成してあとからvpcを作成しようとするのでエラーが発生)
なので、構造体には作成したオブジェクトをinterface{}
に入れ、利用する際に.(*ec2.Vpc)
のように指定することで依存関係を解決できました。
正直これであっているのかわからない。。
ALB
単純に80番で受けて80番へ流す設定です。
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をビルドしてチェックします。
$ go get .
pulumi up
してAWSに構築してみます。
途中でてくるyes/no/detailsはyesを選択することで実際にAWSへのデプロイが発生します。
$ 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
します。
$ 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!!