🐱

pulumiを使ってAWS環境でWEBアプリを構築してみる

2024/10/25に公開

株式会社バニッシュ・スタンダードでエンジニアをしております、chanponです。今回は、個人的にPulumiを使ってAWS上にWEBアプリを構築してみたので、その体験を共有します。

Pulumi

https://www.pulumi.com/

IaC(Infrastructure as Code)のツールとしては、TerraformやAWS CDKなどが一般的ですが、Pulumiもその選択肢の一つです。Pulumiの特徴の一つとして、普段使っているプログラミング言語でインフラを定義できることです。サポートしている言語には、以下のようなものがあります。

  • Node.js (JavaScript, TypeScript)
  • Python
  • Go
  • .NET
  • Java

私は普段からGoを使っているので、今回はGoを使ってPulumiでインフラ構築をしてみました。

基本的な使い方について

Pulumiのインストール方法や、初めてのセットアップについては公式ドキュメントを見ながら進めるのがスムーズです。基本的なチュートリアルも用意されているので、まずは公式を参考にすると始めやすいと思います。

AWS上でのWEBアプリ構築

今回は、AWSの新しいアカウント上にWEBアプリを構築しました。
以下のような構成を作成しました。

  • スケーラブルなネットワークを構築するために、VPCと2つのサブネットを作成しました。
  • ALBを利用して、トラフィックを分散しつつ、HTTPアクセスを処理できるようにしました。
  • アプリケーションをFargateでデプロイし、ALBと連携させました。

以下は、Pulumiで使用したGoのコードです。基本的なVPCの作成から始め、ALBやECSの設定まで、すべてをコードで定義しています。

package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/alb"
	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2"
	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ecs"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		// VPCの作成
		vpc, err := ec2.NewVpc(ctx, "my-vpc", &ec2.VpcArgs{
			CidrBlock:          pulumi.String("10.0.0.0/16"),
			EnableDnsSupport:   pulumi.Bool(true),
			EnableDnsHostnames: pulumi.Bool(true),
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-vpc",
			}),
		})
		if err != nil {
			return err
		}

		// サブネットの作成
		subnet1a, err := ec2.NewSubnet(ctx, "my-subnet1a", &ec2.SubnetArgs{
			VpcId:            vpc.ID(),
			CidrBlock:        pulumi.String("10.0.1.0/24"),
			AvailabilityZone: pulumi.String("ap-northeast-1a"),
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-subnet1a",
			}),
		})
		if err != nil {
			return err
		}

		subnet1c, err := ec2.NewSubnet(ctx, "my-subnet1c", &ec2.SubnetArgs{
			VpcId:            vpc.ID(),
			CidrBlock:        pulumi.String("10.0.2.0/24"),
			AvailabilityZone: pulumi.String("ap-northeast-1c"),
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-subnet1c",
			}),
		})
		if err != nil {
			return err
		}

		// インターネットゲートウェイの作成
		igw, err := ec2.NewInternetGateway(ctx, "my-igw", &ec2.InternetGatewayArgs{
			VpcId: vpc.ID(),
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-igw",
			}),
		})
		if err != nil {
			return err
		}

		// ルートテーブルの作成とigwへの関連付け
		routeTable, err := ec2.NewRouteTable(ctx, "my-route-table", &ec2.RouteTableArgs{
			VpcId: vpc.ID(),
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-route-table",
			}),
		})
		if err != nil {
			return err
		}

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

		_, err = ec2.NewRouteTableAssociation(ctx, "subnet1a-association", &ec2.RouteTableAssociationArgs{
			SubnetId:     subnet1a.ID(),
			RouteTableId: routeTable.ID(),
		})
		if err != nil {
			return err
		}

		_, err = ec2.NewRouteTableAssociation(ctx, "subnet1c-association", &ec2.RouteTableAssociationArgs{
			SubnetId:     subnet1c.ID(),
			RouteTableId: routeTable.ID(),
		})
		if err != nil {
			return err
		}

		// ALB用セキュリティグループの作成
		albSec, err := ec2.NewSecurityGroup(ctx, "my-alb-sec", &ec2.SecurityGroupArgs{
			VpcId: vpc.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"),
					},
				},
			},
			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"),
					},
				},
			},
		})
		if err != nil {
			return err
		}

		// ECS用セキュリティグループの作成
		ecsSec, err := ec2.NewSecurityGroup(ctx, "my-ecs-sec", &ec2.SecurityGroupArgs{
			VpcId: vpc.ID(),
			Egress: ec2.SecurityGroupEgressArray{
				&ec2.SecurityGroupEgressArgs{
					FromPort: pulumi.Int(443),
					ToPort:   pulumi.Int(443),
					Protocol: pulumi.String("tcp"),
					CidrBlocks: pulumi.StringArray{
						pulumi.String("0.0.0.0/0"),
					},
				},
			},
			Ingress: ec2.SecurityGroupIngressArray{
				&ec2.SecurityGroupIngressArgs{
					FromPort: pulumi.Int(80),
					ToPort:   pulumi.Int(80),
					Protocol: pulumi.String("tcp"),
					SecurityGroups: pulumi.StringArray{
						albSec.ID(),
					},
				},
			},
		})
		if err != nil {
			return err
		}

		// ECSクラスターの作成
		cluster, err := ecs.NewCluster(ctx, "my-cluster", &ecs.ClusterArgs{
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-cluster",
			}),
		})
		if err != nil {
			return err
		}

		// ALBの作成
		lb, err := alb.NewLoadBalancer(ctx, "my-alb", &alb.LoadBalancerArgs{
			Subnets: pulumi.StringArray{
				subnet1a.ID(),
				subnet1c.ID(),
			},
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-alb",
			}),
			SecurityGroups: pulumi.StringArray{
				albSec.ID(),
			},
		})
		if err != nil {
			return err
		}

		// ターゲットグループの作成
		targetGroup, err := alb.NewTargetGroup(ctx, "my-target-group", &alb.TargetGroupArgs{
			Port:       pulumi.Int(80),
			Protocol:   pulumi.String("HTTP"),
			VpcId:      vpc.ID(),
			TargetType: pulumi.String("ip"),
			HealthCheck: &alb.TargetGroupHealthCheckArgs{
				Enabled:            pulumi.Bool(true),
				Path:               pulumi.String("/"),
				Interval:           pulumi.Int(30),
				Timeout:            pulumi.Int(5),
				UnhealthyThreshold: pulumi.Int(2),
			},
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-tg",
			}),
		})
		if err != nil {
			return err
		}

		// リスナーの作成
		_, err = alb.NewListener(ctx, "my-listener", &alb.ListenerArgs{
			LoadBalancerArn: lb.Arn,
			Port:            pulumi.Int(80),
			DefaultActions: alb.ListenerDefaultActionArray{
				&alb.ListenerDefaultActionArgs{
					Type:           pulumi.String("forward"),
					TargetGroupArn: targetGroup.Arn,
				},
			},
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-listener",
			}),
		})
		if err != nil {
			return err
		}

		// ECSタスク定義の作成
		taskDef, err := ecs.NewTaskDefinition(ctx, "my-task-def", &ecs.TaskDefinitionArgs{
			Family:      pulumi.String("my-task-family"),
			NetworkMode: pulumi.String("awsvpc"),
			RequiresCompatibilities: pulumi.StringArray{
				pulumi.String("FARGATE"),
			},
			Cpu:    pulumi.String("256"),
			Memory: pulumi.String("512"),
			ContainerDefinitions: pulumi.String(`[{
				 "name": "my-app",
				 "image": "nginx",
				 "essential": true,
				 "portMappings": [{
					  "containerPort": 80,
					  "hostPort": 80
				 }]
			}]`),
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-task-def",
			}),
		})
		if err != nil {
			return err
		}

		// ECSサービスの作成
		_, err = ecs.NewService(ctx, "my-ecs-service", &ecs.ServiceArgs{
			Cluster:        cluster.ID(),
			DesiredCount:   pulumi.Int(2),
			LaunchType:     pulumi.String("FARGATE"),
			TaskDefinition: taskDef.Arn,
			NetworkConfiguration: &ecs.ServiceNetworkConfigurationArgs{
				AssignPublicIp: pulumi.Bool(true),
				Subnets: pulumi.StringArray{
					subnet1a.ID(),
					subnet1c.ID(),
				},
				SecurityGroups: pulumi.StringArray{
					ecsSec.ID(),
				},
			},
			LoadBalancers: ecs.ServiceLoadBalancerArray{
				&ecs.ServiceLoadBalancerArgs{
					TargetGroupArn: targetGroup.Arn,
					ContainerName:  pulumi.String("my-app"),
					ContainerPort:  pulumi.Int(80),
				},
			},
			Tags: pulumi.ToStringMap(map[string]string{
				"Name": "my-ecs-service",
			}),
		})
		if err != nil {
			return err
		}

		// エクスポート
		ctx.Export("vpcId", vpc.ID())
		ctx.Export("loadBalancerDnsName", lb.DnsName)

		return nil
	})
}

pulumi upを実行して、あっという間にAWS上にWEBアプリを立ち上げることができました。

構築が完了したら、ALBのURLにアクセスすることで、無事にデプロイされたアプリケーションが表示されました。

作業が終わったら、pulumi destroyコマンドでリソースを簡単に削除できます。

まとめ

Pulumiを使ってWEBアプリを構築してみました。
やはり普段使っている言語で構築できるのはコードも読みやすくて結構な利点だと思いました。
AWS CDKも同じようにプログラミング言語で構築できますが、PulumiはAWS以外にAzureやGoogle Cloud等にも対応しているので、別のクラウドへ移行する場合も対応しやすそうです。
stateもPulumi側で自動的に持ってもらえるので構築することに集中できて体験も良かったです。
Pulumiに興味を持った方は、公式サイトやドキュメントを参考に、ぜひ自分でも試してみてください。

Discussion