2019-10-17 14:53:36 +03:00
---
2019-12-18 09:59:20 -08:00
title: "Architect AWS Application Infrastructure"
2019-12-03 19:06:35 -08:00
title_tag: "Architect AWS Application Infrastructure with Stack References"
2019-10-17 14:53:36 +03:00
date: "2019-10-17"
meta_desc: "How to architect your AWS infrastructure to optimize team collaboration with Pulumi Stack References"
meta_image: "application-architecture.png"
authors: ["paul-stack"]
Filter blog tags to Top 40, and add back some metadata (#350)
* Only show the top 40 blog tags
In https://github.com/pulumi/pulumi-hugo/pull/215, I had suggested
that instead of physically deleting tags we didn't want to show, we
compute it algorithmically, by only showing the "Top N" tags. This
commit introduces said functionality.
This has a few advantages:
* Preserves old metadata (the authors added the tags because they
felt they were meaningful and captured information about the posts).
* Enables us to surface those tags differently in the future (who
knows, maybe someday we'll want to run a "spinnaker" campaign).
* Notably, also keeps the tag index pages, which Google has indexed.
* Enables us to add a "View More ..." link at the bottom of the
page if folks want to see the entire list.
* Perhaps most importantly, protects against future bloat. For
example, since this tag cleanup happened, we have added top-level
tags for "aliases", "app-runner", "iam", "open-source", and
"refactoring", each of which has only a single post.
I chose 40 as the N in Top N, because that's how many we show today.
I could see an argument for filtering this based on post count
instead (e.g., only those with >3 posts).
* Add back some tags
Now that we filter out unpopular tags, we can add back some of the
ones previously removed.
2021-06-21 17:31:35 -07:00
tags: ["aws", "stack-reference"]
2019-10-17 14:53:36 +03:00
---
In this post, we will talk about the best way to architect your Pulumi applications. We are going to build out the following
infrastructure in AWS:
* AWS Fargate service that does not serve traffic directly
* AWS ALB as the entry point to the Fargate Service
* AWS RDS Instance that is stored in a separate network from the Application and does not service traffic directly from the internet
2019-12-03 19:06:35 -08:00
To do this, we are going to split the infrastructure into two AWS VPCs. One VPC will be used for the application based resources
2019-10-17 14:53:36 +03:00
(e.g Fargate Service, ALB) and the other VPC will be for the database resources. The VPCs will be peered to allow communication
between them so that the application can communicate with the database securely.

## Layer it up
When we think of infrastructure, we tend to think of layers. A layer is a logical set of components / resources that can be
grouped together. In our case, we can think of the networking as a layer upon which our database and application can be built.
2019-12-03 19:06:35 -08:00
For the purposes of this post, an infrastructure layer will be represented as a project in Pulumi. So let's break our application
2019-10-17 14:53:36 +03:00
into separate Pulumi projects:
2019-10-17 11:18:02 -07:00
```bash
$ tree -d -L 1
.
├── application
├── database
└── networking
```
2019-12-03 19:06:35 -08:00
This means all of the work required for the networking components can be in the network project. We can use
2023-05-15 15:25:28 -07:00
[Stack References ](/docs/concepts/stack#stackreferences ) in Pulumi
2019-10-17 14:53:36 +03:00
to expose any required information from the networking layer, to the database and application layers.
Let's create the networking project.
### Building our Networking Project
2019-10-17 11:18:02 -07:00
```bash
$ cd networking & & pulumi new aws-typescript
$ pulumi config set aws:region us-east-2
```
2019-12-03 19:06:35 -08:00
This gives us the shell of our project we need to create our resources. In thinking of how we build our networking resources,
we will create two copies of each resource due to separate VPCs for application and database. Of course, we can use a loop to do
this, but the more elegant way would be to create a class that we could pass parameters into to make life easy. We want an
2019-10-17 12:04:37 -07:00
API as follows:
2019-10-17 14:53:36 +03:00
2019-10-17 11:18:02 -07:00
```typescript
const availabilityZones = aws.getAvailabilityZones({
state: "available",
});
2019-10-17 14:53:36 +03:00
2019-10-17 11:18:02 -07:00
const myVpc = new Vpc("my-vpc", {
description,
baseTags,
baseCidr,
availabilityZoneNames,
enableFlowLogs,
});
```
2019-12-03 19:06:35 -08:00
2019-10-17 12:04:37 -07:00
We can see the power of Pulumi when we build our infrastructure with TypeScript. We create a TypeScript
2019-12-03 19:06:35 -08:00
class that [extends ](https://www.typescriptlang.org/docs/handbook/classes.html ) Pulumi ComponentResource. ComponentResource
is a resource that aggregates one or more other child resources into a higher level abstraction. The component resource
2019-10-17 14:53:36 +03:00
itself is a resource, but does not require custom create, read, update and delete operations for provisioning.
The outline of the class will look as follows:
2019-10-17 11:18:02 -07:00
```typescript
export class Vpc extends ComponentResource {
// ...
}
```
2019-12-03 19:06:35 -08:00
2019-10-17 12:04:37 -07:00
We want to pass a name and some custom parameters to the class, so we create an interface for the parameters:
2019-10-17 14:53:36 +03:00
2019-10-17 11:18:02 -07:00
```typescript
export interface VpcArgs {
// ...
}
```
2019-12-03 19:06:35 -08:00
2019-10-17 12:04:37 -07:00
We update our class with a constructor that allows us to create a new instance of the VPC class:
2019-10-17 14:53:36 +03:00
2019-10-17 11:18:02 -07:00
```typescript
export class Vpc extends ComponentResource {
constructor(name: string, args: VpcArgs, opts?: ComponentResourceOptions) {
super("vpc", name, {}, opts);
2019-12-03 19:06:35 -08:00
// ... resources here ...
2019-10-17 14:53:36 +03:00
}
2019-10-17 11:18:02 -07:00
}
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
After we implement all of the resources needed for our VPC, our Pulumi application looks like the following:
2019-10-17 11:18:02 -07:00
```typescript
async function main() {
const config = new Config();
const azCount = config.getNumber("azCount") || 2;
const availabilityZones = aws.getAvailabilityZones({
state: "available",
});
const appVpc = new Vpc("app-vpc", {
baseCidr: "172.28.0.0/16",
availabilityZoneNames: availabilityZones.names.slice(0, azCount),
enableFlowLogs: true,
});
const dataVpc = new Vpc("data-vpc", {
baseCidr: "172.18.0.0/16",
availabilityZoneNames: availabilityZones.names.slice(0, azCount),
enableFlowLogs: true,
});
return {
appVpcId: appVpc.vpcId(),
dataVpcId: dataVpc.vpcId(),
2019-10-17 14:53:36 +03:00
}
2019-10-17 11:18:02 -07:00
}
module.exports = main();
```
2019-12-03 19:06:35 -08:00
2019-10-17 12:04:37 -07:00
This application will create two instances of the VPC class containing all of the resources needed for a VPC. This
2019-10-17 14:53:36 +03:00
reusable `VPC` class` is a more efficient way of building out infrastructure than to simply loop over the resources. We can
take advantage of relating the resources to the correct parent to build out graph.
We take it one step further, and be able to peer the VPCs. TypeScript, again, gives us the ability to do this in a
2022-03-10 11:03:11 -08:00
concise manner by using a [member function ](https://www.typescriptlang.org/docs/handbook/classes.html#public-private-and-protected-modifiers ). The member function of the VPC class looks as follows:
2019-10-17 14:53:36 +03:00
2019-10-17 11:18:02 -07:00
```typescript
public configurePeering(args: PeerToArgs) {
// ...
}
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
This gives us the ability to peer the VPCs:
2019-10-17 11:18:02 -07:00
```typescript
appVpc.configurePeering({
peerVpc: dataVpc,
nameTag: `${baseTags.ManagedBy} Peer App to Data` ,
routeSubnets: "private",
});
```
2019-10-17 14:53:36 +03:00
2019-12-03 19:06:35 -08:00
After creating a similar member function for the peering security group, we instruct Pulumi to create the resources
2019-10-17 14:53:36 +03:00
by running the command `pulumi up` . This displays a preview of changes first and the list of AWS resources it will create:
2019-10-17 11:18:02 -07:00
```bash
$ pulumi up
```
2019-12-03 19:06:35 -08:00
2019-10-17 11:18:02 -07:00
After being prompted and selecting "yes", your deployment will begin. After completion, you will see:
2019-12-03 19:06:35 -08:00
2019-10-17 11:18:02 -07:00
```bash
...
+ │ ├─ aws:ec2:VpcEndpoint data-vpc-s3-endpoint created
+ │ └─ aws:ec2:VpcEndpoint data-vpc-dynamodb-endpoint created
+ └─ aws:ec2:SecurityGroup data-vpc-app-vpc-sg created
Outputs:
appVpcId : "vpc-07420f4b22cce694a"
appVpcPrivateSubnetIds : [
[0]: "subnet-07ebb7bb1894f76ea"
[1]: "subnet-0cc6c64ffce7fa74a"
]
appVpcPublicSubnetIds : [
[0]: "subnet-07eec9d474411d47c"
[1]: "subnet-093c9e201b5bb3975"
]
dataVpcId : "vpc-04d28ded952454899"
dataVpcPrivateSubnetIds: [
[0]: "subnet-048e68b5d12d6189d"
[1]: "subnet-088fbb5acf3d0a58c"
]
dataVpcPublicSubnetIds : [
[0]: "subnet-0711e414ccc7458a3"
[1]: "subnet-0b8d54dfec34b286b"
]
peeredSecurityGroupId : "sg-08b58260a840a971b"
Resources:
+ 61 created
Duration: 3m8s
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
### Building our Database Project
2019-12-03 19:06:35 -08:00
2019-10-17 11:18:02 -07:00
```bash
$ cd database & & pulumi new aws-typescript
$ pulumi config set aws:region us-east-2
```
2019-10-17 14:53:36 +03:00
Our database project follows the same principals as the networking project - it has a RDS class that extends the
2019-12-03 19:06:35 -08:00
Pulumi ComponentResource.
2019-10-17 14:53:36 +03:00
2019-10-17 11:18:02 -07:00
```typescript
export interface RdsArgs {
// ...
}
export class RdsInstance extends ComponentResource {
constructor(name: string, args: RdsArgs, opts?: ComponentResourceOptions) {
super("db", name, {}, opts);
// ... resources here ...
2019-10-17 14:53:36 +03:00
}
2019-10-17 11:18:02 -07:00
}
```
2019-10-17 14:53:36 +03:00
This project needs other configuration options set:
2019-10-17 11:18:02 -07:00
```bash
pulumi config set database-layer:dbUsername MyDBUser
pulumi config set database-layer:dbName initialDb
pulumi config set database-layer:dbPassword MyDBPassword1234! --secret
```
2019-10-17 14:53:36 +03:00
2019-12-03 19:06:35 -08:00
The database project needs to access the details of the resources creating in the networking layer. To do this, we
use a Pulumi StackReference. We pass the correct stack name that the database project needs to talk to,
so we can pass that as a configuration option. A stack reference in Pulumi will follow the structure `org/project/stack` .
2019-10-17 14:53:36 +03:00
In our case it will be `stack72/networking-layer/dev` and we can pass that as a configuration option to the database project:
2019-10-17 11:18:02 -07:00
```bash
$ pulumi config set networkingStack stack72/networking-layer/dev
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
Inside our database project, we can access any of the exported values from the networking project. The first thing we need
access to is the database private subnet Ids. To access those subnetIds, we would do the following:
2019-10-17 11:18:02 -07:00
```typescript
import {Config, StackReference} from "@pulumi/pulumi ";
const config = new Config();
const networkingStack = new StackReference(config.require("networkingStack"))
2019-12-03 19:06:35 -08:00
const subnetIds = networkingStack.getOutput("dataVpcPrivateSubnetIds")
2019-10-17 11:18:02 -07:00
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
**Please Note:** We need to pass the exact name of the output as specified in the networking stack!
Our database project looks like this:
2019-10-17 11:18:02 -07:00
```typescript
const rds = new RdsInstance("db-instance", {
description: `${baseTags.Project} DB Instance` ,
baseTags: baseTags,
subnetIds: networkingStack.getOutput("dataVpcPrivateSubnetIds"),
username: dbUsername,
password: dbPassword,
initalDbName: dbName,
allocatedStorage: 40,
engineVersion: "11.4",
instanceClass: aws.rds.InstanceTypes.R3_Large,
storageType: "gp2",
finalSnapshotIdentifier: finalSnapshotIdentifier,
sendEnhancedLogsToCloudwatch: true,
monitoringInterval: 10,
securityGroupIds: [networkingStack.getOutput("peeredSecurityGroupId")],
});
```
2019-12-03 19:06:35 -08:00
We can then instruct Pulumi to create our resources with a `pulumi up` command, as before. This will show you a preview
2019-10-17 14:53:36 +03:00
of changes first and the list of AWS resources it will create:
2019-12-03 19:06:35 -08:00
2019-10-17 11:18:02 -07:00
```bash
$ pulumi up
```
2019-12-03 19:06:35 -08:00
2019-10-17 11:18:02 -07:00
After being prompted and selecting "yes", your deployment will begin. After completion, you will see:
```bash
Updating (dev):
Type Name Status
+ pulumi:pulumi:Stack database-layer-dev created
+ ├─ db db-instance created
+ │ ├─ aws:rds:SubnetGroup db-instance-subnet-group created
+ │ ├─ aws:iam:Role db-instance-enhanced-monitoring-role created
+ │ ├─ aws:iam:RolePolicyAttachment enhanced-policy-attachment created
+ │ └─ aws:rds:Instance db-instance-rds created
+ └─ random:index:RandomString my-random-string created
Outputs:
dbAddress : "db-instance-rdsd86e208.cep6renxwgtk.us-west-2.rds.amazonaws.com"
dbEndpoint: "db-instance-rdsd86e208.cep6renxwgtk.us-west-2.rds.amazonaws.com:5432"
dbName : "initialDb"
dbPassword: "[secret]"
dbPort : "5432"
dbUsername: "MyDBUser"
Resources:
+ 7 created
Duration: 7m49s
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
### Building the Application Project
2019-10-17 11:18:02 -07:00
```bash
$ cd application & & pulumi new aws-typescript
$ pulumi config set aws:region us-east-2
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
It will come as no surprise to you that we will build our application project in the same manner as that of networking and
2019-12-03 19:06:35 -08:00
database. So we will have an `Application` class that extends Pulumi ComponentResource.
2019-10-17 14:53:36 +03:00
2019-10-17 11:18:02 -07:00
```typescript
export interface ApplicationArgs {
// ....
2019-12-03 19:06:35 -08:00
}
2019-10-17 11:18:02 -07:00
export class Application extends ComponentResource {
constructor(name: string, args: ApplicationArgs, opts?: ComponentResourceOptions) {
super("application", name, {}, opts);
// ... resources here ...
2019-12-03 19:06:35 -08:00
}
2019-10-17 11:18:02 -07:00
}
```
2019-12-03 19:06:35 -08:00
The application project differs from the database project in that it needs stack references to the networking *and* database
2019-10-17 14:53:36 +03:00
projects. Let's specify those via configuration as we did in the database project:
2019-12-03 19:06:35 -08:00
2019-10-17 11:18:02 -07:00
```bash
$ pulumi config set networkingStack stack72/networking-layer/dev
$ pulumi config set databaseStack stack72/database-layer/dev
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
We can then access details like, database endpoint, database user, application Vpc Id within our application project:
2019-10-17 11:18:02 -07:00
```typescript
import * as awsx from "@pulumi/awsx ";
import { Config, getStack, StackReference } from "@pulumi/pulumi ";
import {Application} from "./application";
const config = new Config();
const networkingStack = new StackReference(config.require("networkingStack"));
const databaseStack = new StackReference(config.require("databaseStack"));
const app = new Application("app", {
vpcId: networkingStack.getOutput("appVpcId"),
// ALB in public subnets
albSubnetIds: networkingStack.getOutput("appVpcPublicSubnetIds"),
// App resources in private subnets
appSubnetIds: networkingStack.getOutput("appVpcPrivateSubnetIds"),
appImage: awsx.ecs.Image.fromPath("app", "./src/backend"),
appPort: 80,
dbName: databaseStack.getOutput("dbName"),
dbUsername: databaseStack.getOutput("dbUsername"),
dbPassword: databaseStack.getOutput("dbPassword"),
dbPort: databaseStack.getOutput("dbPort"),
dbHost: databaseStack.getOutput("dbAddress"),
});
export const albAddress = app.albAddress();
```
2019-12-03 19:06:35 -08:00
Notice that we are taking advantage of the [Pulumi AWS Extensions ](https://github.com/pulumi/pulumi-awsx ).
Pulumi AWSX is a set of component wrappers around many AWS operations. In our case, we are taking advantage of being able
to create an AWS ECS container image from a known path to a [dockerfile ](https://docs.docker.com/engine/reference/builder/ ).
This is a concise API that removes us from the need to run `docker build` as a separate process. It will happen as part of
our Pulumi application.
We can, again, instruct Pulumi to create our resources for us by issuing the `pulumi up` command. In the interactive output,
you will see how Pulumi interacts with docker and also see how it reads both of the stack references we told it to read
2019-10-17 14:53:36 +03:00
information from. It will also show you a preview of the changes and the list of AWS resources it will create:
2019-12-03 19:06:35 -08:00
2019-10-17 11:18:02 -07:00
```bash
$ pulumi up
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
### Testing our application
2019-12-03 19:06:35 -08:00
The application in this repository has a number of endpoints that we can use to test that the AWS ECS Fargate service is
working as expected. We can test them either from a browser or from the command line. We can access the service using the
2019-10-17 14:53:36 +03:00
address of the AWS ALB that was created to serve traffic to the application. Let's test the endpoints from the CLI:
2019-10-17 11:18:02 -07:00
```bash
$ curl $(pulumi stack output albAddress)
{
"hash": "59846138362875439341244157139878939483",
"message": "hello world",
"postgres": "please use the /postgres endpoint"
}
```
```bash
$ curl $(pulumi stack output albAddress)/postgres
{
"hash": "59846138362875439341244157139878939483",
"postgres": [
[
"PostgreSQL 11.4 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 4.8.3 20140911 (Red Hat 4.8.3-9), 64-bit"
]
]
}
```
2019-12-03 19:06:35 -08:00
2019-10-17 11:18:02 -07:00
```bash
$ curl $(pulumi stack output albAddress)/internet
I CAN connect to google.com%
```
```bash
$ curl $(pulumi stack output albAddress)/public-ip
< p > my external IP is: 52.24.188.63 (source: jsonip.com) < / p > %
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
Our application is up and running and we can connect to it!
## Cleaning up
To cleanup the resources we created, we need to go into each project directory and run the command
2019-10-17 11:18:02 -07:00
```bash
$ pulumi destroy --yes & & pulumi stack rm
```
2019-12-03 19:06:35 -08:00
2019-10-17 14:53:36 +03:00
Once the resources have been deleted, then we can follow the interactive prompt to delete the stack.
## Wrapping Up
In this post, we have discussed how we would break our infrastructure into logically grouped projects and how we share
2019-12-03 19:06:35 -08:00
information between these projects. You can find a runable version of this infrastructure in our
2021-10-12 10:47:58 -07:00
[examples ](https://github.com/pulumi/examples/tree/master/aws-ts-stackreference-architecture ) repository.
2019-10-17 14:53:36 +03:00
2019-12-03 19:06:35 -08:00
This demonstrates the suggested way of architecting Pulumi applications both for operability and collaboration.
2019-10-17 14:53:36 +03:00
2020-01-16 11:46:15 -05:00
You can try this code today get started with Pulumi today at [https://www.pulumi.com ](/ ) and
2023-05-15 15:25:28 -07:00
following the [Getting Started ](/docs/clouds/aws/get-started/ ) for managing AWS resources with Pulumi.