Pulumi. AWS VPC and Fargate configuration

Ipeacocks
12 min readMay 21, 2020

--

Modern world has a tendency to keep configuration like a code. It’s very convenient: you can track its changes in repository, you know who and when did that and whom to blame. And after all it’s just easy to revert everything back if something goes wrong or setup/configure additional system. You can see such tendency in many products: Jenkins has pipelines for describing own jobs, Prometheus monitoring system has no GUI at all so all configuration should be added as a code, Terraform, popular infrastructure as a code system (IaC), is kind of default standard in cloud architecture. Even you can draw diagrams as a code! In general, any new product is developed with the expectation that its settings can be stored and read as understandable code.

Often each product provides own language (DSL, Domain-specific language) for describing own configuration logic. For example Terraform has proprietary HCL, Puppet asks to use Ruby DSL. And I would say they are good for quite basic things when somebody has already wrapped all of that scary stuff in modules. But never look at that modules, they can hurt you a lot. Such code does not look intuitive, you just don’t remind what that code does on next day. Sometimes you just have to make a lot copy-pasta for doing the same because of DSL limitation. Have you seen that 100 symbol line in count of Terraform resource? Or how to implement hash list traversal in Puppet? Internet has a lot of articles of such unhappy engineers.

As an active Terraform user I would like to check something new and today I want to share my own non-production experience with Pulumi.

What is Pulumi? According to their site Pulumi is an open source infrastructure as code tool for creating, deploying, and managing cloud infrastructure. Pulumi works with traditional infrastructure like VMs, networks, and databases, in addition to modern architectures, including containers, Kubernetes clusters, and serverless functions. Pulumi supports dozens of public (AWS, Google Cloud, Azure, to name a few), private (OpenStack, vSphere), and hybrid cloud service providers. Such huge amount of supported platforms is due to the fact that Pulumi uses Terraform provider plugins via pulumi-terraform-bridge and thus supports the same functionality.

First public release of Pulumi saw this world in 2018 thanks to efforts of Seattle startup. The main idea of Pulumi was to leave practices of usage vendor-specific templating languages (guess about who are they talking about), be closer to the developers and in this way reduce the gap between them and operations team who supports them. Agree, it’s quite controversial but lets move on.

credits https://www.pulumi.com/

Pulumi enables developers to write code in their favorite language, such as TypeScript, JavaScript, Python, Go and .NET family(C#, F#, VB). This doesn’t mean that Pulumi is imperative way of doing you logic, it’s still declarative tool. But thanks to general purpose languages you also could use standard language construction, libraries and so on. No more DSL limitation!

Pulumi programs are structured as projects and stacks. The distinction between them is:

  • Program: a collection of files written in your chosen programming language
  • Project: a directory containing a program, with metadata, so Pulumi knows how to run it
  • Stack: an instance of your project, each often corresponding to a different cloud environment. It’s like Terraform workspaces.

If you are familiar with Terraform, Pulumi doesn’t blow your mind. It’s still desired state model for managing infrastructure.

credits https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/

Managing host (usually your host with installed Pulumi) sends request on creating/updating/removing your resources, deployed engine, based on state, decides what to do and then sends commands through plugins of providers. State could be saved in remote storage or locally. Details could be found here.

By default Pulumi tries to create resource and only after removes old one for reducing downtime. It’s real thanks to auto-naming functionalities when Pulumi adds random suffix to some of your resources. Of course this feature could be turned off.

Now lets try to do something meaningful with this new shiny tool. We are going to use Python interface with AWS but before install Pulumi and try basic how-to on official site. It’s easy but very important! We will build own VPC with 3 public and 3 private subnets and on top of them deploy basic nginx image in AWS ECS Fargate cluster.

All described above code you can find in my repository https://github.com/ipeacocks/pulumi-aws-example.

Check that you have installed Pulumi successfully:

$ pulumi version
v2.1.0

Export your AWS programmatic access as environment variables:

$ export AWS_ACCESS_KEY_ID=AKIA1234563J76A
$ export AWS_SECRET_ACCESS_KEY=/xLmpmdp1V3abcdefghklmnopabcdefg2nKRDKO
$ export AWS_REGION=us-east-1

There are a lot of ways of doing this for AWS. Use most convenient one for you. Now lets create bucket for future states or checkpoints in Pulumi’s terminology:

$ aws s3api create-bucket \
--bucket pulumi-states-zeezpha \
--region us-east-1
$ aws s3api put-bucket-versioning \
--bucket pulumi-states-zeezpha \
--versioning-configuration Status=Enabled

Instead of pulumi-states-zeezpha choose own unique name. By default Pulumi provides own backend for saving states https://app.pulumi.com/. It’s quite handy and I advice you to use it if you are ready to pay for it and not worry about saving your secrets in another separate organization.

Now we can connect this bucket with pulumi:

$ pulumi login s3://pulumi-states-zeezpha

And we are ready to go next. Create directory for our future stuff:

$ mkdir pulumi-infra-az
$ cd pulumi-infra-az

Initialize Pulumi’s Python boilerplate for AWS:

$ pulumi new aws-pythonThis 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-infra-az)
project description: (A minimal AWS Python Pulumi program)
Created project 'pulumi-infra-az'
stack name: (dev) pulumi-infra-az_dev
Enter your passphrase to protect config/secrets:
Re-enter your passphrase to confirm:
Created stack 'pulumi-infra-az_dev'
Enter your passphrase to unlock config/secrets
(set PULUMI_CONFIG_PASSPHRASE to remember):
aws:region: The AWS region to deploy into: (us-east-1)
Saved config
Your new project is ready to go!
...

Pulumi cli asks for PULUMI_CONFIG_PASSPHRASE each time when you create new stack with AWS S3 storage. It’s a technique for encrypting any sensitive data in code.

New stack pulumi-infra-az_dev is created and activated (see * near stack name):

$ pulumi stack ls
NAME LAST UPDATE RESOURCE COUNT
pulumi-infra-az_dev* 22 minutes ago 27

New stack can be also created with pulumi stack init command. Lets see what’s new in our directory:

$ ls
Pulumi.pulumi-infra-az_dev.yaml Pulumi.yaml __main__.py requirements.txt

Dependencies from requirements.txt need to be installed in venv for keeping main OS as clean as possible:

$ python3 -m venv venv
$ source venv/bin/activate
$ pip3 install -r requirements.txt

It takes a while so be patient. At this moment we are ready to change previously created files:

$ vim Pulumi.yamlname: pulumi-infra-az
description: AWS Infrastructure to run instances with Pulumi
runtime: python

Pulumi.yaml just says how to launch all these stuff with what language. Nothing else needs to be here.

Add in next file highlighted text:

$ vim Pulumi.pulumi-infra-az_dev.yamlencryptionsalt: v1:Rz0nYmmptxM=:v1:r7UoJ9nQeSJUyvtU:ffIWgSSYSlfSrvD/JyFxLwZ+nkIhoQ==
config:
aws:region: us-east-1
pulumi-infra-az:private_subnet_cidrs:
- 172.255.100.0/24
- 172.255.101.0/24
- 172.255.102.0/24
pulumi-infra-az:public_subnet_cidrs:
- 172.255.110.0/24
- 172.255.111.0/24
- 172.255.112.0/24
pulumi-infra-az:vpc_cidr: 172.255.0.0/16
pulumi-infra-az:zones_amount: 3

Where encryptionsalt is salted password for encrypting secrets (created automatically, already mentioned about that above), private_subnet_cidrs is list of IP ranges for private subnets, public_subnet_cidrs is obviously the same for public subnets and vpc_cidr is VPC IP range. We are interested in only 3 availability zones and set that with zones_amount parameter.

Lets look at the main code:

$ vim __main__.py

What was done here?

  • using config.require_object() we got all variables from Pulumi.pulumi-infra-az_dev.yaml
  • we created VPC, Internet Gateway and public route table because this resources don’t depend on not yet created subnets
  • using zip function we created a list of tuples like [(“us-east-1a”, “172.255.100.0/24", “172.255.110.0/24”), (“us-east-1b”, “172.255.101.0/24”, “172.255.111.0/24”), …] and after iterated through it
  • then we created all subnets and all stuff which depends on them, including 3 EIPs, 3 NAT Gateways for private subnets, routes and so on
  • exported variables for saving them in state for future usage with other pieces of code

You maybe noticed import utils line. It’s just for demonstration ability to include any Python code in Pulumi scenario. Script is really simple and there was no any reason to separate it from main listing:

Code is finished so we can apply:

$ pulumi up 

Enter your passphrase to unlock config/secrets
(set PULUMI_CONFIG_PASSPHRASE to remember):
Previewing update (pulumi-infra-az_dev):
Type Name Plan
+ pulumi:pulumi:Stack pulumi-infra-az-pulumi-infra-az_dev create
+ ├─ aws:ec2:Eip pulumi-eip-us-east-1a create
+ ├─ aws:ec2:Vpc pulumi-vpc create
+ ├─ aws:ec2:Eip pulumi-eip-us-east-1b create
+ ├─ aws:ec2:Eip pulumi-eip-us-east-1c create
+ ├─ aws:ec2:InternetGateway pulumi-igw create
+ ├─ aws:ec2:Subnet pulumi-public-subnet-us-east-1a create
+ ├─ aws:ec2:Subnet pulumi-private-subnet-us-east-1a create
+ ├─ aws:ec2:Subnet pulumi-public-subnet-us-east-1b create
+ ├─ aws:ec2:Subnet pulumi-private-subnet-us-east-1b create
+ ├─ aws:ec2:Subnet pulumi-private-subnet-us-east-1c create
+ ├─ aws:ec2:Subnet pulumi-public-subnet-us-east-1c create
+ ├─ aws:ec2:RouteTable pulumi-public-rt create
+ ├─ aws:ec2:NatGateway pulumi-natgw-us-east-1a create
+ ├─ aws:ec2:NatGateway pulumi-natgw-us-east-1c create
+ ├─ aws:ec2:NatGateway pulumi-natgw-us-east-1b create
+ ├─ aws:ec2:RouteTableAssociation pulumi-public-rta-us-east-1a create
+ ├─ aws:ec2:RouteTableAssociation pulumi-public-rta-us-east-1b create
+ ├─ aws:ec2:RouteTableAssociation pulumi-public-rta-us-east-1c create
+ ├─ aws:ec2:RouteTable pulumi-private-rt-us-east-1a create
+ ├─ aws:ec2:RouteTable pulumi-private-rt-us-east-1c create
+ ├─ aws:ec2:RouteTable pulumi-private-rt-us-east-1b create
+ ├─ aws:ec2:RouteTableAssociation pulumi-private-rta-us-east-1a create
+ ├─ aws:ec2:RouteTableAssociation pulumi-private-rta-us-east-1c create
+ └─ aws:ec2:RouteTableAssociation pulumi-private-rta-us-east-1b create
Resources:
+ 25 to create
Do you want to perform this update? yes
Updating (pulumi-infra-az_dev):
Type Name Status
+ pulumi:pulumi:Stack pulumi-infra-az-pulumi-infra-az_dev created
+ ├─ aws:ec2:Eip pulumi-eip-us-east-1a created
...
+ ├─ aws:ec2:RouteTable pulumi-private-rt-us-east-1a created
+ └─ aws:ec2:RouteTableAssociation pulumi-private-rta-us-east-1a created
Outputs:
pulumi-az-amount : 3
pulumi-private-subnet-ids: [
[0]: "subnet-0247a0dc1c4908be5"
[1]: "subnet-0f6782bc1ba956151"
[2]: "subnet-07486fe231764e33a"
]
pulumi-public-subnet-ids : [
[0]: "subnet-04e7996f3136725ff"
[1]: "subnet-07d007de94f1c83e4"
[2]: "subnet-04d494b32373b9f2a"
]
pulumi-vpc-id : "vpc-007606812e1b879d3"
Resources:
+ 25 created
Duration: 3m6sPermalink: https://pulumi-states-zeezpha.s3.amazonaws.com/.pulumi/stacks/pulumi-infra-az_dev.json?...7addac8bdc5cc4d9861

I would admit that Pulumi’s output is perhaps more informative than Terraform’s one. If you wish you could type details before applying code. Also pay attention on Outputs sections, that is result of our work.

Now you can discover your AWS account and enjoy the new resources:

But it’s not the end of our today’s journey. Nobody needs a network w/o any useful resource. So lets setup new AWS ECS Fargate cluster with a service on top of it. The logic is absolutely the same. We need new directories for code:

$ mkdir pulumi-ecs-fargate
$ cd pulumi-ecs-fargate

New stack and template files:

$ pulumi new aws-pythonThis 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-ecs-fargate)
project description: (A minimal AWS Python Pulumi program) A container running in AWS ECS Fargate, using Python infrastructure as code
Created project ‘pulumi-ecs-fargate’
stack name: (dev) pulumi-ecs-fargate_dev
Enter your passphrase to protect config/secrets:
Re-enter your passphrase to confirm:
Created stack ‘pulumi-ecs-fargate_dev’
Enter your passphrase to unlock config/secrets
(set PULUMI_CONFIG_PASSPHRASE to remember):
aws:region: The AWS region to deploy into: (us-east-1)
Saved config
Your new project is ready to go!

And activate Python venv:

$ python3 -m venv venv
$ source venv/bin/activate
$ pip3 install -r requirements.txt

Then edit new content in newly created directory:

$ vim Pulumi.pulumi-ecs-fargate_dev.yamlencryptionsalt: v1:Rz0nYmmptxM=:v1:r7UoJ9nQeSJUyvtU:ffIWgSSYSlfSrvD/JyFxLwZ+nkIhoQ==
config:
aws:region: us-east-1

Highlighted section is one which you need to add. Moving on.

$ vim Pulumi.yamlname: pulumi-ecs-fargate
description: A container running in AWS ECS Fargate, using Python infrastructure as code
runtime: python

Nothing new there, just a metadata for Pulumi. And now most interesting part __main__.py:

Here we done:

  • read network output data with StackReference(f”pulumi-infra-az_dev”)
  • created ECS cluster, security group sgroup because we would like to open only one port on balancer, application balancer, target group for it and listener for forwarding traffic to container
  • ECS service-related stuff: roles and service with task definition described above
  • export url to our test application

Lets apply this code:

$ pulumi upEnter your passphrase to unlock config/secrets
(set PULUMI_CONFIG_PASSPHRASE to remember):
Previewing update (pulumi-ecs-fargate_dev):
Type Name Plan
+ pulumi:pulumi:Stack pulumi-ecs-fargate-pulumi-ecs-fargate_dev create
+ ├─ aws:iam:Role pulumi-app-te-role create
+ ├─ aws:ecs:Cluster pulumi-app-cluster create
+ ├─ aws:ec2:SecurityGroup pulumi-app-sg create
+ ├─ aws:lb:TargetGroup pulumi-app-tg create
+ ├─ aws:iam:RolePolicyAttachment pulumi-app-te-policy-attach create
+ ├─ aws:ecs:TaskDefinition pulumi-app-task-def create
+ ├─ aws:lb:LoadBalancer pulumi-app-alb create
+ ├─ aws:lb:Listener pulumi-app-listener create
+ └─ aws:ecs:Service pulumi-app-svc create
Resources:
+ 10 to create
Do you want to perform this update? yes
Updating (pulumi-ecs-fargate_dev):
Type Name Status
+ pulumi:pulumi:Stack pulumi-ecs-fargate-pulumi-ecs-fargate_dev created
+ ├─ aws:iam:Role pulumi-app-te-role created
...
+ └─ aws:ecs:Service pulumi-app-svc created
Outputs:
url: "pulumi-app-alb-f80aa12-414577325.us-east-1.elb.amazonaws.com"
Resources:
+ 10 created
Duration: 3m49sPermalink: https://pulumi-states-zeezpha.s3.amazonaws.com/.pulumi/stacks/pulumi-ecs-fargate_dev.json?X-Amz-Algorithm=AWS4-HMAC-SHA256...4b0ba0813247c1

Now on http://pulumi-app-alb-f80aa12–414577325.us-east-1.elb.amazonaws.com our plain nginx service lives:

Don’t forget to remove all services if you don’t need them anymore:

$ pulumi destroy

Now you maybe think when to start migrating on Pulumi. It’s really quite promising product and I wish it good luck…but take into consideration next facts:

  • it’s very young product: first public version was presented in 2018 only!
  • not enough documentation. Good examples your can find in GitHub repositories but very often on JavaScript/TypeScript only. It’s really hard to find how to use Pulumi or how to organize code/dir structure with it.
  • no locks with 3rd-party storage for states. Those locks which help you to avoid running the same code at the same time. But maybe it’s not a huge problem because it’s a general purpose language and you can use mature libraries for doing that by yourself.
  • as Pulumi uses general purpose languages your teammates could create a real monster even worse than with Terraform! So you definitely need to have rules how code needs to look in your command.
  • your team needs to know any general purpose language and its strutures. But its learning definitely worth time investment.

But it doesn’t mean that you cannot try it for your pet project or even to use it alongside with Terraform because Pulumi can read Terraform states or even convert Terraform code to own one. So it’s not necessary to destroy and rebuild everything from the scratch. Have fun and enjoy studying!

Links:

--

--