🌱

Pulumi&Goを利用してAWS上でWebサーバ一式を立ててみた

2022/08/05に公開

はじめに

普段はTerraformを利用していますが、PulumiだったりSDKだったり、汎用言語を使ってAWSリソースを作ってみたかったのでやってみました。
言語は現在勉強しているGoを使いました。

チュートリアルをやった記事

以前チュートリアルをやった話を個人ブログに書いたので参考までに記載します。
https://guranytou.github.io/blog/post/20220523-create-aws-resource-use-pulumi-1/

実装について

実際にALB-EC2でWebサーバを立ててみました。なお構成図は以下のリポジトリにあります。
コードが置いてあるリポジトリ: https://github.com/guranytou/pulumi-ec2-alb

ファイル配置

以下のようにファイルを配置しています。

├── main.go
├── network.go
└── application.go

各ファイルの簡単な解説

network.go

ネットワークに関するリソースを定義しています。

network.go
package main

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

type Network struct {
	VpcID           pulumi.IDOutput
	PublicSubnetIDs []pulumi.IDOutput
	PrivateSubnetID pulumi.IDOutput
}

func createNetwork(ctx *pulumi.Context) (*Network, error) {
	vpc, err := ec2.NewVpc(ctx, "example_vpc", &ec2.VpcArgs{
		CidrBlock: pulumi.String("10.100.0.0/16"),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("example_vpc"),
		},
	})
	if err != nil {
		return nil, err
	}

	var pubSubIDs []pulumi.IDOutput
	pubSub1a, err := ec2.NewSubnet(ctx, "pubSub1a", &ec2.SubnetArgs{
		VpcId:            vpc.ID(),
		CidrBlock:        pulumi.String("10.100.0.0/24"),
		AvailabilityZone: pulumi.String("ap-northeast-1a"),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("example_public_1a"),
		},
	})
	if err != nil {
		return nil, err
	}

	pubSubIDs = append(pubSubIDs, pubSub1a.ID())

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

	pubSubIDs = append(pubSubIDs, pubSub1c.ID())

	priSub1a, err := ec2.NewSubnet(ctx, "priSub1a", &ec2.SubnetArgs{
		VpcId:            vpc.ID(),
		CidrBlock:        pulumi.String("10.100.100.0/24"),
		AvailabilityZone: pulumi.String("ap-northeast-1a"),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("example_private_1a"),
		},
	})
	if err != nil {
		return nil, err
	}

	eip, err := ec2.NewEip(ctx, "eip", &ec2.EipArgs{
		Vpc: pulumi.Bool(true),
	})
	if err != nil {
		return nil, err
	}

	igw, err := ec2.NewInternetGateway(ctx, "igw", &ec2.InternetGatewayArgs{
		VpcId: vpc.ID(),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("example_igw"),
		},
	})
	if err != nil {
		return nil, err
	}

	natgw, err := ec2.NewNatGateway(ctx, "natgw", &ec2.NatGatewayArgs{
		AllocationId: eip.ID(),
		SubnetId:     pubSub1a.ID(),
	}, pulumi.DependsOn([]pulumi.Resource{eip}))
	if err != nil {
		return nil, err
	}

	pubRoutaTable, err := ec2.NewRouteTable(ctx, "pubRouteTable", &ec2.RouteTableArgs{
		VpcId: vpc.ID(),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("example_route_table_public"),
		},
	})
	if err != nil {
		return nil, err
	}

	_, err = ec2.NewRoute(ctx, "pubRoute", &ec2.RouteArgs{
		RouteTableId:         pubRoutaTable.ID(),
		DestinationCidrBlock: pulumi.String("0.0.0.0/0"),
		GatewayId:            igw.ID(),
	})
	if err != nil {
		return nil, err
	}

	_, err = ec2.NewRouteTableAssociation(ctx, "pubRoute1a", &ec2.RouteTableAssociationArgs{
		SubnetId:     pubSub1a.ID(),
		RouteTableId: pubRoutaTable.ID(),
	})
	if err != nil {
		return nil, err
	}

	_, err = ec2.NewRouteTableAssociation(ctx, "pubRoute1c", &ec2.RouteTableAssociationArgs{
		SubnetId:     pubSub1c.ID(),
		RouteTableId: pubRoutaTable.ID(),
	})
	if err != nil {
		return nil, err
	}

	priRouteTable, err := ec2.NewRouteTable(ctx, "priRouteTable", &ec2.RouteTableArgs{
		VpcId: vpc.ID(),
		Tags: pulumi.StringMap{
			"Name": pulumi.String("example_route_table_private"),
		},
	})
	if err != nil {
		return nil, err
	}

	_, err = ec2.NewRoute(ctx, "priRoute", &ec2.RouteArgs{
		RouteTableId:         priRouteTable.ID(),
		DestinationCidrBlock: pulumi.String("0.0.0.0/0"),
		NatGatewayId:         natgw.ID(),
	})
	if err != nil {
		return nil, err
	}

	_, err = ec2.NewRouteTableAssociation(ctx, "priRoute1a", &ec2.RouteTableAssociationArgs{
		SubnetId:     priSub1a.ID(),
		RouteTableId: priRouteTable.ID(),
	})
	if err != nil {
		return nil, err
	}

	network := new(Network)
	network.VpcID = vpc.ID()
	network.PublicSubnetIDs = pubSubIDs
	network.PrivateSubnetID = priSub1a.ID()

	return network, nil
}

application.go

EC2及びALB関連リソースを定義しています。
なお、EC2立ち上げ時に呼び出すユーザデータは install_apache.shとして別に切り出しています。

application.go
package main

import (
	"encoding/base64"
	"io/ioutil"
	"os"

	"github.com/pulumi/pulumi-aws/sdk/v5/go/aws/ec2"
	"github.com/pulumi/pulumi-aws/sdk/v5/go/aws/lb"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func createApplication(ctx *pulumi.Context) error {
	network, err := createNetwork(ctx)
	if err != nil {
		return err
	}

	sgForALB, err := ec2.NewSecurityGroup(ctx, "sgForALB", &ec2.SecurityGroupArgs{
		Name:  pulumi.String("example_sg_for_ALB"),
		VpcId: network.VpcID,
		Ingress: ec2.SecurityGroupIngressArray{
			&ec2.SecurityGroupIngressArgs{
				FromPort: pulumi.Int(80),
				ToPort:   pulumi.Int(80),
				Protocol: pulumi.String("tcp"),
				CidrBlocks: pulumi.StringArray{
					pulumi.String("0.0.0.0/0"),
				},
			},
		},
		Egress: ec2.SecurityGroupEgressArray{
			&ec2.SecurityGroupEgressArgs{
				FromPort: pulumi.Int(0),
				ToPort:   pulumi.Int(0),
				Protocol: pulumi.String("-1"),
				CidrBlocks: pulumi.StringArray{
					pulumi.String("0.0.0.0/0"),
				},
			},
		},
	})
	if err != nil {
		return err
	}

	alb, err := lb.NewLoadBalancer(ctx, "ALB", &lb.LoadBalancerArgs{
		Name:             pulumi.String("example"),
		LoadBalancerType: pulumi.String("application"),
		SecurityGroups: pulumi.StringArray{
			sgForALB.ID(),
		},
		Subnets: pulumi.StringArray{
			network.PublicSubnetIDs[0],
			network.PublicSubnetIDs[1],
		},
		Tags: pulumi.StringMap{
			"Name": pulumi.String("example"),
		},
	})
	if err != nil {
		return err
	}

	httpTG, err := lb.NewTargetGroup(ctx, "httpTG", &lb.TargetGroupArgs{
		Name:     pulumi.String("HTTPTG"),
		Port:     pulumi.Int(80),
		Protocol: pulumi.String("HTTP"),
		VpcId:    network.VpcID,
		HealthCheck: lb.TargetGroupHealthCheckArgs{
			Path:     pulumi.String("/"),
			Matcher:  pulumi.String("403"),
			Port:     pulumi.String("80"),
			Protocol: pulumi.String("HTTP"),
		},
	})
	if err != nil {
		return err
	}

	_, err = lb.NewListener(ctx, "listener", &lb.ListenerArgs{
		LoadBalancerArn: alb.Arn,
		Port:            pulumi.Int(80),
		Protocol:        pulumi.String("HTTP"),
		DefaultActions: lb.ListenerDefaultActionArray{
			&lb.ListenerDefaultActionArgs{
				Type:           pulumi.String("forward"),
				TargetGroupArn: httpTG.Arn,
			},
		},
	}, pulumi.DependsOn([]pulumi.Resource{alb}))
	if err != nil {
		return err
	}

	sgForInstance, err := ec2.NewSecurityGroup(ctx, "sgForInstance", &ec2.SecurityGroupArgs{
		Name:  pulumi.String("example_sg_for_instance"),
		VpcId: network.VpcID,
		Ingress: ec2.SecurityGroupIngressArray{
			&ec2.SecurityGroupIngressArgs{
				FromPort: pulumi.Int(80),
				ToPort:   pulumi.Int(80),
				Protocol: pulumi.String("tcp"),
				SecurityGroups: pulumi.StringArray{
					sgForALB.ID(),
				},
			},
		},
		Egress: ec2.SecurityGroupEgressArray{
			&ec2.SecurityGroupEgressArgs{
				FromPort: pulumi.Int(0),
				ToPort:   pulumi.Int(0),
				Protocol: pulumi.String("-1"),
				CidrBlocks: pulumi.StringArray{
					pulumi.String("0.0.0.0/0"),
				},
			},
		},
	})
	if err != nil {
		return err
	}

	f, err := os.Open("install_apache.sh")
	if err != nil {
		return err
	}

	b, err := ioutil.ReadAll(f)
	if err != nil {
		return err
	}

	enc := base64.StdEncoding.EncodeToString(b)

	ins, err := ec2.NewInstance(ctx, "instance", &ec2.InstanceArgs{
		Ami:          pulumi.String("ami-0992fc94ca0f1415a"),
		InstanceType: pulumi.String("t2.micro"),
		SubnetId:     network.PrivateSubnetID,
		VpcSecurityGroupIds: pulumi.StringArray{
			sgForInstance.ID(),
		},
		UserDataBase64: pulumi.String(enc),
	})
	if err != nil {
		return err
	}

	_, err = lb.NewTargetGroupAttachment(ctx, "TGattach", &lb.TargetGroupAttachmentArgs{
		TargetGroupArn: httpTG.Arn,
		TargetId:       ins.ID(),
		Port:           pulumi.Int(80),
	})
	if err != nil {
		return err
	}

	return nil
}

main.go

EC2&ALBを作成する関数を呼び出しています。

main.go
package main

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

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

Goで書いてみての感想

Terraformと同じような感覚で書けるので、Terraform書ける人はパッと書けるなと思いました。
インフラ構築を汎用言語で選択する場合、フロントエンドやバックエンドで利用している言語と同じ言語を使えるので、システム全体の統一性が上がるのと学習コストの圧縮ができるのかなあと感じました。
個人的には少なくともGoで書くよりTerraformで書く方がシンプルかな?と思ったので、特別な理由がない限りはTerraformを選択しそうです。TypeScriptなど別の言語だとまた違う感想が出そうですね。

Discussion