ちゃんるいすのブログ

オタクエンジニアの雑記

Pulumi を使えば Infrastructure as Code の本来の目的が果たせると思う


IaC は DevOps の中で重要な立ち位置に居て、インフラ専門部隊だけではなく、バックエンドの開発者も柔軟に構成が変更できるためには
今までの Terraform や Ansible では敷居が高かった(ツール独自のループの書き方とか色々)。だけど Pulumi や aws cdk の登場により好きな言語でリソース管理ができるならその敷居はもっと下がって無駄なコストを減らせると思う(リソースの変更を願いせずとも PR 出せばいいとか)

Pulumi

好きな言語で Infrastructure as Code を実現できるフレームワーク?ツール?
AWS だけではなく、GCP、Azure、Kubernetes、その他 SaaS にも対応してる。
対応言語は Go, Python, Node.js, .NET.Core

仕組みは Terraform とほぼ一緒。

www.pulumi.com

Pulumi の良いところ

Terraform や Ansible といった独自言語(HCL)、もしくはツールに対しての知識は不要で好きな言語で IaC を実現できるのでインフラ専属じゃない人でも気軽にリソースを弄ることができる。(Terraform のバージョン追従みたいので辛い思いしなくていいし、loop の書き方をいちいち調べなくても良い)
また、Pulumi は Go にも対応してるので aws-cdk にはないメリット。

www.pulumi.com

そして、Pulumi の provider は Terraform の provider から生成されてるので tf2pulumi とかいうマイグレツールもある。
github.com

Aurora Cluster を作ってみる

CLI のインストールや、state ファイルの指定などは公式ドキュメントへ。

全貌

func createAuroraSecurityGroup(ctx *pulumi.Context) (*ec2.SecurityGroup, error) {
	name := fmt.Sprintf("aurora-%s-%s", env, prj)

	args := &ec2.SecurityGroupArgs{
		VpcId: pulumi.String("vpc-0ccbc720526afbcff"),
		Ingress: ec2.SecurityGroupIngressArray{
			ec2.SecurityGroupIngressArgs{
				CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")},
				FromPort:   pulumi.Int(3306),
				ToPort:     pulumi.Int(3306),
				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"),
			},
		},
		Name: pulumi.String(name),
		Tags: tags,
	}

	return ec2.NewSecurityGroup(ctx, name, args)
}

func createAuroraSubnetGroup(ctx *pulumi.Context) (*rds.SubnetGroup, error) {
	args := &rds.SubnetGroupArgs{
		Name:      pulumi.String(fmt.Sprintf("%s", prj)),
		SubnetIds: pulumi.StringArray{pulumi.String("subnet-082199a2239986516"), pulumi.String("subnet-00e5e8eda3f4cdef1")},
		Tags:      tags,
	}

	return rds.NewSubnetGroup(ctx, fmt.Sprintf("%s", prj), args)
}

func createAuroraClusterParameterGroup(ctx *pulumi.Context) (*rds.ClusterParameterGroup, error) {
	timeZone := rds.ClusterParameterGroupParameterArgs{
		Name:  pulumi.String("time_zone"),
		Value: pulumi.String("Asia/Tokyo"),
	}

	args := &rds.ClusterParameterGroupArgs{
		Family:     pulumi.String("aurora-mysql5.7"),
		Parameters: rds.ClusterParameterGroupParameterArray{timeZone},
		Tags:       tags,
	}

	return rds.NewClusterParameterGroup(ctx, fmt.Sprintf("%s-%s", env, prj), args)
}

func createAuroraCluster(ctx *pulumi.Context, clusterParameterGroupName, subnetGroupName pulumi.StringPtrInput, securityGroupID pulumi.IDOutput) (*rds.Cluster, error) {
	name := fmt.Sprintf("%s-%s-cluster", env, prj)

	args := &rds.ClusterArgs{
		ApplyImmediately:            pulumi.Bool(true),
		ClusterIdentifier:           pulumi.String(name),
		DbClusterParameterGroupName: clusterParameterGroupName,
		DbSubnetGroupName:           subnetGroupName,
		Engine:                      pulumi.String("aurora-mysql"),
		EngineMode:                  pulumi.String("provisioned"),
		EngineVersion:               pulumi.String("5.7.mysql_aurora.2.07.2"),
		MasterPassword:              pulumi.String(c.Require("aurora_master_password")),
		MasterUsername:              pulumi.String(c.Require("aurora_master_username")),
		Tags:                        tags,
		VpcSecurityGroupIds:         pulumi.StringArray{securityGroupID},
	}

	return rds.NewCluster(ctx, name, args)
}

func createAuroraClusterInstance(ctx *pulumi.Context, cluster *rds.Cluster) error {
	count := c.RequireInt("aurora_instances")

	for i := 0; i < count; i++ {
		name := fmt.Sprintf("%s-%s-instance-%d", env, prj, i)

		args := &rds.ClusterInstanceArgs{
			ApplyImmediately:           pulumi.Bool(true),
			ClusterIdentifier:          cluster.ID(),
			Engine:                     pulumi.String("aurora-mysql"),
			EngineVersion:              pulumi.String("5.7.mysql_aurora.2.07.2"),
			Identifier:                 pulumi.String(name),
			InstanceClass:              pulumi.String(c.Require("aurora_instance_class")),
			PerformanceInsightsEnabled: pulumi.Bool(false), // 2020/05/26 5.7.mysql_aurora.2.07.2 is not supported
			PubliclyAccessible:         pulumi.Bool(false),
			Tags:                       tags,
		}

		_, err := rds.NewClusterInstance(ctx, name, args)
		if err != nil {
			return err
		}
	}

	return nil
}

Tips

使ってみた感じ

configuration variable

外部から注入する変数みたいなもので

$ pulumi config set env stg

env 変数を作って、コードで取得する際は c.Require("env") とするだけ
この変数もスタック(dev 用とか、stg 用とかで分けれる)ごとに分かれる。

configuration variable を取得する際にエラーハンドリングをしなくても落ちてくれる
Diagnostics:
  pulumi:pulumi:Stack (test-pulumi-stg):
    panic: fatal: A failure has occurred: missing required configuration variable 'test-pulumi:project'; run `pulumi config` to set
env = c.Require("env")
if env == "" {
	return fmt.Errorf("'env' variable not defined")
}

こんなことしなくて OK

既存のリソースに対しての変更もちゃんとできる

pulumi で作ったリソースのコードにタグを追加した結果 ↓

Previewing update (stg):
     Type                 Name                Plan       Info
     pulumi:pulumi:Stack  test-pulumi-stg             1 message
 ~   └─ aws:ecs:Cluster   stg-test         update     [diff: ~tags]

Diagnostics:
  pulumi:pulumi:Stack (test-pulumi-stg):
    &{CustomResourceState:{ResourceState:{urn:{OutputState:0xc000207c00} providers:map[] aliases:[] name:stg-test transformations:[]} id:{OutputState:0xc000207b90}} Arn:{OutputState:0xc0002078f0} CapacityProviders:{OutputState:0xc000207960} DefaultCapacityProviderStrategies:{OutputState:0xc0002079d0} Name:{OutputState:0xc000207a40} Settings:{OutputState:0xc000207ab0} Tags:{OutputState:0xc000207b20}}

エラー内容も結構分かりやすい

Previewing update (stg):
     Type                      Name                Plan     Info
     pulumi:pulumi:Stack       test-pulumi-stg
     └─ aws:ec2:SecurityGroup  aurora-stg-test           3 errors

Diagnostics:
  aws:ec2:SecurityGroup (aurora-stg-test):
    error: aws:ec2/securityGroup:SecurityGroup resource 'aurora-stg-test' has a problem: "ingress.0.from_port": required field is not set
    error: aws:ec2/securityGroup:SecurityGroup resource 'aurora-stg-test' has a problem: "ingress.0.to_port": required field is not set
    error: aws:ec2/securityGroup:SecurityGroup resource 'aurora-stg-test' has a problem: "ingress.0.protocol": required field is not set

手で消したリソースを pulumi にも反映させる方法

$ pulumi state delete urn:pulumi:stg::test-pulumi::aws:rds/cluster:Cluster::stg-test-cluster
 warning: This command will edit your stack's state directly. Confirm? Yes
 Multiple resources with the given URN exist, please select the one to edit: "tf-20200525154551533900000001" (Pending Deletion)
Resource deleted successfully