The Future of Infra-As-Code
The term IaC is thrown around a lot. In this article we explore an exciting new concept. IfC - Infrastructure From Code, we think this could be the future of cloud infra.
This article picks up where “History of Infra-As-Code” left off. In that article I talked about technologies including Puppet, Ansible, Terraform and Pulumi which were designed to solve specific problems around server configuration, automation and cloud infra provisioning & state management. Give that article a read first if you haven’t already.
Each generation of tooling iterated on weaknesses & gaps highlighted by the previous. Simultaneously, the ecosystem was offering up cloud-native & container platforms which were begging for some kind of standardization.
The present
Most organizations now have an eclectic mix of (Bash) scripts tying together Terraform Stacks, Ansible Playbooks, Image Builders, Kubernetes Manifests and some Python/Boto programs thrown in for good measure.
We’re still no closer to the fabled dream of provider portability.
Pain points
If we look at what is problematic for developers right now, we can get a pretty good indicator of what issues people will focus on solving:
Infra code is treated separate from application code: HCL or Pulumi code may live inside your repo, but it’s not closely tied to application code.
Infra frameworks are rigid and cloud vendor specific: AWS CloudFormation, Azure ResourceManager are unique and tied to their respective API’s. Terraform’s providers are incompatible across clouds.
A different set of skills, knowledge and experience are required for infra engineering and software development: The mindsets for infra design are not always synchronized with application architecture and development.
Has k8s failed us?
Kubernetes promised to be a platform-agnostic approach to app deployment. It’s great at being a standardized container platform. You can run it in a datacenter, on Azure, AWS, On-Prem or even on your local machine with MiniKube! The interface should remain the same and the behaviour of your pods should behave consistently across environments.
So why does it not always work this way?
One example; almost every system needs some sort of data persistence. A database, of some kind. Most use-cases are going to benefit greatly from utilizing RDS (AWS Relational Database Service) as their database, or Azure’s equivalent. It’s possible to run your database as a Kubernetes pod, but there are many reasons why that is probably a bad idea. RDS tends to be:
More reliable
Faster
Better distributed
“Powerful tools make for powerful foot-guns”
Container schedulers like k8s are amazing at launching, relaunching, monitoring and scaling container pods. They are not optimal for persisting and snapshotting your data forever so you can always get it back. This is the first problem.
The second, is that even if k8s provides you with a “Universal Database Interface” for AWS RDS provisioning, it’s likely not going to be compatible with other cloud vendors. These services are so customized to be vendor-specific, that getting and maintaining a standardized subset is difficult.
Back to vendor lock-in land?
This brings us back to needing custom-code, separate to k8s manifests, in order to orchestrate and maintain the entirety of our platform. K8s itself needs to be deployed and maintained using a system like Terraform, along with any other cloud-native services upon which we depend.
Pulumi and CDK (AWS Cloud Development Kit) have graced us with the ability to write complex programs in the language of our choosing, however they are still implicitly bound to a given cloud provider.
Where to now?
I’ve talked in depth about the previous generations of tools and the current state of container platforms and cloud vendors.
We’ve established that we still need some sort of cloud-vendor-specific interfacing language in order to bootstrap our container platform, baseline security architecture, and other cloud-vendor dependencies like RDS or SQS (Simple Queueing Service).
Introducing IfC
Infra-From-Code is a term I think you’re going to hear thrown around more often.
IfC is a paradigm-shift in how we think about and define the services upon which our application depends. Instead of writing logically separate “DevOps” style programs and tooling which function alongside our code, IfC seeks to derive the desired state of the cloud FROM the code.
Some pioneers have already created products in this fledgling space, and their approaches share some commonalities with some differences.
Commonly, IfC parses your application’s code to infer required resources then it creates and manages those cloud resources for you automatically in the background.
Below are three examples of real-world IfC implementations, with some subtle yet important differences.
Wing
“A unified programming model that combines both infrastructure and application code into a single programming language”.
Wing’s approach to IfC is a tight blend of traditional programming AND infrastructure service capabilities embedded as first-class-citizens. They call this a “cloud oriented programming language”
# Wing Lang
bring cloud;
let queue = new cloud.Queue(timeout: 2m);
let bucket = new cloud.Bucket();
queue.setConsumer(inflight (body: str): str => {
let key = "file.txt";
bucket.put(key, body);
});
In the above example a message from the queue is written to a text file inside the bucket.
You’ll notice the use of the “inflight” keyword. Wing introduces two conceptual execution phases… Preflight & Inflight.
Preflight code runs once at compile time to generate the infrastructure configuration and dependencies. E.g - a bucket and a queue along with IAM configuration. This is the default execution phase and is declared with the ‘let’ keyword.
Inflight code is executed at runtime and is what implements the application’s functionality. E.g - pushing / popping messages from the queue.
Nitric
Nitric is a set of libraries which supporting multiple clouds and multiple languages.
By adding lightweight declarations of resources as part of the codebase, the library is able to infer the required cloud resources implicitly.
This creates a tight-coupling relationship between the application code and the cloud resources it’s dependent on.
# Nitric Lang
from nitric.resources import bucket
profiles = bucket("test").allow("reading")
file = await profile.file("test.txt").read()
Nitric is solving the cloud provider compatibility problem! By offering a common interface to resources like “bucket” and “queue”. The main tradeoff is we miss out the latest-and-greatest features, because the library needs to adopt new feature-sets after they are released by cloud providers.
Klotho
Klotho uses annotations to describe cloud resources, these are declared close-to the dependency in the codebase.
# Klotho
from fastapi import FastAPI
# @klotho::expose {
# id = "pet-api"
# target = "public"
# }
app = FastAPI()
This is a powerful and attractive approach because it does not introduce a new framework or library, instead it augments existing code with declarative annotations.
Klotho uses Pulumi under-the-hood to manage the cloud state which was inferred from the annotated code. It also generates visualizations to show the resource/relationships before deploying them.
Conclusion
We live in exciting times. The next generation of Infra-*-Code related tools and frameworks are before us, and it’s just a matter of time before industry awareness and adoption begin to take hold.
What direction do you think the industry will take from here? We’d love to hear from you in the comments!
-Daniel Korel