好きな言語でIaC構成管理が記述できる!Pulumiを体験

 こんにちは、エンジニアの石川です。

今回は好きな言語が利用可能なIaC構成管理ツールであるPulumiを利用して、JavaでAWSのEC2インスタンスを立ち上げてnginxを起動するものを構築してみました。

なぜPulumi?

2023年8月中旬ごろ、Terraformのライセンス変更(HashiCorp adopts Business Source License)が話題となった際に私はPulumiについて知りました。

私のようなエンドユーザーにはこのライセンス変更の影響は全くなかったのですが、一部のユーザーはTerraformの代替案を探しており、その代替案の中にPulumiがありました。

Pulumiは

  • 好きなプログラミング言語(TypeScript、JavaScript、Python、Go、C#、Java、YAML)を使用してIaCの構成管理を記述することができる

  • Pulumi AIというソースコード作成をAIがサポートしてくれるという機能がある

と紹介されており、興味をもったため試してみることにしました。

Pulumiの環境構築

Get started with Pulumi & AWSに従いつつ、自分の状況に合わせて設定していきました。


  1. pulumiのインストール

Get started with Pulumi & AWSのとおりにpulumiをインストールしました。


  1. Javaの動作環境を用意

Pulumiに必要なのは、Java11以上とMaven3.6.1以上です。普段はJava8を利用しているため、11に切り替えました。


  1. AWSアカウントの設定

利用するAWSアカウントは既に作成済みのため、AWSの認証情報に関する設定を行いました。

具体的には、AWSの認証情報を含むプロファイル(今回はpulumi-mfaという名前のプロファイル)を作成しました。


  1. Pulumiプロジェクトの作成
    この部分はGet started with Pulumi & AWS に従いながら、Javaを使うためのプロジェクトを作成しました。

ファイル構成は以下の通りです。


\QUICKSTART_PULUMI

  .gitignore

  pom.xml

  Pulumi.dev.yaml

  Pulumi.yaml

└───src

   └───main

       └───java

           └───myproject

                   App.java


  1. Pulumiが参照するプロファイルの指定
    「3. AWSアカウントの設定」で作成した認証情報をPulumiがデプロイ時などに利用するために、プロジェクト内のPulumi.dev.yamlに作成したプロファイル名を追記しました。

config:

  aws:profile: pulumi-mfa

  aws:region: ap-northeast-1

AWSでEC2インスタンスを立ち上げてnginxを起動するJavaスクリプトを作成

今回の目標は、AWSでEC2インスタンスを立ち上げてnginxを起動するJavaスクリプトの作成です。

モジュール化などは含んでいません。


VPCを作成し、パブリックサブネット1つだけを作成しています。


var vpc = new Vpc("vpc-pulumi", com.pulumi.awsx.ec2.VpcArgs.builder()

        .cidrBlock("172.24.0.0/16")

        .subnetSpecs(SubnetSpecArgs.builder()

        .type(SubnetType.Public) //publicサブネット1つのみ

        .build())

        .natGateways(NatGatewayConfigurationArgs.builder()

        .strategy(NatGatewayStrategy.None) //natは今回不要

        .build())

        .numberOfAvailabilityZones(1) //アベイラビリティゾーンを1つにする(デフォルトは3)

        .build());


awsxを利用するためのdependencyをpom.xmlに追記します。


    <dependencies>

        <dependency>

...

        </dependency>

        <dependency>

            <groupId>com.pulumi</groupId>

            <artifactId>awsx</artifactId>

            <version>1.0.5</version>

        </dependency>

    </dependencies>


  • セキュリティグループ

ssh用の22番ポートとhttp用の80番ポートを解放しています。


        var sg = new SecurityGroup("ec2sg-pulumi", SecurityGroupArgs.builder()        

            .vpcId(vpc.vpcId())

            .ingress(SecurityGroupIngressArgs.builder()

                .description("allow http access") //httpアクセス用

                .fromPort(80)

                .toPort(80)

                .protocol("tcp")

                .cidrBlocks("0.0.0.0/0")

                .build(),

                SecurityGroupIngressArgs.builder()

                 .description("allow ssh access") //sshで内部を確認する用

                .fromPort(22)

                .toPort(22)

                .protocol("tcp")

                .cidrBlocks("0.0.0.0/0")

                .build()

                )

            .egress(SecurityGroupEgressArgs.builder()

                .fromPort(0)

                .toPort(0)

                .protocol("-1")

                .cidrBlocks("0.0.0.0/0")

                .build())

            .tags(Map.of("env", "dev"))

            .build());



  • sshアクセス用のキー

コマンド「$ ssh-keygen -t rsa -b 2048 -f 」で作成したパブリックキーの中身をpublicKey部分に指定しました。


var ec2key = new KeyPair("sshkey-pulumi", KeyPairArgs.builder()        

        .publicKey("{自分で作成したsshキーのpublicキー}")

        .keyName("ishikawa-pulumi-key")

        .build());


  • EC2

一般的なEC2の設定と変わる部分は特にありません。

userDataにはnginxのインストールと起動を行うためのスクリプトを記述しています。

subnetIdやvpcSecurityGroupIdsではapplyValueというメソッドを利用しています。

Inputs & Outputs | Pulumi Conceptsで説明があるのですが、自分なりにまとめると「出力の型(Output型)で囲まれた値を取得する」ことができるメソッドです。

vpc.publicSubnetIds()の値の型は Output<List<String>> です。
applyValueメソッドを利用することで、List<String>として利用することができるようになります。 


        // nginx起動用シェルスクリプト 

        String userData = "#!/bin/bash\n"+

        "sudo yum update -y # システムを更新する\n"+

        "sudo yum install nginx -y # Nginxをインストールする\n"+

        "sudo systemctl start nginx # Nginxを起動する\n"+

        "sudo systemctl enable nginx # Nginxを自動起動に設定する";


        var ec2 = new Instance("ec2-pulumi", InstanceArgs.builder()

            .subnetId(vpc.publicSubnetIds().applyValue(ids -> ids.get(0)))     

            .vpcSecurityGroupIds(sg.id().applyValue(List::of))

            .ami("ami-0310b105770df9334")

            .instanceType("t2.micro")

            .tags(Map.of("env", "dev"))

            .keyName(ec2key.keyName())

            .userData(userData)

            .build());


最終的にApp.javaは以下のようになりました。


package myproject;


import com.pulumi.Pulumi;

import com.pulumi.Context;

import com.pulumi.aws.ec2.Instance;

import com.pulumi.aws.ec2.InstanceArgs;

import com.pulumi.aws.ec2.SecurityGroup;

import com.pulumi.aws.ec2.SecurityGroupArgs;

import com.pulumi.aws.ec2.inputs.SecurityGroupEgressArgs;

import com.pulumi.aws.ec2.inputs.SecurityGroupIngressArgs;

import com.pulumi.aws.ec2.KeyPair;

import com.pulumi.aws.ec2.KeyPairArgs;

import com.pulumi.awsx.ec2.Vpc;

import com.pulumi.awsx.ec2.enums.NatGatewayStrategy;

import com.pulumi.awsx.ec2.enums.SubnetType;

import com.pulumi.awsx.ec2.inputs.NatGatewayConfigurationArgs;

import com.pulumi.awsx.ec2.inputs.SubnetSpecArgs;


import java.util.List;

import java.util.Map;


public class App {

    public static void main(String[] args) {

        Pulumi.run(App::stack);

    }


    public static void stack(Context ctx) {


        var vpc = new Vpc("vpc-pulumi", com.pulumi.awsx.ec2.VpcArgs.builder()

        .cidrBlock("172.24.0.0/16")

        .subnetSpecs(SubnetSpecArgs.builder()

        .type(SubnetType.Public) //publicサブネット1つのみ

        .build())

        .natGateways(NatGatewayConfigurationArgs.builder()

        .strategy(NatGatewayStrategy.None) //natは今回不要

        .build())

        .numberOfAvailabilityZones(1) //アベイラビリティゾーンを1つにする(デフォルトは3)

        .build());

            

        var sg = new SecurityGroup("ec2sg-pulumi", SecurityGroupArgs.builder()        

            .vpcId(vpc.vpcId())

            .ingress(SecurityGroupIngressArgs.builder()

                .description("allow http access") //httpアクセス用

                .fromPort(80)

                .toPort(80)

                .protocol("tcp")

                .cidrBlocks("0.0.0.0/0")

                .build(),

                SecurityGroupIngressArgs.builder()

                 .description("allow ssh access") //sshで内部を確認する用

                .fromPort(22)

                .toPort(22)

                .protocol("tcp")

                .cidrBlocks("0.0.0.0/0")

                .build()

                )

            .egress(SecurityGroupEgressArgs.builder()

                .fromPort(0)

                .toPort(0)

                .protocol("-1")

                .cidrBlocks("0.0.0.0/0")

                .build())

            .tags(Map.of("env", "dev"))

            .build());


        var ec2key = new KeyPair("sshkey-pulumi", KeyPairArgs.builder()        

        .publicKey("{自分で作成したsshキーのpublicキー}")

        .keyName("ishikawa-pulumi-key")

        .build());


        // nginx起動用シェルスクリプト 

        String userData = "#!/bin/bash\n"+

        "sudo yum update -y # システムを更新する\n"+

        "sudo yum install nginx -y # Nginxをインストールする\n"+

        "sudo systemctl start nginx # Nginxを起動する\n"+

        "sudo systemctl enable nginx # Nginxを自動起動に設定する";


        var ec2 = new Instance("ec2-pulumi", InstanceArgs.builder()

            .subnetId(vpc.publicSubnetIds().applyValue(ids -> ids.get(0)))     

            .vpcSecurityGroupIds(sg.id().applyValue(List::of))

            .ami("ami-0310b105770df9334")

            .instanceType("t2.micro")

            .tags(Map.of("env", "dev"))

            .keyName(ec2key.keyName())

            .userData(userData)

            .build());   

    }

}

Pulumiでデプロイ

デプロイする前に、コマンド「$ pulumi up 」でどのようなリソースが作られるのかを確認します。


$ pulumi up     

Previewing update (dev)


View in Browser (Ctrl+O): https://app.pulumi.com/n-ishikawa/quickstart_pulumi/dev/previews/73873587-f26d-4d81-9ddb-6119d5538555


     Type                                          Name                   Plan

 +   pulumi:pulumi:Stack                           quickstart_pulumi-dev  create

 +   ├─ awsx:ec2:Vpc                               vpc-pulumi             create

 +   │  └─ aws:ec2:Vpc                             vpc-pulumi             create

 +   │     ├─ aws:ec2:Subnet                       vpc-pulumi-public-1    create

 +   │     │  └─ aws:ec2:RouteTable                vpc-pulumi-public-1    create

 +   │     │     ├─ aws:ec2:RouteTableAssociation  vpc-pulumi-public-1    create

 +   │     │     └─ aws:ec2:Route                  vpc-pulumi-public-1    create

 +   │     └─ aws:ec2:InternetGateway              vpc-pulumi             create

 +   ├─ aws:ec2:KeyPair                            sshkey-pulumi          create

 +   ├─ aws:ec2:SecurityGroup                      ec2sg-pulumi           create

 +   └─ aws:ec2:Instance                           ec2-pulumi             create


Resources:

    + 11 to create



Do you want to perform this update?

> yes

  no

  details




最後の選択肢でyesを選択すればデプロイをし、noを選択すればデプロイをキャンセル、detailsを選択すればリソースのさらに詳しい情報を取得することができます。

デプロイ後にEC2のパブリックIPアドレスにアクセスし以下のような画面が表示されれば成功です。

感想

以下は今回Pulumiを試してみたときの感想です。


  1. 例が少ない

Terraformと比較すると日本語の資料や例の数が少なく感じました。
こちらはドキュメントを読む、ソースコードを読むことで対処可能である範囲ですが、Pulumiが初めてのIaC構成管理ツールであるという人にはTerraformと比較すると難易度が高いです。


  1. PulumiAIを上手く活用することができなかった

Pulumi AIに依頼をするとソースコードを書いてくれるのですが、そこにはもちろん誤りも含まれています。今回は誤りの部分の修正を行うためにドキュメントを読みこんで自分でコードを修正して...と進めていきました。

本来はPulumi AIとやり取りをしながらより良いコードを作成していくと思うのですが、私はあまり頼ることができませんでした。
英語でのやり取りが苦手ということも頼ることができなかった一因であるため、いつか日本語対応してくれる日が来ればおんぶに抱っこ状態になるかもしれません。


  1. Terminalで日本語が出力された時に必ず文字化けする

Terminalのエンコードの言語を色々なものに変えて試しましたが、どうしても日本語の文字化けは直すことができませんでした。
エラーが日本語で出力された場合は、それ以外の部分の情報から自分でたどり着くしかないのでしょうか...

おわりに

今回はPulumiを利用して、JavaでAWSのEC2インスタンスを立ち上げてnginxを起動するものを構築してみました。

次回はOpenTofu(豆腐)などの話題に関する記事を書きたいなと思っています。
ありがとうございました!

今回利用したAPIドキュメントなどのリンク