ScaleSec Blog

Well-Architected Infrastructure on AWS Using Pulumi Crosswalk & TypeScript

Written by Eric Evans | Dec 18, 2019 8:00:00 AM

Well-Architected Infrastructure on AWS Using Pulumi Crosswalk and TypeScript

Using Infrastructure as Code (IaC) to define your infrastructure helps to avoid deployment inconsistencies, increase developer productivity, and lower costs. We at ScaleSec love to use IaC to secure the cloud — check out our blog posts on using Terraform for AWS Organizations Service Control Policies and Google Cloud VPC service controls. In this post, we will take a look at Pulumi and how we can develop well-architected infrastructure as code in Amazon Web Services (AWS) using TypeScript.

About Well-Architected Infrastructure

Well-Architected infrastructure is designed to exhibit several key qualities, which are to be secure, reliable, high-performing, cost-effective, and operationally excellent. AWS has provided extensive documentation on the Well-Architected Framework which encourages the infrastructure best practices enumerated above and explored in this blog post. Having well-architected infrastructure ensures that a holistic approach has been taken in the architecture and engineering of cloud workloads.

The Five Pillars of the AWS Well-Architected Framework

Pulumi allows IaC to be defined in a general-purpose programming language, which unlocks a number of capabilities for declaring infrastructure such as loops, functions, classes, and package management. It does so by using a desired state model for managing infrastructure, very similar to the behavior of other IaC solutions such as Terraform and AWS CloudFormation. It works on all major public clouds and can even adapt Terraform providers.

How Pulumi Crosswalk Helps

Pulumi Crosswalk is available for AWS and Kubernetes. Crosswalk is a collection of best practices which makes common infrastructure tasks easier and more secure. Therefore, it helps to provide guardrails for applications by building in security standards in its default configurations.

Pulumi Crosswalk for Amazon Web Services (AWS)

Examples of Well-Architected Infrastructure as Code

The Well-Architected Framework has five pillars, and below are an example of how infrastructure as code can be used to achieve goals for each pillar of the framework.

Operational Excellence

Operational Excellence

Pulumi Crosswalk for Kubernetes was recently released and has compatibility for the Kubernetes platforms in all three major public clouds: AWS, GCP, and Azure. Since this particular article is focused on best practices within AWS, Elastic Kubernetes Service (EKS) will be showcased in the Operational Excellence example although support also exists for Google Kubernetes Engine (GKE) and Azure Kubernetes Service (AKS).

Performing operations as code is important for the Operational Excellence of an organization. This means you can treat infrastructure, applications, and the operations associated with them with code and can use similar tooling and methods to architect and engineer solutions. Pulumi helps to facilitate this by allowing infrastructure, application code, and deployments to be expressed in a common programming language, for example TypeScript.

To begin with, we will start with creating a repository for the Docker image we want to build and eventually deploy to a Kubernetes cluster. Other than importing the necessary library, creating a new Elastic Container Registry (ECR) is a one-liner in the default index.ts:

import * as awsx from "@pulumi/awsx";

const repo = new 
awsx.ecr.Repository("repository");

Executing a pulumi up will create a repository with a default lifecycle policy that will only keep at most one untagged image around, a best practice for Docker repositories. Now let’s create a very simple Dockerfile to build in the same directory as our Pulumi code:

FROM busybox:latest
CMD ["date"]

Now let’s add some code to our index.ts file to build the Docker image:

const dockerImage = 
repo.buildAndPushImage(`./`);

And run a pulumi up which executes a docker build behind the scenes and pushes this newly created image into the repository. With simply a few lines of code, we now have a way to build and store a docker image an ECR repository. Now let’s set up our EKS control plane and export the generated kubeconfig with a new import and couple lines of:

import * as eks from "@pulumi/eks";

const cluster = new eks.Cluster("cluster");
export const kubeconfig = cluster.kubeconfig;

This will create an EKS cluster with good defaults — such as using two t2.medium sized nodes with AWS IAM Authenticator to leverage IAM for secure access to your cluster. In addition, the generated output can be used with the kubectl CLI. Now that we have a cluster and a docker repository with our built image stored, lets create a Kubernetes deployment by adding this to our TypeScript file:

import * as k8s from "@pulumi/kubernetes";

// Create a k8s provider
const provider = new k8s.Provider("provider", {
    kubeconfig: cluster.kubeconfig,
});

// Create a Deployment of the built container
const appLabels = { app: dockerImage };
const appDeployment = new k8s.apps.v1.Deployment("app", {
    spec: {
        selector: { matchLabels: appLabels },
        replicas: 1,
        template: {
            metadata: { labels: appLabels },
            spec: {
                containers: [{
                    name: dockerImage,
                    image: dockerImage,
                    ports: [{name: "http", containerPort: 80}],
                }],
            }
        },
    }
}, { provider: provider });

And with a pulumi up, we now have a busybox running on our Kubernetes cluster. What this example demonstrates is that we can express our infrastructure (ECR and Kubernetes cluster), build process (docker build/docker push) and deployment (via a Kubernetes Deployment) in a concise and uniform fashion. This helps to facilitate operational access by using robust infrastructure as code.

Security

Security

AWS Identity and Access Management (IAM) enables cloud practitioners to access AWS services and resources securely. Using IAM, one can allow and deny access to AWS resources using permissions. Pulumi, by leveraging general purpose programming language capabilities, can simplify generating and working with IAM’s policy language (JSON). A great example of this is taking advantage of TypeScript’s strong typing to check for mistyped attributes at compile time to prevent errors. This means misconfigurations can be found when code compiles during pulumi up rather than waiting for an AWS API error, or worse — discovering the error after deployment into an environment leading to security misconfigurations in deployed IAM policies.

This can help to save time, add an additional developer-friendly check for coding infrastructure inside the IDE (shift security left), and ensure IAM roles, users, groups, and policies are managed in a secure fashion. Here’s an example of using the PolicyDocument interface to add strong checking to an IAM STS trust policy for EC2:

import * as aws from "@pulumi/aws";

const policy: aws.iam.PolicyDocument = {
    Version: "2012-10-17",
    Statement: [
        {
            Action: "sts:AssumeRole",
            Principal: {
               Service: "ec2.amazonaws.com"
            },
            Effect: "Allow",
            Sid: "",
        },
    ],
};

Reliability

Reliability

Being reliable in the cloud ensures that your workloads have enough resources available to them and that solutions contain elasticity to handle fluctuations in resource usage. Reliability in the cloud ensures that a system can reduce, mitigate, and recover from issues as they happen which results in increased uptime for the business and productivity for developers.

There are many ways to achieve reliability when architecting for the cloud. One of the major technologies that comes to mind is the use of containers. Containers decouple applications from the operating system and infrastructure they run on, providing an acceptable answer to the old saying “it works on my machine.”

For containers to be most effective in the cloud, a container orchestrator is necessary. Container orchestrators help with stages of the container lifecycle which include provisioning, scheduling, and scaling/allocating resources. Amazon Elastic Container Service (ECS) helps with this by providing a way to do all of the above and more, even run containers in a serverless fashion via AWS Fargate. ECS integrates well with other AWS services, such as Elastic Load Balancing (ELB) which provides additional reliability in the cloud by automatically distributing application traffic across multiple targets with little management overhead.

Pulumi Crosswalk has first class support for both ECS and ELB, which will be demonstrated in the next example using nginx. An application load balancer (ALB) is simple to provision by using the code below:

import * as awsx from "@pulumi/awsx";

*// Create a load balancer that listens on port 
80 (HTTP) *const alb = new
awsx.lb.ApplicationListener("nginx", { port: 80
});

The lb that we defined above can be referenced in an AWS ECS cluster using Fargate. In the following example, we will run three instances of nginx in our serverless Docker cluster.

const service = new 
awsx.ecs.FargateService("nginx", { taskDefinitionArgs: { containers: { nginx: { image: "nginx", portMappings: [ alb ], }, }, }, desiredCount: 3, });

At the end of our file, we will export the load balancer’s hostname so that we can refer to it to visit the service.

export const url = alb.endpoint.hostname;

After running a pulumi up we can access the nginx service that’s running by using Pulumi’s stack output in the command line: curl http://$(pulumi stack output url). More examples of how to tune an ECS cluster in Pulumi can be found in their documentation. We now have a cluster that is backed by AWS Fargate, giving AWS the responsibility of managing the virtual machines that our containers will run on, and overall contributing to the reliability of the workload.

Performance Efficiency

Performance Efficiency

An important part of maintaining the performance of a cloud environment is being able to monitor it. There is a whole section regarding monitoring in the AWS Well Architected framework whitepaper regarding the Performance Efficiency Pillar.

Pulumi Crosswalk for CloudWatch allows for automatic secure by design defaults including automatic log groups (if one is not specified, it will be created automatically) and a reasonable retention policy. Adding Cloudwatch logging from a container is relatively easy by building on our ECS Fargate example above. Here’s the complete code for that:

import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";

const alb = new awsx.lb.ApplicationListener("nginx", { port: 80 
}); const logGroup = new
aws.cloudwatch.LogGroup("logs"); const service = new
awsx.ecs.FargateService("nginx", { taskDefinitionArgs: { containers: { nginx: { image: "nginx", portMappings: [ alb ], }, }, }, desiredCount: 3, }); export const url*** ***= alb.endpoint.hostname;

A really neat feature is that using the pulumi logs -f command after this code has deployed, it is possible to tail these logs in the command line in real time. If we do that with the code above, incoming traffic to the nginx server, including load balancer health checks and visits to the default page, can be inspected (example output below):

2019-11-27T20:18:48.982-05:00[                 
nginx-0bbf916] 172.31.15.119 - -
[28/Nov/2019:01:18:48 +0000] "GET / HTTP/1.1"
200 612 "-" "ELB-HealthChecker/2.0" "-" 2019-11-27T20:18:50.418-05:00[
nginx-0bbf916] 172.31.15.119 - -
[28/Nov/2019:01:18:50 +0000]
"GET / HTTP/1.1" 200 612 "-" "ELB-HealthChecker/2.0" "-" 2019-11-27T20:18:52.544-05:00[
nginx-0bbf916] 172.31.15.119 - -
[28/Nov/2019:01:18:52 +0000] "GET / HTTP/1.1"
304 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS
X 10_15_1) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/78.0.3904.108 Safari/537.36"

"127.0.0.1" 2019-11-27T20:18:53.405-05:00[
nginx-0bbf916] 172.31.47.29 - -
[28/Nov/2019:01:18:53 +0000] "GET / HTTP/1.1"
200 612 "-" "ELB-HealthChecker/2.0" "-"

In this example we show how Pulumi Crosswalk can help us monitor the code that we deploy in a seamless fashion, which can also be applied to situations in which performance monitoring or troubleshooting is needed.

Cost Optimization

Cost Optimization

One part of cost optimization in the cloud is taking advantage of the elasticity that is provided to scale down or turn off services that are not being used. This can be done in AWS by taking advantage of it’s autoscaling features and automatically scaling based on a schedule. For instance, if we want to scale up instances on Saturday, and then scale down on Tuesday, this can be achieved easily with Pulumi Crosswalk:

import * as awsx from "@pulumi/awsx";

const asg = new awsx.autoscaling.AutoScalingGroup("asg", {
    templateParameters: { minSize: 1 },
    launchConfigurationArgs: { instanceType: "t3.nano" },
});

asg.scaleOnSchedule("scaleUpOnFriday", {
    desiredCapacity: 3,
    recurrence: { dayOfWeek: "Saturday" },
});

asg.scaleOnSchedule("scaleDownOnTuesday", {
    desiredCapacity: 1,
    recurrence: { dayOfWeek: "Tuesday" },
});

Setting up schedules is easy to do using this method and helps to achieve the goal of adjusting capacity of workloads in AWS in order to maintain capacity at the lowest possible cost.

Conclusion

We have demonstrated how infrastructure as code can be used to achieve well-architected workloads in AWS. We have done so by choosing Pulumi Crosswalk as the solution and having examples for each pillar of the well-architected framework. Obviously to achieve the goals outlined in the framework takes a lot more than what was demonstrated in this post, however this article demonstrates how using built-in best practices and additional guardrails in infrastructure as code can help achieve these goals in the cloud. Let us help you get there!