LoginSignup
2

More than 3 years have passed since last update.

PulumiをGoで書いてみる

Last updated at Posted at 2020-12-25

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

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が実行されるのでそこにコードを書いていきます。

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
    })
}
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は利用せずに新しく作成します。
あとで利用するので構造体に入れておきます。

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用に作成します。
こちらも構造体に入れておきます。

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

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

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からインスタンスを立ち上げ、サブネットとセキュリティグループの指定をします。

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は各リソースの依存関係をうまいこと解決してくれるのですが、なにも考えずにたとえば

...(省略
        SubnetId:                 pulumi.String(opt.SubnetID),
        VpcSecurityGroupIds:      pulumi.StringArray{sg.SecurityGroupID},

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

ALB

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

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をビルドしてチェックします。

$ 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!!

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2