Infrastructure as Code in TypeScript
Tags: typescript, pulumi, aws
Published: 2020-04-06 19:50:19

---
title: Infrastructure as Code in TypeScript
published: true
description: 
tags: typescript, pulumi, aws
---

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](https://www.pulumi.com/docs/get-started/aws/) 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](https://www.pulumi.com/docs/tutorials/aws/ec2-webserver/) 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](https://www.pulumi.com/docs/reference/pkg/aws/ec2/routetable/) docs. But, fortunately, it’s based on Terraform [aws_route_table](https://github.com/terraform-providers/terraform-provider-aws/blob/master/website/docs/r/route_table.html.markdown) 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.”

```typescript
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:

```typescript
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](https://www.pulumi.com/docs/reference/pkg/aws/ec2/securitygroup/) 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`”.

```typescript
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](https://www.pulumi.com/docs/reference/pkg/aws/ec2/keypair/).

```typescript
// 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](https://cloud-images.ubuntu.com/locator/ec2/) directly. Also, if you don’t have [EC2-Classic](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-classic-platform.html) 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.

```typescript
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.

```typescript
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](https://www.pulumi.com/docs/reference/pkg/aws/)
* [Pulumi AWS EC2 API Reference](https://www.pulumi.com/docs/reference/pkg/aws/ec2/)
* [Pulumi AWS Node.js SDK on GitHub](https://github.com/pulumi/pulumi-aws/tree/master/sdk/nodejs)
* [Pulumi examples on Github](https://github.com/pulumi/examples) - Examples in all supported languages. Particularly the folder `aws-ts-resources` helped me with my effort.
* [Provider: AWS - Terraform by HashiCorp](https://www.terraform.io/docs/providers/aws/index.html) - You can also look into Terraform docs as I did with RouteTable specification.