IaC for developers using AWS Copilot (Part 1)

Aridany Ruano Freire
edataconsulting
8 min readDec 28, 2022

--

Development teams managing, in a self-service manner, the infrastructure platforms where their applications will run is one of DevOp’s main results.

The combination of Cloud Platforms and the concept of Infrastructure as Code (IaC) has provided us with the foundations to achieve this goal. But, tooling (and specially developer-oriented tools) are needed if we want teams to stop worrying and love the operational aspects of being the owners of their code.

In this pair of articles, I’m going to talk about the advantages that we, as developers, can get by using one of these tools: AWS Copilot.

In the first one, I will mostly rant about why we need a tool that simplifies and help us to enforce good practices. In the second one, we will go deeper into Copilot and see what we can get from it.

Why a tool like this?

Cloud computing services are great for developers, aren’t they? Before that, for apparently easy things such as getting a little more RAM for our applications, we had to request a specialized team that had to go through a bureaucratic cycle of tickets until someone, luckily, solved our need in a data center. Now, managing things like creating the environments where our applications will run can be done by ourselves, be it by API call or mouse click.

The same can be said about IaC.

Do you have a working testing environment and want to create a production replica (or the other way around) that is the same but some parameters? easily done using IaC.

Do you remember that tiny, urgent configuration fix we had to apply manually to the production environment to solve an issue that surged there and how we forgot to do it in staging? And how, two years later, it was incompatible with an update we did, so we went crazy because it broke production and we couldn’t replicate it in staging? That shouldn’t happen with IaC, as it makes easy to avoid applying manual changes to environments.

So many pains can be eased with IaC, including rollbacks, change version control, continuous integration, and even documenting your infrastructure.

But, very soon, we, developers, discovered that (surprise, surprise) making code run in production is more complex than doing it in our local environments. Sure, it’s easy to deploy our web application in AWS, be it in containers or not.

Simple diagram of an instance in AWS

But in production, we see that we have a much larger rate of requests, and we are expected to be 24/7 online. Thankfully, is not hard to make a cluster with our application in AWS, and anyway, we build our applications as stateless for nothing isn’t it?

A simple diagram of a cluster of instances in AWS

Well, but now we need something in front of it to route all the requests, as we are not going to make all of our users learn the IPs of our servers, aren’t we? So we need a load balancer.

A diagram of a cluster of instances with an Application Load Balancer in front

We are almost there, but not. This is better than instances IPs, yet we don’t want the application to be seen at whatevertheweirdname-2378461293.polar-north-2.elb.amazonaws.com. Therefore, we need DNS configuration with (potentially) Route53. Oh! and certainly, we want our app to be accessed by HTTPS, so AWS Certificate Manager is to the rescue.

A more detailed diagram of an application in AWS using Route53 and Certificate Manager

These are the kind of things that we have to know and manage for the simplest of applications (I haven’t even gone into persistence). And you did think that those infrastructure and operation teams do nothing? those heroes deal with this stuff on a daily basis¹.

And let’s be honest, developers don’t want to work with that level of detail day and night. Me, personally, I like it, and when I started to work with a devops style of development, I thought that it was something that all good developers should dominate. Now, I’ve learned how unfair was to try to impose the things I find enjoyable to others; the truth is that as much as I like IaC solutions like AWS Cloud Formation or Terraform, they are not the best tool for most developers. Being too low-level, they make things that are easy to think about (e.g. creating a web application that is available through the internet) a tedious process (for most developers) of obscure copy-pasting.

Just add a little bit more complexity to the solution, and it gets worse. I haven’t talked about security and networking (e.g. only the load balancer should be able to see the application instances). Or imagine that we built our application with some modularity, and now the load balancer has to route by path. Doing these with Cloud Formation, even I find boring².

It’s because of all of this that something that adds a level of abstraction is needed if we want development teams to be the owners of the code and not hate it, and of course, that development teams want to be the owners of the code, especially when the code is in its most critical stage, in production. If something fails there, who do you prefer to have the full access and knowledge to fix it? I, for one, vote for the people that built it over a team that only knows about where it’s deployed.

What is Copilot?

Copilot is a CLI tool to manage the lifecycle of container-based applications deployed in AWS. It is the replacement for the original ECS management command line tool, but it has evolved to be much more.

It simplifies the operation of our environments in AWS following and enforcing established good practices to create deployment platforms like the one we were starting to hint before.

So, an example to see how easy it is to start with it. We have a git repository containing a project that works in our local environment (like the simple web application in the previous section). The only prerequisite is that the application is containerized, so let’s assume we have a working Dockerfile in the repo.

Ok, now, after installing the copilot cli tool, we only have to type

copilot init

and copilot will ask you some simple questions

  1. Name of your app.
  2. Type of service you are deploying. For our case, it would be Load Balanced Web Service. We’ll later see the other ones.
  3. Name of the service. Our example is the most simple of cases, a single module, monolithic application, but of course, Copilot works great for modular multi-service applications.
  4. Which is the docker file we want to use to deploy the application. When there is only one (as it probably should) it will default gracefully.
  5. In which port inside your container do you want to send traffic to.

And lastly, after setting things up, it will ask you if you want to deploy to a test environment, and after a couple of minutes, you will have an URL where you can connect to your application.

And what do we get? Something similar to one of the diagrams I showed before

The architecture of a Copilot simple service

all running, all working, vpcs and security groups, ECS tasks and services, load balancer listeners and target groups.

As with many devops tools, copilot mixes an imperative approach, where, as we saw before, you command what you want to be done, with a declarative one, where you describe the desired state for your platform and then apply it. Copilot uses a YAML (nobody’s perfect) manifest. To find it, after creating the service with copilot init we can see a copilot folder in our repo root, inside that another one with the name we give to the service, and there we can see a manifest.yml.

# The manifest for the "example" service.
# Read the full specification for the "Load Balanced Web Service" type at:
# https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/

# Your service name will be used in naming your resources like log groups,
# ECS services, etc.
name: example
type: Load Balanced Web Service

# Configuration for your containers and service.
image:
# Docker build arguments.
build: Dockerfile
# Port exposed through your container to route traffic to it.
port: 8280

cpu: 256 # Number of CPU units for the task.
memory: 512 # Amount of memory in MiB used by the task.
count: 1 # Number of tasks that should be running in your service.
exec: true # Enable running commands in your container.

As you can see, it’s straightforward and down to the point, abstracting all the details behind the scenes; it just shows you the things you want to know and change. So if we want to create more instances of our application, it’s as easy as changing that count parameter to the number we want and run

copilot svc deploy

and that’s it, service scaled up.

The architecture of a balanced copilot service

And that’s all for the first chapter. In the next one, we will go deeper into AWS Copilot; and we will see how we can solve our real-life problems with it, showing both its strong points and its limitations. See you!

[1] ^ And it gets worse, companies with several teams working independently, that is, the kind of companies we will deal with for most of our careers. We need some kind of governance there, some set of good practices that we want to apply to some extent, compliance requirements, cost managing etc. It is a daunting task to try to manage all of that without creating an independent, application-ignorant silo.

[2] ^ My view now is that these low level platform knowledge should be for developers like regular expressions are for most. We know how they work and what they are used for, we remember just the basic stuff for the most simplest things, and we know where to look for help when the unusual feature that needs it arise. Forgetting all about it just a couple of days after we finished.

--

--