a personal blog
2518 words
13 minutes
Infra as Code 101

Table of contents#

Background#

How is this relevant to me?#

In your software dev journey, you might find yourself using cloud services such as databases on Google Cloud or API endpoints on AWS. One way of creating and managing those services is to simply click through the available UI console or use some CLI commands.

However, as the number of services increases, and as you need to manage multiple environments (dev, test, prod), this manual approach becomes error-prone and time-consuming.

For example, imagine having to set up 10 different API endpoints, some of them with a specific configuration, others only used for testing purposes, and all of them needing to be deployed to different regions. Doing this manually and keeping track of all the changes would be a challenge.

Boss giving intern instructions

Infrastructure as Code is a relatively new paradigm that mitigates these issues.

Why is IaC better?#

IaC has several benefits over manual infrastructure management:

  • Less errors due to manual interventions by automating the provisioning, configuration, and management of infrastructure.
  • More consistency across environments and reduces configuration drift.
  • Easier rollbacks by versioning infrastructure configurations in source control.
  • Faster deployments by automating repetitive tasks.
  • Faster recovery from failures by allowing infrastructure to be rebuilt from code in a consistent manner.
  • Reproducibility between development, testing, and production environments.

What you need to know#

Infrastructure as Code (IaC) refers to the practice of managing resources by declaring them in code format, rather than manually using a UI console or a CLI. This concept relies on two key practices:

1. Idempotency is a property used to describe operations that can be run multiple times without changing the end result. For example, installing a package on a server is idempotent because running the installation command multiple times will not change the state of the server (the dependency list remains unchanged after the 1st installation).

2. Declarative syntax specifies the desired end state without explicitly listing steps to achieve it, for example: create a cake with 3 layers, chocolate frosting, and sprinkles. In contrast, imperative syntax specifies the exact steps to achieve the desired state, for example: mix flour, sugar, and eggs in a bowl, then bake at 350°F for 30 minutes, then add frosting and sprinkles.

Note: Most IaC code typically uses declarative syntax, but it is possible to use imperative syntax. This can be useful in complex infrastructure deployments, or when the order of events is critical.

Good to know#

TypeScript#

TypeScript is a superset of JavaScript that adds static types to the language. This means that you can define the types of your variables, function parameters, and return values. Most code editors provide autocomplete and type hints based on the types you’ve defined, so you can catch errors before running your code.

// JavaScript
function add(a, b) {
  return a + b;
}

// TypeScript
function add(a: number, b: number): number {
  return a + b;
}

TypeScript has become very popular for web development, serverless, and cloud development because it catches errors early and provides better documentation for your code. It is compiled into JavaScript before running, so it can run on any JavaScript runtime.

Serverless computing#

IaC tools are often used to create serverless applications. Serverless computing is an execution model where the cloud provider is responsible for executing your piece of code by dynamically allocating the resources. This is in contrast to traditional computing, or “serverful”, where you have to manage the servers yourself.

Why would I want to create serverless apps? Making your apps serverless, as opposed to managing your own servers, is:

  • Less expensive because you only pay for the time your code is running
  • Less responsibility because you don’t have to worry about uptime, security updates, or maintaining the infrastructure
  • Less work because the provider will scale your resources up and down automatically

So you only need to worry about your actual code, not the infrastructure it runs on.

The cold start problem#

Serverless functions are small pieces of code that run in response to events. They are often used in serverless applications to handle HTTP requests, process data, or trigger other services. For example, you could have a serverless function that sends an email when a new user signs up for your app.

Each time you invoke a serverless function, the cloud provider will create a new container to run your code. This container contains the runtime environment, your code, and any dependencies. The first time you invoke a function, there will be a delay as the cloud provider wakes up the resources needed. This is known in IaC as the cold start problem.

Cold start problem illustration

One way to mitigate the cold start problem is to keep your functions warm. This means invoking your function every few minutes to keep the container alive. Another way is to optimize your functions to reduce the time it takes to start up. For example, you could minimize the number of dependencies or pre-load resources.

This article explains this concept much more in-depth and illustrates it with an experiment on AWS Lambda.

Case Study : Amazon CloudFormation#

AWS offers a service called CloudFormation that allows you to define your infrastructure in a JSON or YAML template. This template can be used to create, update, or delete resources in your AWS account.

However, those templates can easily become very verbose because you have to define every single resource and its properties. This also means it can be hard to maintain and reuse those templates. Lastly, the learning curve for CloudFormation can be really steep because you need to be constantly looking up the documentation to know what to write.

Example of a YAML CloudFormation template to create an S3 Bucket:

Resources:
  MyBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-bucket

AWS launched AWS Cloud Development Kit (CDK) in August 2018 to address the drawbacks of CloudFormation. CDK lets you use modern programming languages to create CloudFormation templates, instead of YAML or JSON. It supports TypeScript, JavaScript, Python, Java, and C#.

Example of a TypeScript CDK code to create an S3 Bucket:

import * as s3 from '@aws-cdk/aws-s3';

const bucket = new s3.Bucket(this, 'MyBucket', {
  bucketName: 'my-bucket'
});

From here, there are commands you can run in a terminal to generate the corresponding CloudFormation templates and deploy the resources to AWS. If you use an IaC framework like SST or Terraform, it will have its own set of commands to deploy and manage your resources.

Good patterns, bad patterns#

When working with IaC, there are some best practices you should follow to ensure your infrastructure is reliable, secure, scalable, and if you want to generally make your life easier:

Don’t Repeat Yourself (DRY), meaning turn duplicate code into reusable modules, functions, or custom libraries.
Use resource naming conventions which include the environment name, region, and a timestamp to avoid collision and ensure traceability (e.g. my-bucket-dev-us-east-1-2024-03-25).
Use immutable infrastructure, meaning you should not modify existing resources but instead create new resources with the desired configuration and then swap them out. This reduces the risk of configuration drift and makes it easier to roll back changes.
Keep things simple and try to only use the configurations you need, rather than using every feature available. This will make your infrastructure easier to follow and maintain.

On the other hand, some anti-patterns include:

🚫 Giving ”*” (open) access policies to resources could facilitate accidental deletion or modification of resources at best, and cause a security breach at worst. Instead, always apply the principle of least privilege. For example, rather than allowing all actions on an S3 Bucket, allow only s3:GetObject from a specific Lambda resource number on a specific S3 Bucket resource number.
🚫 Hardcoding credentials or API keys is a leading cause of security breaches. Instead, use environment variables or a secrets manager like AWS SSM Parameter Store.
🚫 Manually changing resources in the console or CLI can lead to configuration drift, meaning your IaC code and the actual resources in the cloud are no longer identical. You can do proofs of concept manually, but once you start using IaC in production you should avoid manual changes.
🚫 Deploying straight to production can cause outages for your users if there are unforeseen issues in your code. Always start by testing your changes locally or in a test/staging environment first.

Summary#

Do thisNot this
✅ Modularize your code (DRY)🚫 Rewrite the exact same function multiple times
✅ Use resource naming conventions like my-bucket-dev-us-east-1-2024-03-25🚫 Use undescriptive names like bucket1 or my-bucket
✅ New resources should overwrite existing ones🚫 Modify existing resources directly
✅ Use only the configurations you need🚫 Use every feature available
✅ Use least privilege access policies🚫 Give open access to resources
✅ Use environment variables or secrets managers🚫 Hardcode credentials or API keys
✅ Test changes in a staging environment first🚫 Deploy straight to production

The code below illustrates DRY, naming conventions, immutable infrastructure, and least privilege access policies:

function createBucket(scope: cdk.Construct, name: string): Bucket {
    const environment = process.env.ENVIRONMENT || 'dev';
    const region = process.env.REGION || 'us-east-1';
    const dateString = new Date().toISOString().split('T')[0]; // 'YYYY-MM-DD'
    const bucketName = `${name}-${environment}-${region}-${dateString}`;

    return new Bucket(scope, `${name}Bucket`, {
        bucketName: bucketName, 
        // 'my-bucket-dev-us-east-1-2024-03-25'
        removalPolicy: RemovalPolicy.DESTROY, 
        // clean up the bucket when the stack is deleted (immutable infrastructure)
        blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 
        // prevent public access (probably a good idea for most buckets)
    });
}

// Say we already have a Lambda function defined elsewhere that needs permissions to read from this bucket
const myBucket = createBucket(this, 'MyBucket');
myLambda.addEnvironment('MY_BUCKET_NAME', myBucket.bucketName);
myBucket.grantRead(myLambda);

Getting started with IaC#

Personal projects#

🟢 Simple: Setup AWS CDK using their quick-start guide, then try to deploy a simple Lambda function. The function could write to a DynamoDB table, or send an email using Amazon SES, or anything else you can think of.
🟡 Intermediate: Follow this Terraform tutorial to deploy a REST API using AWS Lambda and API Gateway. Make some calls to the API endpoints using curl or Postman.
🔴 Complex: Use the SST guide as a basis to deploy an end-to-end, full-stack web app on AWS. This includes a frontend, backend, database, authentication, CI/CD, monitoring, and more.

At your job#

✌️ If you are not using IaC yet, try automating a manual process. This could be setting up a CI/CD pipeline, creating a monitoring dashboard, or deploying a Lambda function that performs some business logic.
👌 If you are already using IaC, see if your existing infrastructure follows the best practices or if there are any vulnerabilities. You could also try to optimize your infrastructure for cost, performance, or security.
🤲 If you need to convince your boss, try deploying a proof-of-concept project for a small part of your infrastructure, and measuring the time saved, the reduction in errors, or the increase in scalability.

References#

Stuff I want to read:

Thanks for reading#

If you have any questions or feedback, feel free to reach out. Happy coding!

Infra as Code 101
https://www.nadia.fyi/posts/infra-as-code/
Author
Nadia
Published at
2024-04-06