【Pulumi×Ansible】PulumiでAnsible Navigatorを使用可能なEC2インスタンスを構築したい

## はじめに

過去の記事（[【Pulumi】Pulumi AIが生成したコードでEC2インスタンスを構築したい～第1章：YAML編～][1]）では、  
Pulumi AIで生成したYAMLをもとにしてEC2インスタンスを構築しました。  
今回はEC2インスタンス構築後にスクリプトを実行して、Ansible Navigatorを使用可能な環境を構築します。

## TL;DR

- PulumiでEC2インスタンス構築後にスクリプトを実行するには、[aws:ec2:Instance][3]の`userData`の箇所にスクリプトを記載する
- スクリプトは`root`ユーザーで実行されるので、特定のユーザーでの処理がある場合は`su`コマンドでユーザーを切り替える

## 前提条件

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をインストール
  3. Pulumi Projectの作成

### 2. AWSリソースの定義ファイル作成

1. AWSリソースの定義ファイル（`Pulumi.yaml`）を以下のように記載する
    - [【Pulumi】Pulumi AIが生成したコードでEC2インスタンスを構築したい～第1章：YAML編～][1]で紹介した定義ファイルと大差ありません
    - [aws:ec2:Instance][3]に`userData`が追加されています

    ```yaml
    ---
    name: quickstart
    runtime: yaml
    description: A program to create an AWS EC2 instance within a VPC with a subnet and security group, accessible over the internet.
    variables:
      amiId: "<amiId>"    # ex) ami-xxxxxxxxxxxxxxxxx
      keyName: "<keyName>"
      instanceType: "<instanceType>"    # ex) t2.micro
    resources:
      # Create a new VPC
      myVpc:
        type: aws:ec2:Vpc
        properties:
          cidrBlock: "10.0.0.0/16"
          tags:
            Name: "my-vpc"

      # Create an Internet Gateway
      myInternetGateway:
        type: aws:ec2:InternetGateway
        properties:
          vpcId: ${myVpc.id}
          tags:
            Name: "my-internet-gateway"

      # Create a new Subnet within the VPC
      mySubnet:
        type: aws:ec2:Subnet
        properties:
          vpcId: ${myVpc.id}
          cidrBlock: "10.0.1.0/24"
          mapPublicIpOnLaunch: true
          availabilityZone: ${aws:region}a
          tags:
            Name: "my-subnet"

      # Create a new Route Table
      myRouteTable:
        type: aws:ec2:RouteTable
        properties:
          vpcId: ${myVpc.id}
          tags:
            Name: "my-route-table"

      # Create a route that directs 0.0.0.0/0 to the Internet Gateway
      myRoute:
        type: aws:ec2:Route
        properties:
          destinationCidrBlock: "0.0.0.0/0"
          gatewayId: ${myInternetGateway.id}
          routeTableId: ${myRouteTable.id}

      # Associate the Route Table with the Subnet
      myRouteTableAssociation:
        type: aws:ec2:RouteTableAssociation
        properties:
          subnetId: ${mySubnet.id}
          routeTableId: ${myRouteTable.id}

      # Create a Security Group within the VPC
      mySecurityGroup:
        type: aws:ec2:SecurityGroup
        properties:
          vpcId: ${myVpc.id}
          description: "Allow SSH inbound traffic"
          tags:
            Name: "my-security-group"
          ingress:
            - protocol: "tcp"
              fromPort: 22
              toPort: 22
              cidrBlocks: ["0.0.0.0/0"]
          egress:
            - protocol: "-1" # Allow all outbound traffic
              fromPort: 0
              toPort: 0
              cidrBlocks: ["0.0.0.0/0"]

      # Launch an EC2 instance within the subnet
      myInstance:
        type: aws:ec2:Instance
        properties:
          ami: ${amiId}
          instanceType: ${instanceType}
          subnetId: ${mySubnet.id}
          securityGroups: ["${mySecurityGroup.id}"]
          availabilityZone: ${aws:region}a
          keyName: ${keyName}
          userData: |
            #!/bin/bash

            dnf install -y podman python3-pip
            su ec2-user << EOF
            podman pull ghcr.io/ansible-community/community-ee-base:latest
            python3 -m pip install ansible-navigator --user
            echo 'export PATH=$HOME/.local/bin:$PATH' >> ~/.profile
            EOF
          tags:
            Name: "my-instance"

    outputs:
      vpcId: ${myVpc.id}
      subnetId: ${mySubnet.id}
      internetGatewayId: ${myInternetGateway.id}
      routeTableId: ${myRouteTable.id}
      routeTableAssociationId: ${myRouteTableAssociation.id}
      routeID: ${myRoute.id}
      securityGroupId: ${mySecurityGroup.id}
      instanceId: ${myInstance.id}
      instancePublicIp: ${myInstance.publicIp}
    ```

    - ポイントは以下の通り
      1. AMI IDはRHEL9を指定する
      2. [aws:ec2:Instance][3]の`userData`の箇所に、インスタンスの構築後に実行したいスクリプトを記述する
          - Ansible Navigatorのインストール方法は、[Ansible Navigator Documentation][5]を参照
          - Ansible実行環境イメージは、[ghcr.io/ansible-community/community-ee-base][2]を使用する
      3. スクリプトは`root`ユーザーで実行される。そのため、特定のユーザー（今回は`ec2-user`）での  
      処理をしたい場合は、`su`コマンドでユーザーを切り替える必要がある

### 3. 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               quickstart-dev           created (47s)       
    +   ├─ aws:ec2:Vpc                    myVpc                    created (1s)        
    +   ├─ aws:ec2:SecurityGroup          mySecurityGroup          created (3s)        
    +   ├─ aws:ec2:InternetGateway        myInternetGateway        created (0.78s)     
    +   ├─ aws:ec2:Subnet                 mySubnet                 created (11s)       
    +   ├─ aws:ec2:RouteTable             myRouteTable             created (1s)        
    +   ├─ aws:ec2:Route                  myRoute                  created (0.73s)     
    +   ├─ aws:ec2:RouteTableAssociation  myRouteTableAssociation  created (0.91s)     
    +   └─ aws:ec2:Instance               myInstance               created (32s)  
    ```

### 4. Ansible Navigatorの実行確認

1. 構築されたEC2インスタンスにログインする
    - user：ec2-user（ユーザー名はAMI IDによって異なります）
2. Ansible実行環境イメージが存在することを確認する

    ```console
    $ podman images
    REPOSITORY                                   TAG         IMAGE ID      CREATED     SIZE
    ghcr.io/ansible-community/community-ee-base  latest      71b6d35cd089  2 days ago  347 MB
    ```

3. テスト用のPlaybookを作成する

    ```console
    $ vi playbook.yml
    ```

    ```yaml
    ---
    - name: Sample Play
      hosts: localhost
      gather_facts: false
      tasks:
        - name: Hello World!
          ansible.builtin.debug:
            msg: "Hello World!"
    ```

4. Playbookを実行する

    ```console
    $ ansible-navigator run playbook.yml --eei ghcr.io/ansible-community/community-ee-base --pp never -m stdout
    ```

    ```console
    PLAY [Sample Play] *************************************************************

    TASK [Hello World!] ************************************************************
    ok: [localhost] => {
        "msg": "Hello World!"
    }

    PLAY RECAP *********************************************************************
    localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    ```

## さいごに

- AWSリソースの定義ファイル（`Pulumi.yaml`）で、EC2インスタンス構築後に実行するスクリプトを書けるのは便利だと感じます
- AWSリソースの定義ファイル（`Pulumi.yaml`）は複数のEC2インスタンスを構築することも可能なので、  
Ansibleの対象機器も構築できれば開発の更なる効率化が見込めるのではないかと思います

## 参考URL

- [ghcr.io/ansible-community/community-ee-base][2]
  - 今回使用したAnsible実行環境イメージ
- [aws:ec2:Instance][3]
  - Pulumiにおける`aws:ec2:Instance`の使用方法
- [Ansible Navigator Documentation][5]
  - Ansible Navigatorのインストール方法

## 関連記事

- [【Pulumi】Pulumi AIが生成したコードでEC2インスタンスを構築したい～第1章：YAML編～][1]
- [【Ansible】Amazon EC2でansible-navigatorを使いたい～第2章：ansible-navigatorの実行～][4]

---

[1]: https://qiita.com/masa2223/items/ae09ef1af1f1503a950e
[2]: https://github.com/orgs/ansible-community/packages/container/package/community-ee-base
[3]: https://www.pulumi.com/registry/packages/aws/api-docs/ec2/instance/
[4]: https://qiita.com/masa2223/items/f97c00bd62f76d89dbf9
[5]: https://ansible.readthedocs.io/projects/navigator/installation/#install-ansible-navigator