2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-07-24

はじめに

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

構成について

構成図

スクリーンショット 2020-07-24 19.32.59.png

今回作成したリソース

  • VPC
  • Subnet(public、private)
  • InternetGateway
  • RouteTable
  • RouteTableAssociation
  • SecurityGroup(EC2用、RDS用)
  • KeyPair(ec2 ssh用)
  • EC2
  • EIP
  • RDS SubnetGroup
  • RDS

実際のコード

VPC

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

vpcについては、最低限、resource_nameとcidr_blockが設定されていれば良い

Subnet

#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のコンソールから確認することも可能です
スクリーンショット 2020-07-24 20.04.33.png

InternetGateway

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

RouteTable、RouteTableAssociation

# 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)で設定し、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

# 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型(SecurityGroupEgressSecurityGroupEgress)のリストで設定します
protocolはHTTPやSSHなどのレイヤーのプロトコルを指定できるわけではなく、TCP、UDPなどを指定するパラメータとなります。
HTTPやSSHなどのプロトコルを直接設定したい場合でもコードのようにfrom_port、to_portにポート番号で直接設定する必要があるようでした。
ポート番号で指定しても、作成したリソースをAWSコンソール上で確認するとHTTPやSSHと解釈してくれています。
スクリーンショット 2020-07-24 23.02.09.png

KeyPair

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

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も用意されているのでそれを利用して、既存のリソースからidを取得しても良いと思います。

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

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

EIP

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

今回はeip作成時にeipを紐付けるec2インスタンスまで指定していますが、下記のように作成と紐付けを別々にコード化することも可能です

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

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])

RDSを作成する際に注意が必要なのがskip_final_snapshotのパラメータです。
このパラメータはDefaultではFalseが設定されているのですが、そのままではRDSを削除する際に、snap shotを残そうとしてます。
この場合pulumi destroyで削除しようとした際に下記のようなエラーが出てしまいます。

DB Instance FinalSnapshotIdentifier is required when a final snapshot is required

pulumiからRDSを削除できるようにしておくにはskip_final_snapshotTrueに設定しておく必要があります。

コード全体


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で任意のリソースだけを更新したい場合

# -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情報をアップデートしてくれます。

> pulumi refresh

terraform planのようなdryrun機能

#差分だけ確認する
> 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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?