PulumiでAWSにWEBアプリの最小構成を構築する

# はじめに
Pulumiはプログラミング言語（現在サポートされているのは、JavaScript、TypeScript、python、go、C#）によりインフラをコード管理するためのツールです。

Pulumiを使ってpythonでAWSにWEBアプリを動かすための基本的な構成を構築してみて気づいたことなどを書いてみます。

pulumi環境構築事などについては下記のチュートリアルを参考にしてください
https://www.pulumi.com/docs/get-started/aws/

# 実行環境
python: 3.7.7
pulumi: 2.7.1
aws-cli: 1.18.102

# 構成について
### 構成図
<img width="728" alt="スクリーンショット 2020-07-24 19.32.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680074/e177d0c2-9e89-f3fa-ea69-a020106475f7.png">

### 今回作成したリソース
- VPC
- Subnet(public、private)
- InternetGateway
- RouteTable
- RouteTableAssociation
- SecurityGroup(EC2用、RDS用)
- KeyPair(ec2 ssh用)
- EC2
- EIP
- RDS SubnetGroup
- RDS

# 実際のコード
#### VPC
```python
vpc = aws.ec2.Vpc(
    "pulumi-vpc",
    cidr_block="10.0.0.0/16",
    tags={
        "Name": "pulumi-vpc",
    })
```
vpcについては、最低限、resource_nameとcidr_blockが設定されていれば良い
#### Subnet
```python
#public subnetの作成
public_subnet_a = aws.ec2.Subnet(
    "pulumi-public-subnet-a",
    cidr_block="10.0.1.0/24",
    availability_zone="ap-northeast-1a",
    tags={
        "Name": "pulumi-public-subnet-a",
    },
    vpc_id=vpc.id)

public_subnet_c = aws.ec2.Subnet(
    "pulumi-public-subnet-c",
    cidr_block="10.0.2.0/24",
    availability_zone="ap-northeast-1c",
    tags={
        "Name": "pulumi-public-subnet-c",
    },
    vpc_id=vpc.id)

#private subnetの作成
private_subnet_a = aws.ec2.Subnet(
    "pulumi-private-subnet-a",
    cidr_block="10.0.3.0/24",
    availability_zone="ap-northeast-1a",
    tags={
        "Name": "pulumi-private-subnet-a",
    },
    vpc_id=vpc.id)

private_subnet_c = aws.ec2.Subnet(
    "pulumi-private-subnet-c",
    cidr_block="10.0.4.0/24",
    availability_zone="ap-northeast-1c",
    tags={
        "Name": "pulumi-private-subnet-c",
    },
    vpc_id=vpc.id)
```
`vpc_id`には上記で作成したvpcのid(`vpc.id`)を入れています
pulumi側で依存関係を解決してくれて、vpc->subnetという順番でawsにリソースを作成してくれます（依存関係がないリソースは並列で作成してくれる）
また、リソースの依存関係についてはpulumiのコンソールから確認することも可能です
<img width="1408" alt="スクリーンショット 2020-07-24 20.04.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680074/5168502e-ba31-c5f8-9d91-4c672e8f7246.png">
#### InternetGateway
```python
igw = aws.ec2.InternetGateway(
    "pulumi-igw",
    tags={
        "Name": "pulumi-igw",
    },
    vpc_id=vpc.id)

```
#### RouteTable、RouteTableAssociation
```python
# RouteTableの作成
public_route_table_a = aws.ec2.RouteTable(
    "pulumi-public-route-table-a",
    routes=[
        {
            "cidr_block": "0.0.0.0/0",
            "gateway_id": igw.id,
        },
    ],
    tags={
        "Name": "pulumi-public-route-table-a",
    },
    vpc_id=vpc.id)

public_route_table_c = aws.ec2.RouteTable(
    "pulumi-public-route-table-c",
    routes=[
        {
            "cidr_block": "0.0.0.0/0",
            "gateway_id": igw.id,
        },
    ],
    tags={
        "Name": "pulumi-public-route-table-c",
    },
    vpc_id=vpc.id)

# RouteTableAssociationの作成
route_table_association_public_a = aws.ec2.RouteTableAssociation(
    "pulumi-route-table-association-public-a",
    subnet_id=public_subnet_a.id,
    route_table_id=public_route_table_a.id)

route_table_association_public_c = aws.ec2.RouteTableAssociation(
    "pulumi-route-table-association-public-c",
    subnet_id=public_subnet_c.id,
    route_table_id=public_route_table_c.id)
```
RouteTableのrouteについてcider_blockと宛先のdic型（pulumi docでは[RouteTableRoute](https://www.pulumi.com/docs/reference/pkg/aws/ec2/routetable/#routetableroute)）で設定し、routesにリスト形式で設定します
今回はinternet gateway向けのルートしかないので宛先を`gateway_id`のkeyを使用していますが、他にも下記のようなkeyも設定可能です

| keyに設定する値     |宛先 |
|:-----------------|:------------------|
| gateway_id       |Internet Gateway|
| instance_id      |ec2インスタンス　　　　|
| nat_gateway_id   |NAT Gateway|
| vpc_peering_connection_id|VPC Peeringの接続先VPC|

#### SecurityGroup
```python
# SecurityGroupの作成
ec2_sg = aws.ec2.SecurityGroup(
    "pulumi-ec2-sg",
    ingress=[
        {
            "from_port": 80,
            "protocol": "TCP",
            "to_port": 80,
            "cidr_blocks": ["0.0.0.0/0"]
        },
        {
            "from_port": 22,
            "protocol": "TCP",
            "to_port": 22,
            "cidr_blocks": ["0.0.0.0/0"]
        },
    ],
    egress=[
        {
            "from_port": 0,
            "protocol": "TCP",
            "to_port": 65535,
            "cidr_blocks": ["0.0.0.0/0"]
        },
    ],
    tags={
        "Name": "pulumi-ec2-sg",
    },
    vpc_id=vpc.id)

rds_sg = aws.ec2.SecurityGroup(
    "pulumi-rds-sg",
    ingress=[
        {
            "from_port": 3306,
            "protocol": "TCP",
            "to_port": 3306,
            "security_groups": [ec2_sg.id]
        },
    ],
    egress=[
        {
            "from_port": 0,
            "protocol": "TCP",
            "to_port": 65535,
            "cidr_blocks": ["0.0.0.0/0"]
        },
    ],
    tags={
        "Name": "pulumi-rds-sg",
    },
    vpc_id=vpc.id)
```
ingressとegressを指定する際に、dic型([SecurityGroupEgress](https://www.pulumi.com/docs/reference/pkg/aws/ec2/securitygroup/#securitygroupingress)、[SecurityGroupEgress](https://www.pulumi.com/docs/reference/pkg/aws/ec2/securitygroup/#securitygroupegress))のリストで設定します
`protocol`はHTTPやSSHなどのレイヤーのプロトコルを指定できるわけではなく、TCP、UDPなどを指定するパラメータとなります。
HTTPやSSHなどのプロトコルを直接設定したい場合でもコードのように`from_port、to_port`にポート番号で直接設定する必要があるようでした。
ポート番号で指定しても、作成したリソースをAWSコンソール上で確認するとHTTPやSSHと解釈してくれています。
<img width="554" alt="スクリーンショット 2020-07-24 23.02.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/680074/0dbaf808-15ae-0eab-a3ab-25c3f21fb63a.png">

#### KeyPair
```python
key_pair = aws.ec2.KeyPair(
    "pulumi-keypair",
    public_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD3F6tyPEFEzV0LX3X8BsXdMsQz1x2cEikKDEY0aIj41qgxMCP/iteneqXSIFZBp5vizPvaoIR3Um9xK7PGoW8giupGn+EPuxIA4cDM4vzOqOkiMPhz5XK0whEjkVzTo4+S0puvDZuwIsdiW9mxhJc7tgBNL0cYlWSYVkz4G/fslNfRPW5mYAM49f4fhtxPb5ok4Q2Lg9dPKVHO/Bgeu5woMc7RY0p1ej6D4CKFE6lymSDJpW0YHX/wqE9+cfEauh7xZcG0q9t2ta6F6fmX0agvpFyZo8aFbXeUBr7osSCJNgvavWbM/06niWrOvYX2xwWdhXmXSrbX8ZbabVohBK41 email@example.com",
    tags={
        "Name": "pulumi-keypair",
    })
```
#### EC2
```python
ec2 = aws.ec2.Instance(
    "pulumi-ec2",
    ami="ami-0ee1410f0644c1cac",
    instance_type="t2.micro",
    subnet_id=public_subnet_a.id,
    tags={
        "Name": "pulumi-ec2",
    },
    associate_public_ip_address=True,
    key_name=key_pair.key_name,
    user_data="#!/bin/bash \n\n yum install -y mysql",
    vpc_security_group_ids=[ec2_sg.id])
```
`ami`はamiのidを直接指定していますが、pulumiにはgetの[api](https://www.pulumi.com/docs/reference/pkg/aws/ec2/ami/#look-up)も用意されているのでそれを利用して、既存のリソースからidを取得しても良いと思います。

`user_data`については、String型での指定となります。
今回は、直接ユーザデータをStringとしてベタ書きしていますが、別ファイルに切り出してファイル読み込みする方が良いと思います
また、`user_data_base64`というパラメータもあり、ユーザデータをbase64でエンコードしたものでも設定できるようでした

EC2インスタンスに紐付けるsecurity_groupの設定ですが、`security_groups`というパラメータがDeprecatedとなっていたため`vpc_security_group_ids`で指定しています

#### EIP
```python
ec2_eip = aws.ec2.Eip(
    "pulumi-eip-ec2",
    instance=ec2.id,
    tags={
        "Name": "pulumi-eip-ec2",
    },
    vpc=True)
```
今回はeip作成時にeipを紐付けるec2インスタンスまで指定していますが、下記のように作成と紐付けを別々にコード化することも可能です

```python
ec2_eip = aws.ec2.Eip(
    "pulumi-eip-ec2",
    tags={
        "Name": "pulumi-eip-ec2",
    },
    vpc=True)

eip_assoc = aws.ec2.EipAssociation("eipAssoc",
    allocation_id=ec2_eip.id,
    instance_id=ec2.id)
```
#### RDS SubnetGroup

```python
rds_subnet = aws.rds.SubnetGroup(
    "pulumi-rds-subnet",
    subnet_ids=[
        private_subnet_a.id,
        private_subnet_c.id,
    ],
    tags={
        "Name": "pulumi-rds-subnet",
    })
```

#### RDS

```python
rds = aws.rds.Instance(
    "pulumi-rds",
    allocated_storage=20,
    db_subnet_group_name=rds_subnet.name,
    engine="mysql",
    engine_version="5.7",
    identifier="pulumi-rds",
    instance_class="db.t2.micro",
    name="pulumi",
    parameter_group_name="default.mysql5.7",
    password="password",
    skip_final_snapshot=True,
    storage_type="gp2",
    tags={
        "Name": "pulumi-rds",
    },
    username="admin",
    vpc_security_group_ids=[rds_sg.id])
```
RDSを作成する際に注意が必要なのが`skip_final_snapshot`のパラメータです。
このパラメータはDefaultでは`False`が設定されているのですが、そのままではRDSを削除する際に、snap shotを残そうとしてます。
この場合`pulumi destroy`で削除しようとした際に下記のようなエラーが出てしまいます。

```shell
DB Instance FinalSnapshotIdentifier is required when a final snapshot is required
```
pulumiからRDSを削除できるようにしておくには`skip_final_snapshot`は`True`に設定しておく必要があります。

#### コード全体
```python

import pulumi
import pulumi_aws as aws

# VPCの作成
vpc = aws.ec2.Vpc(
    "pulumi-vpc",
    cidr_block="10.0.0.0/16",
    tags={
        "Name": "pulumi-vpc",
    })

# Subnetの作成
public_subnet_a = aws.ec2.Subnet(
    "pulumi-public-subnet-a",
    cidr_block="10.0.1.0/24",
    availability_zone="ap-northeast-1a",
    tags={
        "Name": "pulumi-public-subnet-a",
    },
    vpc_id=vpc.id)

public_subnet_c = aws.ec2.Subnet(
    "pulumi-public-subnet-c",
    cidr_block="10.0.2.0/24",
    availability_zone="ap-northeast-1c",
    tags={
        "Name": "pulumi-public-subnet-c",
    },
    vpc_id=vpc.id)

private_subnet_a = aws.ec2.Subnet(
    "pulumi-private-subnet-a",
    cidr_block="10.0.3.0/24",
    availability_zone="ap-northeast-1a",
    tags={
        "Name": "pulumi-private-subnet-a",
    },
    vpc_id=vpc.id)

private_subnet_c = aws.ec2.Subnet(
    "pulumi-private-subnet-c",
    cidr_block="10.0.4.0/24",
    availability_zone="ap-northeast-1c",
    tags={
        "Name": "pulumi-private-subnet-c",
    },
    vpc_id=vpc.id)

# InternetGatewayの作成
igw = aws.ec2.InternetGateway(
    "pulumi-igw",
    tags={
        "Name": "pulumi-igw",
    },
    vpc_id=vpc.id)

# RouteTableの作成
public_route_table_a = aws.ec2.RouteTable(
    "pulumi-public-route-table-a",
    routes=[
        {
            "cidr_block": "0.0.0.0/0",
            "gateway_id": igw.id,
        },
    ],
    tags={
        "Name": "pulumi-public-route-table-a",
    },
    vpc_id=vpc.id)

public_route_table_c = aws.ec2.RouteTable(
    "pulumi-public-route-table-c",
    routes=[
        {
            "cidr_block": "0.0.0.0/0",
            "gateway_id": igw.id,
        },
    ],
    tags={
        "Name": "pulumi-public-route-table-c",
    },
    vpc_id=vpc.id)

# RouteTableAssociationの作成
route_table_association_public_a = aws.ec2.RouteTableAssociation(
    "pulumi-route-table-association-public-a",
    subnet_id=public_subnet_a.id,
    route_table_id=public_route_table_a.id)

route_table_association_public_c = aws.ec2.RouteTableAssociation(
    "pulumi-route-table-association-public-c",
    subnet_id=public_subnet_c.id,
    route_table_id=public_route_table_c.id)

# SecurityGroupの作成
ec2_sg = aws.ec2.SecurityGroup(
    "pulumi-ec2-sg",
    ingress=[
        {
            "from_port": 80,
            "protocol": "TCP",
            "to_port": 80,
            "cidr_blocks": ["0.0.0.0/0"]
        },
        {
            "from_port": 22,
            "protocol": "TCP",
            "to_port": 22,
            "cidr_blocks": ["0.0.0.0/0"]
        },
    ],
    egress=[
        {
            "from_port": 0,
            "protocol": "TCP",
            "to_port": 65535,
            "cidr_blocks": ["0.0.0.0/0"]
        },
    ],
    tags={
        "Name": "pulumi-ec2-sg",
    },
    vpc_id=vpc.id)

rds_sg = aws.ec2.SecurityGroup(
    "pulumi-rds-sg",
    ingress=[
        {
            "from_port": 3306,
            "protocol": "TCP",
            "to_port": 3306,
            "security_groups": [ec2_sg.id]
        },
    ],
    egress=[
        {
            "from_port": 0,
            "protocol": "TCP",
            "to_port": 65535,
            "cidr_blocks": ["0.0.0.0/0"]
        },
    ],
    tags={
        "Name": "pulumi-rds-sg",
    },
    vpc_id=vpc.id)

# KeyPairの作成
key_pair = aws.ec2.KeyPair(
    "pulumi-keypair",
    public_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDDoh/IrdxamyghEb22+wUAFqz1Hd/dxAQaaUO050WaZgKzFTl/gpiJokNNGY9lA1PmilHW9ZGYW8aTrbGyiOIF4l6hsGVFWXMjwZdY0b4rM1uHkLUnf8X26uDn/BUzTZLCgeoThp4IAuHWCKoJJucVuN09Kpdsd75vSjRyDBl3/q0ex0rQIrCmpqA7OrYr7PaJlgoFk1dpaQvxrQZyVR5oh7+IvMLdm+NP4uUSAlPpHUv3XoPKZ74bsnd/G+oNIWlo4ZW/uJX4oM1g7sSKxRmkF5VQK3afEt28Su1mpdrJczQPBm0Dq0sfYg+sS432KLanc7cGPbKf6tjLE6ouxZYN fukusako@yutanoMacBook-puro-3.local",
    tags={
        "Name": "pulumi-keypair",
    })

# EC2インスタンスの作成
ec2 = aws.ec2.Instance(
    "pulumi-ec2",
    ami="ami-0ee1410f0644c1cac",
    instance_type="t2.micro",
    subnet_id=public_subnet_a.id,
    tags={
        "Name": "pulumi-ec2",
    },
    associate_public_ip_address=True,
    key_name=key_pair.key_name,
    user_data="#!/bin/bash \n\n yum install -y mysql",
    vpc_security_group_ids=[ec2_sg.id])

# EIPの作成
ec2_eip = aws.ec2.Eip(
    "pulumi-eip-ec2",
    instance=ec2.id,
    tags={
        "Name": "pulumi-eip-ec2",
    },
    vpc=True)

# RDS SubnetGroupの作成
rds_subnet = aws.rds.SubnetGroup(
    "pulumi-rds-subnet",
    subnet_ids=[
        private_subnet_a.id,
        private_subnet_c.id,
    ],
    tags={
        "Name": "pulumi-rds-subnet",
    })

#RDSインスタンスの作成
rds = aws.rds.Instance(
    "pulumi-rds",
    allocated_storage=20,
    db_subnet_group_name=rds_subnet.name,
    engine="mysql",
    engine_version="5.7",
    identifier="pulumi-rds",
    instance_class="db.t2.micro",
    name="pulumi",
    parameter_group_name="default.mysql5.7",
    password="password",
    skip_final_snapshot=True,
    storage_type="gp2",
    tags={
        "Name": "pulumi-rds",
    },
    username="admin",
    vpc_security_group_ids=[rds_sg.id])
```

# まとめ
普段、terraformを用いてaws環境のリソースをコード管理しているのですが、pulumiはterraformと違いpythonなど馴染みのある言語で記述できるので、個人的には直感的にわかりやすくコード化できた気がします。
また、state管理を専用のコンソールで管理できるため、terraformの用にtfstateファイルを管理する必要もないですし、pulumi up/destroy時の依存関係も勝手に解決してくれるので、コード化できる言語云々よりもステート管理がterraformよりもpulumiの方が長けているのではと感じました。

~~ただし、pulumiはterraformよりもplan、apply時の差分がlogとして分かりづらい（変更されるパラメータ名しか表示されない）のが残念でした。~~　コンソールから差分の詳細を確認できました
また、チームなど複数メンバーで使用するとなると料金がかかってしまうので、それがterraformと比べて導入するハードルになっているのかなと思います。

今後はALB+EC2(auto scaling)のような基本的なwebアプリ構成やAPIGW+Lambdaのようなサーバレスの構築もpulumiで構築してみたいと思います。

# おまけ
pulumiを触っていてハマったことなど
#### pulumi up/destroyで任意のリソースだけを更新したい場合
```shell
# -t オプションで任意のリソースを指定できる
> pulumi up -t リソースのURN
> pulumi destroy -t リソースのURN

#リソースのURNは、恐らくpulumiがリソースを管理するためのIDでstackオプションで確認できる
> pulumi stack -u
Current stack resources (16):
    TYPE                                            NAME
    pulumi:pulumi:Stack                             aws-ec2-public-aws-ec2
    │  URN: urn:pulumi:aws-ec2::aws-ec2-public::pulumi:pulumi:Stack::aws-ec2-public-aws-ec2
    ├─ aws:ec2/vpc:Vpc                              pulumi-vpc
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/vpc:Vpc::pulumi-vpc
    ├─ aws:ec2/subnet:Subnet                        pulumi-public-subnet-a
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/subnet:Subnet::pulumi-public-subnet-a
    ├─ aws:ec2/subnet:Subnet                        pulumi-public-subnet-c
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/subnet:Subnet::pulumi-public-subnet-c
    ├─ aws:ec2/subnet:Subnet                        pulumi-private-subnet-a
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/subnet:Subnet::pulumi-private-subnet-a
    ├─ aws:ec2/subnet:Subnet                        pulumi-private-subnet-c
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/subnet:Subnet::pulumi-private-subnet-c
    ├─ aws:ec2/internetGateway:InternetGateway      pulumi-igw
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/internetGateway:InternetGateway::pulumi-igw
    ├─ aws:ec2/securityGroup:SecurityGroup          pulumi-ec2-sg
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/securityGroup:SecurityGroup::pulumi-ec2-sg
    ├─ aws:rds/subnetGroup:SubnetGroup              pulumi-rds-subnet
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:rds/subnetGroup:SubnetGroup::pulumi-rds-subnet
    ├─ aws:ec2/routeTable:RouteTable                pulumi-public-route-table-a
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/routeTable:RouteTable::pulumi-public-route-table-a
    ├─ aws:ec2/routeTable:RouteTable                pulumi-public-route-table-c
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/routeTable:RouteTable::pulumi-public-route-table-c
    ├─ aws:ec2/securityGroup:SecurityGroup          pulumi-rds-sg
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/securityGroup:SecurityGroup::pulumi-rds-sg
    ├─ aws:ec2/keyPair:KeyPair                      pulumi-keypair
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/keyPair:KeyPair::pulumi-keypair
    ├─ aws:rds/instance:Instance                    pulumi-rds
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:rds/instance:Instance::pulumi-rds
    ├─ aws:ec2/instance:Instance                    pulumi-ec2
    │     URN: urn:pulumi:aws-ec2::aws-ec2-public::aws:ec2/instance:Instance::pulumi-ec2
    └─ pulumi:providers:aws                         default_2_13_1
          URN: urn:pulumi:aws-ec2::aws-ec2-public::pulumi:providers:aws::default_2_13_1
```

#### AWSのコンソールからリソースを更新した場合
state管理されているリソースをAWSコンソールから直接更新した場合、stateファイルと実際の環境で差分が出てしまい、この状態でコードから改めて更新(pulumi upなど)しようとするとうまく動作しないことがあります（当たり前ですが）。
このような場合`refresh`オプションを使用するとstate管理されているリソースについて現行のAWSリソースの情報を元にstate情報をアップデートしてくれます。

```shell
> pulumi refresh
```
#### terraform planのようなdryrun機能
```shell
#差分だけ確認する
> pulumi preview
#apply
> pulumi up -y
```
`pulumi up`だけでも差分を確認した後に適用するか、yes、noで確認されてnoを選択すれば差分を確認だけできる。
ただし、CIサーバーなどからのdeployを想定する場合コンソールからyes、noを選択するやり方はやりにくいので、dryrunは`pulumi preview`、実際の適用は`pulumi up -y`を利用するのが良い

#参考
https://www.pulumi.com/docs/
https://qiita.com/KsntsTt/items/6fb71af2d265939184c3