【Pulumi】Pulumi AIが生成したコードでEC2インスタンスを構築したい～第2章：Python編～

## はじめに

前回の記事（[【Pulumi】Pulumi AIが生成したコードでEC2インスタンスを構築したい～第1章：YAML編～][1]）では、  
Pulumi AIを用いてYAMLでコードを自動生成しました。  
確かに、YAMLでコードを書けるのはシンプルで使いやすいですが、プログラミング言語のメリット（分岐や繰り返し処理など）  
を活用できないデメリットがあるので、今回はPythonで書いてみます。

## TL;DR

- Pulumiはプログラミング言語でインフラを構築可能なプロビジョニングツールであり、  
慣れ親しんだ開発言語(Python/TypeScript/Goなど)やマークアップ言語(YAMLなど)で記述可能
- YAMLで書けるが、プログラミング言語のメリットが活用できない
- 細かい処理をするなら、Pythonなどのプログラミング言語で書いた方がよい

## 前提条件

PulumiでAWSを操作するには以下が必要です

- Pulumiのアカウントを所持していること
  - https://www.pulumi.com/
- AWSにてIAMユーザーのアクセスキーが作成してあること

## 実行環境

- Amazon マシンイメージ（AMI）
  - Amazon Linux 2023 AMI
- Pulumi
  - v3.112.0

## 詳細

### 1. 事前準備

- 前回の記事（[【Pulumi】Pulumi AIが生成したコードでEC2インスタンスを構築したい～第1章：YAML編～][1]）における以下の箇所を実施する
  1. アクセストークンの作成
  2. EC2インスタンスにPulumiをインストール

### 2. Pulumi Projectの作成

1. Pulumiにログインする。ここで、事前準備で作成したトークンを入力する

    ```console
    $ pulumi login
    ```

2. 作業用ディレクトリを作成する

    ```console
    $ mkdir pulumi-python && cd pulumi-python
    ```

3. 作業用Projectを作成する
    - 今回はPythonでAWSリソースの定義ファイルを書くので`aws-python`としています
    - Pythonでは仮想環境（venv）が作成されます

    ```console
    $ pulumi new aws-python
    ```

    - 色々と聞かれますが、特に何も入力せずに`Enter`でも問題ありません(ここではRegionを`us-west-2`に変更しました)

    ```console
    This command will walk you through creating a new Pulumi project.

    Enter a value or leave blank to accept the (default), and press <ENTER>.
    Press ^C at any time to quit.

    project name (pulumi-python):  
    project description (A minimal AWS Python Pulumi program):  
    Created project 'pulumi-python'

    Please enter your desired stack name.
    To create a stack in an organization, use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).
    stack name (dev):  
    Created stack 'dev'

    aws:region: The AWS region to deploy into (us-east-1): us-west-2
    ～以下、省略～
    ```

4. 以下のファイルが作成されたことを確認する
    - Pulumi.dev.yaml: 環境変数などを記述するファイル
    - Pulumi.yaml: Pulumi プロジェクトの設定ファイル
    - \_\_main__.py: AWSリソースの定義ファイル
    - requirements.txt: Pythonパッケージの一覧
    - venv: Pythonの仮想環境

    ```console
    $ ls
    Pulumi.dev.yaml  Pulumi.yaml  __main__.py  requirements.txt  venv
    ```

    なお、 `Pulumi.dev.yaml`は以下のようになっているので、そのまま使います

    ```yaml:Pulumi.dev.yaml
    config:
      aws:region: us-west-2
    ```

### 3. Pulumi AIでAWSリソースの定義ファイル作成

1. Pulumiのホーム画面右上の[Pulumi AI]を選択する
    - https://app.pulumi.com/
2. [Python]を選択し、処理内容を記述して[Update program]を選択する。処理内容は可能な限り具体的に書くとよい
    - 記述した内容
      - AWSでインターネット経由で接続可能なEC2インスタインス、VPC、サブネット、セキュリティグループを作成する
    - 出力結果
      - 結果の見やすさ向上のために、フォーマッター（Black）の処理をしています

      ```python
      import pulumi
      import pulumi_aws as aws

      # Create a VPC
      vpc = aws.ec2.Vpc(
          "my_vpc",
          cidr_block="10.0.0.0/16",
          enable_dns_support=True,
          enable_dns_hostnames=True,
      )

      # Create an Internet Gateway
      internet_gateway = aws.ec2.InternetGateway("my_gateway", vpc_id=vpc.id)

      # Create a Route Table
      route_table = aws.ec2.RouteTable(
          "my_route_table",
          vpc_id=vpc.id,
          routes=[
              aws.ec2.RouteTableRouteArgs(
                  cidr_block="0.0.0.0/0",
                  gateway_id=internet_gateway.id,
              )
          ],
      )

      # Create a Subnet
      subnet = aws.ec2.Subnet(
          "my_subnet", vpc_id=vpc.id, cidr_block="10.0.1.0/24", map_public_ip_on_launch=True
      )  # Enable auto-assign public IP on launch

      # Associate Subnet with Route Table
      route_table_association = aws.ec2.RouteTableAssociation(
          "my_route_table_association", route_table_id=route_table.id, subnet_id=subnet.id
      )

      # Create a Security Group that allows SSH and HTTP traffic
      security_group = aws.ec2.SecurityGroup(
          "my_security_group",
          description="Allow SSH and HTTP",
          vpc_id=vpc.id,
          ingress=[
              aws.ec2.SecurityGroupIngressArgs(
                  protocol="tcp",
                  from_port=22,
                  to_port=22,
                  cidr_blocks=["0.0.0.0/0"],
              ),
              aws.ec2.SecurityGroupIngressArgs(
                  protocol="tcp",
                  from_port=80,
                  to_port=80,
                  cidr_blocks=["0.0.0.0/0"],
              ),
          ],
          egress=[
              aws.ec2.SecurityGroupEgressArgs(
                  protocol="-1",  # allows all outbound traffic
                  from_port=0,
                  to_port=0,
                  cidr_blocks=["0.0.0.0/0"],
              ),
          ],
      )

      # Create an EC2 instance
      instance = aws.ec2.Instance(
          "my_instance",
          ami="ami-0c55b159cbfafe1f0",  # example AMI-ID, please replace it with a valid one
          instance_type="t2.micro",
          vpc_security_group_ids=[security_group.id],
          subnet_id=subnet.id,
          associate_public_ip_address=True,
      )

      # Export the public IP of the EC2 instance
      pulumi.export("instance_public_ip", instance.public_ip)

      ```

3. Pulumi AIの結果をPulumiのドキュメント([Pulumi > AWS > API Docs][2])などをもとに修正し、`__main__.py`に記入する
    - 細かい修正をしたい場合は、別途Pulumi AIで作成した定義ファイルを参考にしてもよい

      ```python
      import pulumi
      import pulumi_aws as aws

      # Create a Config instance to access the Pulumi configuration
      config = pulumi.Config("aws")
      # Create Availability Zone from the Pulumi configuration file under the 'aws' namespace
      availability_zone = config.require("region") + "a"

      # Create a VPC
      vpc = aws.ec2.Vpc(
          "my_vpc",
          cidr_block="10.0.0.0/16",
          # enable_dns_support=True,
          # enable_dns_hostnames=True,
          tags={"Name": "my_vpc"},
      )

      # Create an Internet Gateway
      internet_gateway = aws.ec2.InternetGateway(
          "my_gateway", vpc_id=vpc.id, tags={"Name": "my_gateway"}
      )

      # Create a Route Table
      route_table = aws.ec2.RouteTable(
          "my_route_table",
          vpc_id=vpc.id,
          # routes=[
          #     aws.ec2.RouteTableRouteArgs(
          #         cidr_block="0.0.0.0/0",
          #         gateway_id=internet_gateway.id,
          #     )
          # ],
          tags={"Name": "my_route_table"},
      )

      # Create a Route in the Route Table pointing to the Internet Gateway
      route = aws.ec2.Route(
          "my_route",
          destination_cidr_block="0.0.0.0/0",
          gateway_id=internet_gateway.id,
          route_table_id=route_table.id,
      )

      # Create a Subnet
      subnet = aws.ec2.Subnet(
          "my_subnet",
          vpc_id=vpc.id,
          cidr_block="10.0.1.0/24",
          map_public_ip_on_launch=True,  # Enable auto-assign public IP on launch
          availability_zone=availability_zone,
          tags={"Name": "my_subnet"},
      )

      # Associate Subnet with Route Table
      route_table_association = aws.ec2.RouteTableAssociation(
          "my_route_table_association", route_table_id=route_table.id, subnet_id=subnet.id
      )

      # Create a Security Group that allows SSH and HTTP traffic
      security_group = aws.ec2.SecurityGroup(
          "my_security_group",
          description="Allow SSH and HTTP",
          vpc_id=vpc.id,
          ingress=[
              aws.ec2.SecurityGroupIngressArgs(
                  protocol="tcp",
                  from_port=22,
                  to_port=22,
                  cidr_blocks=["0.0.0.0/0"],
              ),
              aws.ec2.SecurityGroupIngressArgs(
                  protocol="tcp",
                  from_port=80,
                  to_port=80,
                  cidr_blocks=["0.0.0.0/0"],
              ),
          ],
          egress=[
              aws.ec2.SecurityGroupEgressArgs(
                  protocol="-1",  # allows all outbound traffic
                  from_port=0,
                  to_port=0,
                  cidr_blocks=["0.0.0.0/0"],
              ),
          ],
          tags={"Name": "my_security_group"},
      )


      # Create an EC2 instance
      instance = aws.ec2.Instance(
          "my_instance",
          ami="ami-0c55b159cbfafe1f0",  # example AMI-ID, please replace it with a valid one
          instance_type="t2.micro",
          vpc_security_group_ids=[security_group.id],
          subnet_id=subnet.id,
          associate_public_ip_address=True,
          key_name="my_key",
          availability_zone=availability_zone,
          tags={"Name": "my_instance"},
      )

      # Export the EC2 instance information
      pulumi.export("vpc_id", vpc.id)
      pulumi.export("subnet_id", subnet.id)
      pulumi.export("internet_gateway_id", internet_gateway.id)
      pulumi.export("route_table_id", route_table.id)
      pulumi.export("route_table_association_id", route_table_association.id)
      pulumi.export("route_id", route.id)
      pulumi.export("security_group_id", security_group.id)
      pulumi.export("instance_id", instance.id)
      pulumi.export("instance_public_ip", instance.public_ip)

      ```

補足として、`Pulumi.dev.yaml`にある`aws:region`の値は、
`pulumi.Config`を使用して取得します（詳細は、[Pulumi > Configuration][3]を参照）

### 4. Pulumiによるリソースの作成

1. IAMユーザーのアクセスキーを登録する

    ```console
    $ export AWS_ACCESS_KEY_ID="<YOUR_ACCESS_KEY_ID>"
    $ export AWS_SECRET_ACCESS_KEY="<YOUR_SECRET_ACCESS_KEY>"
    ```

2. リソースを作成する

    ```console
    $ pulumi up
    ```

3. リソースが作成されていることを確認する(以下はログの一部)

    ```console
        Type                              Name                        Status              
    +   pulumi:pulumi:Stack               pulumi-python-dev           created (69s)       
    +   ├─ aws:ec2:Vpc                    my_vpc                      created (2s)        
    +   ├─ aws:ec2:SecurityGroup          my_security_group           created (3s)        
    +   ├─ aws:ec2:RouteTable             my_route_table              created (1s)        
    +   ├─ aws:ec2:InternetGateway        my_gateway                  created (1s)        
    +   ├─ aws:ec2:Subnet                 my_subnet                   created (12s)       
    +   ├─ aws:ec2:Route                  my_route                    created (1s)        
    +   ├─ aws:ec2:Instance               my_instance                 created (43s)       
    +   └─ aws:ec2:RouteTableAssociation  my_route_table_association  created (0.77s) 
    ```

### 5. Pulumiマイページからの結果確認

- 前回の記事（[【Pulumi】Pulumi AIが生成したコードでEC2インスタンスを構築したい～第1章：YAML編～][1]）の  
以下の箇所を実施する
  6. Pulumiマイページからの結果確認

### 6. Pulumiによるリソースの削除

1. リソースを削除する

    ```console
    $ pulumi down
    ```

2. リソースが削除されていることを確認する(以下はログの一部)

    ```console
        Type                              Name                        Status              
    -   pulumi:pulumi:Stack               pulumi-python-dev           deleted (0.43s)     
    -   ├─ aws:ec2:Route                  my_route                    deleted (1s)        
    -   ├─ aws:ec2:Instance               my_instance                 deleted (41s)       
    -   ├─ aws:ec2:RouteTableAssociation  my_route_table_association  deleted (1s)        
    -   ├─ aws:ec2:SecurityGroup          my_security_group           deleted (1s)        
    -   ├─ aws:ec2:RouteTable             my_route_table              deleted (1s)        
    -   ├─ aws:ec2:Subnet                 my_subnet                   deleted (1s)        
    -   ├─ aws:ec2:InternetGateway        my_gateway                  deleted (1s)        
    -   └─ aws:ec2:Vpc                    my_vpc                      deleted (0.75s)     
    ```

## さいごに

- Pulumiで細かい処理をするならPythonなどのプログラミング言語で書いた方がよいですが、  
YAMLと比較するとどうしても難しく感じてしまいます
- YAMLで書くのか否かは、処理内容の複雑さなどから判断した方がよさそうです

## 参考URL

- [Pulumi > AWS > API Docs][2]
  - PulumiでAWSを操作するための記述方法
- [Pulumi > Configuration][3]
  - Pulumiで環境ごとに異なる変数を使用する方法

## 関連記事

- [【Pulumi】Pulumi AIが生成したコードでEC2インスタンスを構築したい～第1章：YAML編～][1]

---

[1]: https://qiita.com/masa2223/items/ae09ef1af1f1503a950e
[2]: https://www.pulumi.com/registry/packages/aws/api-docs/
[3]: https://www.pulumi.com/docs/concepts/config/#code