はじめに
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
構成について
構成図
今回作成したリソース
- 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のコンソールから確認することも可能です
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型(SecurityGroupEgress、SecurityGroupEgress)のリストで設定します
protocol
はHTTPやSSHなどのレイヤーのプロトコルを指定できるわけではなく、TCP、UDPなどを指定するパラメータとなります。
HTTPやSSHなどのプロトコルを直接設定したい場合でもコードのようにfrom_port、to_port
にポート番号で直接設定する必要があるようでした。
ポート番号で指定しても、作成したリソースをAWSコンソール上で確認するとHTTPやSSHと解釈してくれています。
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_snapshot
はTrue
に設定しておく必要があります。
コード全体
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