Infrastructure as Code in TypeScript
jakubkoci
Posted on April 6, 2020
It's been about 4 three years since I participated in the AWS hackathon. There were some great talks about AWS before the hackathon itself. I remember I was both amazed and overwhelmed at the same time. I was amazed by the possibilities you have with AWS and saw a huge potential there. On the other hand, I was overwhelmed by all the clicking here and there across the AWS console. Time went by and I haven’t worked with infrastructure so much until now.
Fortunately, I’ve recently found Pulumi which saved me from the AWS console clicking panic. Pulumi is a tool that allows you to define your infrastructure as a code. It supports AWS, Azure, GCP, and many other cloud providers. You can also use it for Kubernetes. I know there are already similar tools like AWS CloudFormation or Terraform. The problem is that the first one is only for AWS and the second has its own custom language. Here comes the biggest advantage Pulumi has from my perspective. You can define your infrastructure in JavaScript or TypeScript. It also supports Python and there is some preview version for Go and C#.
My infrastructure is just one EC2 instance with inbound SSH and outbound internet access. However, I still ended up with something a little bit different than what was in the official Pulumi Getting Started documentation and I also found some errors I needed to solve along the way. That's what I want to share with you here.
Pulumi Get Started with AWS | Pulumi manual is quite good, so look for the Pulumi installation, AWS Setup and creating a new project there.
After successfully going through the quickstart
project, I continued with a project for my infrastructure. It consists of VPC, subnet in the VPC, routing tables, internet gateway, EC2 instance and connecting them all together. Therefore Deploy a Webserver to AWS EC2 | Pulumi wasn’t enough for me.
Let’s start with the VPC, internet gateway, one subnet and routing table. Here, it’s important to say that you don't need to define 10.0.0.0/24
with local
target because it's created automatically. Actually, it throws an error if you try to do so.
This is not mentioned in RouteTable | Pulumi docs. But, fortunately, it’s based on Terraform aws_route_table where you can find the explanation “Note that the default route, mapping the VPC’s CIDR block to ‘local’, is created implicitly and cannot be specified.”
import * as aws from '@pulumi/aws';
const vpc = new aws.ec2.Vpc('my-vpc', {
cidrBlock: '10.0.0.0/16'
});
const internetGateway = new aws.ec2.InternetGateway('my-internetgateway', {
vpcId: vpc.id
});
const subnet = new aws.ec2.Subnet('my-public-subnet', {
cidrBlock: '10.0.0.0/24',
tags: {
Name: 'Main'
},
vpcId: vpc.id
});
// Allow access to any address outside VPC except for addresses inside VPC.
const publicRouteTable = new aws.ec2.RouteTable('my-route-table', {
routes: [
{
cidrBlock: '0.0.0.0/0',
gatewayId: internetGateway.id
}
],
vpcId: vpc.id
});
AWS creates a default routing table when creating VPC, but it permits everything. We needed to create a routing table to allow access from the internet and associate it with subnet here:
const publicRouteTableAssociation = new aws.ec2.RouteTableAssociation(
'my-route-table-association',
{
routeTableId: publicRouteTable.id,
subnetId: subnet.id
}
);
Now, let's create a security group allowing inbound SSH access to EC2 instance and security group allowing outbound internet access to EC2 instance. For the definition of the second one, the SecurityGroup | Pulumi documentation says: “If you select a protocol of -1
(semantically equivalent to all
, which is not a valid value here), you must specify a from_port
and to_port
equal to 0
. If not icmp
, tcp
, udp
, or -1
”.
const sshGroup = new aws.ec2.SecurityGroup('ssh-access', {
ingress: [
{ protocol: 'tcp', fromPort: 22, toPort: 22, cidrBlocks: ['0.0.0.0/0'] }
],
vpcId: vpc.id
});
const internetGroup = new aws.ec2.SecurityGroup('internet-access', {
egress: [
{ protocol: '-1', fromPort: 0, toPort: 0, cidrBlocks: ['0.0.0.0/0'] }
],
vpcId: vpc.id
});
Pulumi can also upload a public key for SSH access to EC2 instance for you. I don't like to hardcode such things so I added dotenv
package and set the key in .env
file, which is not versioned. This is the beauty of using TypeScript because you can use it in a way you're used to in your usual development process. The format of the key and other details are described here KeyPair | Pulumi.
// Add import at the start of the file
import * as dotenv from 'dotenv';
...
dotenv.config();
...
const publicKey = process.env.SSH_KEY;
if (!publicKey) throw new Error('Missing public key.');
const key = new aws.ec2.KeyPair('key', { publicKey });
Finally, we define our EC2 server. You can see I didn't use aws.getAmi(...)
method, but I set amiId
I found in Ubuntu Amazon EC2 AMI Finder directly. Also, if you don’t have EC2-Classic VPC, you need to assign groups with vpcSecurityGroupIds
attribute. The code in the official documentation example shows usage of securityGroups: [group.name]
, but it has been deprecated and throws an error.
const server = new aws.ec2.Instance('my-server', {
instanceType: size,
ami: amiId,
subnetId: subnet.id,
vpcSecurityGroupIds: [sshGroup.id, internetGroup.id],
keyName: key.keyName
});
One more thing. This EC2 instance we created doesn't have a public IPv4 assigned by default. We can set associatePublicIpAddress: true
in InstanceArgs
definition or define Elastic IP. The benefit of Elastic IP is that it stays the same even if you restart the machine.
const eip = new aws.ec2.Eip(‘my-server-eip', {
instance: server.id,
vpc: true
});
Now, you can run pulumi up
and Pulumi creates everything in AWS for you. No clicking, just the code. You can access the machine by ssh -i ~/.ssh/id_rsa ubuntu@<ELASTIC_IP>
.
There aren’t as many tutorials and examples on the Pulumi docs site. You would need to go through the API reference or look into Github (yes, it’s open-source). I’m adding some links I found useful:
- Pulumi AWS API Reference
- Pulumi AWS EC2 API Reference
- Pulumi AWS Node.js SDK on GitHub
-
Pulumi examples on Github - Examples in all supported languages. Particularly the folder
aws-ts-resources
helped me with my effort. - Provider: AWS - Terraform by HashiCorp - You can also look into Terraform docs as I did with RouteTable specification.
Posted on April 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.