Sunday, February 03, 2019

How is software like cooking?

Time for a light-hearted post. After my move to the UK and having had my share of fish and chips, I have become by reaction more interested in Italian culinary history and practice. So I started diving into the science and the tradition of cooking, reading books such as the science of meat which combine chemistry and good taste, and I have now cooked enough lasagne to build a statistically significant sample.

Disclaimer: this post is full of meat references as that's culturally significant as a metaphor to transmit the concepts I have in mind. You may find this distasteful if you have chosen to follow a different path.

So here's 5 ways in which software development and cooking are alike...

Feedback loops

"to serve man"
There's a joke in a Futurama episode about Bender (being a robot) not having a sense of taste and hence playfully disgusting the humans in the team with too much salt. The joke works because in cooking you need a continuous feedback loop to conform to your taste, for example adding salt and pepper at the end of a preparation until it tastes right.

We are no strangers to this process in software development: most of the practices I preach about lead to getting working software in front of someone that will use it as soon as possible, to better steer future development with the feedback.

There are shorter, inner feedback loops than tasting: the speed with which meat is browning will make you adjust your source of heat for that phase of the preparation to avoid charring the exterior surface to a pitch black color. Not too different from your unit tests failing and informing you of an issue well before it gets to an actual customer that will send the steak back to the kitchen.

Quality is in the eye of the beholder

Cacio e pepe: simple&tasty or poor?
Taste has lots of different components, including not just what your tongue perceives but also smells, presentation, and expectations. But for most of these aspects, quality is in the eye of the beholder and we can't avoid coming to grips with the variety of people and cultures.

Therefore, despite how good you think your burgers are, some people just don't like fat, mince meat. I appreciate Indian curries but I have some physical limits on the spiciness levels that make me almost always choose mild korma. And imagine the cultural shock of discovering I have been wrongly putting lemon in tea all my life instead of milk as the only acceptable choice.
(I know, tea doesn't even grow in Europe, but I grew up with British tea as the standard.)

We all have good intentions in thinking hard about what a user will enjoy or be productive with; but we have to recognize there is a vast variety of users and we have to design (or cook) for each of them.

Control the process, rather than micromanaging the material

This cake was fluorescent on purpose
Convection ovens are a great example of a controlled process for cooking uniformly. In the context of large pieces of meat or fish, this mainly means getting them to a uniform, high-but-not-too-high temperature to avoid overcooking. The oven fan pushes hot air all around them, heating the surface evenly. Air transfers less heat than, for example, water; so there is time for the temperature to rise across your roasting chicken rather than overcooking the outside while leaving some parts dangerously raw.

Thus for large cuts this is pretty much impossible to achieve in a pan, unless you literally cut everything into slices thin enough that they can cook quickly. The oven-based process is much more convenient as you literally abandon your tray in there, checking from time to time if it's ready with a thermometer.

Generally speaking of enterprise applications and websites, I favor a process in which we catch bugs with multiple safety nets (up to user experimentation if possible) than overdesigning for every possible problem. While you can think of possible scenarios to test endlessly, bugs are always going to happen and it's more important to have a process in place for which they will be fixed and never regress because of new automated tests. That makes your software converge to a steady, stable state like a perfectly cooked chicken.

You need to measure

That's definitely cold, not just for meat
If you want to consistently cook meat to your liking, there is no escape from using a thermometer to understand when it's ready - its center reaching a set temperature that corresponds to medium-rare (for a steak) or well-cooked (for poultry) or something else. Looking at the external color? No relation with the inside. Checking how hard it has become? Too subjective. Roast for a certain amount of time? Ignores the variability of both the ingredients and the heat source.

When we perceive part of an application as slow, we need to use a profiler to find out what functions or methods are taking the most time to execute. Making as few assumptions as possible, we collect data to point us in the right direction. Opinions don't count: your browser timings and other metrics do (if collected correctly).

You can substitute ingredients, to a certain extent

Focaccia genovese
Cornstarch and flour are used in small quantities in many recipes, with the goal of thickening a liquid. This is due to their starch content, as this carbohydrate granules swell up with water creating enough friction to transform a liquid with the viscosity of water into something that feels like cream.

If you try to use cornstarch to make bread however, you won't be able to get an elastic product as it lacks the proteins that would build gluten. Even if you use the wrong flour (cake flour as opposed to bread flour, to keep it simple), this will greatly affect the result due to the smaller percentage of proteins that it contains. Baking, both for sweet and savoury goods, require much more precision.

In software development, we have grown up with Lego bricks as a metaphor and we continuously try to swap out pieces, hiding details behind a useful abstraction that sometimes leaks. Nowadays relational databases can be queried interchangeably if you stick to standard SQL queries. But the data types for columns can be pretty different in the range they support, especially if they are somewhat more exotic like JSON and XML fields rather than integer and strings. A wise decision is still required to understand when substituting components is possible, or where some combinations will never work.

And here are 5 ways in which software development and cooking are very different...

Cooking is a repeatable process

Lots of Dutch cheese around Amsterdam
Recipes (at least the good ones) are literally the codification of a process that should be robust to external variations to get a consistent result. It's the mark of a good cook to be able to deal with variations in ingredients or tools, but unless you are up on a mountain water boils at pretty much the same temperature, and the physical transformation that your carrots undertake when they are heated is well established.

In software, every new feature is a new design to make rather than the execution of a plan. Even porting software or reimplementing it bears surprises as the platform it is running on is now different. And no one understands how long it took to produce the original version, no matter give an estimation for the new one that is being created. We have processes for understanding what a feature should do, and safely implementing it and rolling it out; but there are always land mines waiting on the path.

Cooking has some precise physical quantities you can rely on

Peppered pork fillet steak
Understood: measurement is needed in both fields. But as much as your oven oscillates around its target temperature, it is still much more precise than a developer's effort. Even without meetings and other time variables, how fast and precise we are in a certain day varies: humans aren't robots. Just how knowledgeable we are about a technology influences greatly the designing and testing phases. The Mythical Man-Month remains, well, mythical.

In the food industry, the right tools can even measure the strength of a flour, to check whether it's good for the bread you want to obtain. If you look at a technology team, measuring how many tasks per week we have completed is probably as good as it gets. There's humans involved and applying social science to a very small group probably doesn't get you very far in terms of collecting data and drawing inferences.

You can still measure other times objectively, like time to deploy: how long it takes for a commit on master to reach the production environment. We partly do this because it's important but also because it's feasible to measure. What most project managers would care about will be time from idea to complete implementation instead. But that requires estimating the length of a queue that changes all the time, and is just the first step of a creative development process with its own variations.

Determinism of the digital world

Blackberries from the garden
It's pretty difficult to get the same tomatoes, courgettes or grapes as last week, and pretty much impossible to get the same ones in-season and off-season. You can ship them in from South Africa or Australia but travel time and refrigeration can modify their contents, and thus their taste.

If you look at a physical server, it's much more similar to laboratory equipment than to a living product: you can run programs and see them always taking a similar amount of time to complete, controlling the randomness of the operating system around it. This gets eroded a bit in the cloud, where performance may be affected by your neighbors due to co-tenancy.

Timing in the kitchen

Very unstable crochembouche
Whether it is simply changing the temperature of a meat joint, or a more complex transformation like baking a cake, timing should be one of the concerns if you want to obtain a good result. I formalize this concept by thinking that it's not possible to stop time, in many cases.

Cooks know tricks like cooking eggs or rice to a certain degree, than cooling it down and finish the process later when the food has to be served; or simply reheat it if fully cooked. This works for various categories of products, but it's an ad-hoc process.

Consider the power we have in a digital world: firing up a debugger literally stops execution at some point in the life of the program, allowing us to take a look at what we want in the right context. Since the state of the program is the Matrix, we can slow it down, speed it up, and change things causing a déjà vu to your objects.

If you want to reproduce some computation, you have the tools available to build a Docker image containing all sorts of dependencies and store it for future usage. If you want to reproduce your perfect croissants, the only tools you have are a recipe and your own memories. Add the variation of ingredients and even temperature and humidity in your kitchen, and you can understand why scientific exploration needs a laboratory with its controlled conditions to be able to make progress.

Cooking equipment makes a difference

Now, grate parmesan without this...
Besides basic tools like appropriately shaped knives, a pressure cooker would make you able to reach certain results that would take a long time with an ordinary pot of boiling water. A temperature bath (I don't own one of these) can help cooking meat evenly only to then finish the process with a 2-minute searing. Even a scale is just necessary for baking, as measuring ingredients like flour by volume has a 50% margin of error due to its compressibility.

Consider how you can write code on your old laptop from the beach instead. You target an open source interpreter, and the end product will run on the same server that could accept strictly regulated banking software. As long as you can literally string bytes together, you can produce running software: everything else helps. The ephemeralization of software tools due to virtualization and the large availability of open source platforms make digital startups a reality, whereas opening a restaurant remains a capital-intensive operation.

But there's more...

The power of metaphors

Metaphors can foster understanding of a new system, or lead us ashtray. They are powerfully transmitting a mental model, but that model has its limitations and may even be less precise than a more formal model like a math analogy. But especially in complicated fields like cryptography, terms such as key and signature have popularized concepts to generation of students that would have otherwise found them very hard to think about.

I wrote this post for fun, but I stand behind most of the comparisons: that's all for now. You'll find me using an Helm to ship my containers...

Thursday, January 10, 2019

Practical Helm in 5 minutes

https://helm.sh/
Yet another ship-themed name

Containerization is increasingly a powerful way to deploy applications on anonymous infrastructure, such as a set of many identical virtual machines run by some cloud provider. Since container images ship a full OS, there is no need to manage packages for the servers (a PHP or Python interpreter), but there are still other environment-specific choices that need to be provided to actually run the application: configuration files and environment variables, ports, hostnames, secrets.

In an environment like Kubernetes, you would create all of this declaratively, writing YAML files describing each Pod, ConfigMap, Service and so on. Kubernetes will take these declarations and apply them to its state to reach what is desired.

As soon as you move outside of a demo towards multiple environments, or towards updating one, you will start to see Kubernetes YAML resources not directly as code to be committed into a repository, but as an output of a generation process. There are many tweaks and customizations that need to be performed in each environment, from simple hostnames (staging--app.example.com vs app.example.com) to entire sections being present or not (persistence and replication of application instances).

The problem you need to solve then is to generate Kubernetes resources from some sort of templates: you could choose any template engine for this task, and execute kubectl apply on the result. To avoid reinventing the wheel, Helm and other competitors were created to provide an higher abstraction layer.

Enter Helm

Helm provides templating for Kubernetes .yaml file; as part of this process, it extracts the configuration values for Kubernetes resources into a single, hierarchical data source.

Helm doesn't stop there however: it aims to be a package manager for Kubernetes, hence it won't just create resources such as a Deployment, but it will also:
  • apply the new resources on the Kubernetes cluster
  • tag the Deployment with metadata and labels
  • list everything that is installed in terms of applications, rather then Deployments and ConfigMaps
  • find older versions of the Deployment to be replaced or removed
The set of templates, helpers, dependencies and default values Helm uses to deploy an application is called a chart whereas every instance of a chart created on a cluster is called a release. Therefore, Helm keeps track of objects in terms of releases and allows you to update a release and all its contents, or to remove it and replace it with a new one.

Folder structure

The minimal structure of an Helm chart is simply a folder on your filesystem, whose name must be the name of the chart. As an example, I'll use green-widgets as a name, a fictional web application for ordering green widgets online.

This is what you'll see inside a chart:
  • Chart.yaml: metadata about the chart such as name, description and version.
  • values.yaml: configuration values that may vary across releases. At a bare minimum the image name and tag will have defaults here, along with ports to expose.
  • the templates/ subfolder: contains various YAML templates that will be rendered as part of the process of creating a new release. There is more in this folder like a readme for the user and some helper functions for generating common snippets.
Apart from this minimal setup, there may also be a requirements.yaml file and a charts/ subfolder to deal with other charts to use as dependencies; for example, to install a database through an official chart rather than setting up PostgreSQL replication on your own. These can be safely ignored until you need these features though.

Once you have the helm binary on your system, you can generate a new chart with helm create green-widgets.

Cheatsheet

You can download a helm binary for your platform from the project's releases page on Github. The helm init command will use your kubectl configuration (and authentication) to install tiller, the server-side part of Helm, onto a cluster's system namespace.

Once this is setup, you will be able to execute helm install commands against the cluster, using charts on your local filesystem. For real applications, you can install official charts that are automatically discovered from the default Helm repositories.

The command I prefer to use to work on a chart however is:
helm upgrade --install --set key=value green-widgets--test green-widgets/

The mix of upgrade and install means this command is idempotent and will work for the first installation as well as for updates. Normally you would issue a new release for a change to the chart, but this approach allows you to test out a chart while it's in development, using a 0.0.1 version.
There is no constraint on the release name green-widgets--test, and Helm can even generate random names for you. I like to use the application name and its environment name as a team convention, but you should come up with your own design choices.

A final command to keep in mind is helm delete green-widgets--test which will delete the release and all the resources created by your templates. This is enough to stop using CPU, memory and IP addresses, but it's not enough to completely remove all knowledge of the release from Tiller's archive. To do so (and free the release name allowing its re-creation) you should use add the --purge flag.

Caveats

This 5-minute introduction makes it all seem plain and simple, but it should be clear that simply downloading Helm and installing it is not a production-ready setup. I myself have only rolled out this setup to testing environment at the time of writing.

I can certainly see several directions to explore, that I either cut from the scope in order to get these environments up and running for code review; or investigated and used but not included in this post. For example:
  • requirements.yaml allows to include other charts as dependencies. This is very powerful for off-the-shelf open source software such as databases, caches and queues; it needs careful choices for the configuration values being passed to these dependencies, and your mileage may vary with the quality of the chart you have chosen.
  • chart repositories are a good way to host stable chart versions rather than copying them onto a local filesystem. For example, you could push tarballs to S3 and have a plugin regenerate the index.
  • the whole Helm and Tiller setup arguably needs to be part of a Infrastructure as Code apporach like the rest of the cluster. For example, I am creating a EKS cluster using Terraform and that would need to include also the installation and configuration of Tiller to provide a turnkey solution for new clusters.

Wednesday, January 02, 2019

The path from custom VM to VM with containers

https://commons.wikimedia.org/wiki/File:Kanda_container.jpg
Image of a single container being transported by OiMax
Before the transition to Docker containers started at eLife, a single service deployment pipeline would pick up the source code repository and deploy it to one or more virtual machines on AWS (EC2 instances booted from a standard AMI). As the pipeline went across the environments, it repeated the same steps over and over in testing, staging and production. This is the story of the journey from a pipeline based on source code for every stage, to a pipeline deploying an immutable container image; the goal pursued here being the time savings and the reduced failure rate.

The end point is seen as an intermediate step before getting to containers deployed into an orchestrator, as our infrastructure wasn't ready to accept a Kubernetes cluster when we started the transition, nor Kubernetes itself was trusted yet for stateful, old-school workloads such as running a PHP applications that writes state on the filesystem. Achieving containers-over-EC2 allows developers to target Docker as the deployment platform, without realizing yet cost savings related to the bin packing of those containers onto anonymous VMs.

Starting state

A typical microservice for our team would consist of a Python or PHP codebase that can be deployed onto a usually tiny EC2 instance, or onto more than one if user-facing. Additional resources that are usually not really involved in the deployment process are created out of band (with Infrastructure as Code) for this service, like a relational database (outsourced to RDS), a load balancer, DNS entries and similar cloud resources.

Every environment replicates this setup, whether it is a ci environment for testing the service in isolation, or an end2end one for more large-scale testing, or even a sandbox for exploratory, manual testing. All these environments try to mimic the prod one, especially end2end which is supposed to be a perfect copy on fewer resources.

A deployment pipeline has to go through environments as a new release is promoted from ci to end2end and prod. The amount of work that has to be repeated to deploy from source on each of the instances is sizable however:

  • ensure the PHP/Python interpreter is correctly setup and all extensions are installed
  • checkout the repository, which hopefully isn't too large
  • run scripts if some files need to be generated (from CSS to JS artifacts and anything similar)
  • installing or updating the build-time dependencies for these tasks, such as a headless browser to generate critical CSS
  • run database migrations, if needed
  • import fixture data, if needed
  • run or update stub services to fill in dependencies, if needed (in testing environments)
  • run or update real sidecar services such as a queue broker or a local database, if present
These ever-expanding sequence of operations for each stage can be optimized, but in the end the best choice is not to repeat work that only needs to be performed once per release.

There is also a concern about the end result of a deploy being different across environments. This difference could be in state, such as a JS asset served to real users being different from what you tested; but also in outcome, as a process that can run perfectly in testing may run into a APT repository outage when in production, failing your deploy halfway through, only on one of the nodes. Not repeating operations leads not just to time savings but to a simpler system in which fewer operations can fail just because there are fewer of them in general.

Setting a vision

I've automated before builds that generated a set of artifacts from the source code repository and then deploy that across environments, for example zipping all the PHP or Python code into an archive or in some other sort of package. This approach works well in general, and it is what compiled languages naturally do since they can't get away with recompiling in every environment. However, artifacts do not take into account OS level dependencies like the Python or PHP version with their configuration, along with any other setup outside of the application folder: a tree of directories for the cache, users and groups, deb packages to install.

Container images promise to ship a full operating system directory tree, which will run in any environment only sharing a kernel with its host machine. Seeing docker build as the natural evolution of tar -cf ... | bzip2, I set out to port the build processes of the VMs into portable container images per each service. We would then still be deploying these images as the only service on top an EC2 virtual machine, but each deployment stage should just be consisting of pulling one or more images and starting them with a docker-compose configuration. The stated goal was to reduce the time from commit to live, and the variety of failures that can happen along the way.

Image immutability and self-sufficiency

To really save on deployment time, the images being produced for a service must be the same across environments. There are some exceptions like a ci derivative image that adds testing tools to the base one, but all prod-like environment should get the same artifact; this is not just for reproducibility but primarily for performance.

The approach we took was to also isolate services into their own containers, for example creating two separate fpm and nginx images (wsgi and nginx for Python); or to use a standard nginx image where possible. Other specialized testing images like our own selenium extended image can still be kept separate.

The isolation of images doesn't just make them smaller than a monolith, but provides Docker specific advantages like leveraging independent caching of their layers. If you have a monolith image and you modify your composer.json or package.json file, you're in for a large rebuild. But segregating responsibilities leads instead to only one or two of the application images being rebuilt: never having to reinstall those packages for Selenium debugging. This can also be achieved by embedding various targets (FROM ... AS ...) into a single Dockerfile, and having docker-compose build one of them at a time with the build.target option.

When everything that is common across the environments is bundled within them, what remains is configuration in the form of docker-compose.yml and other files:
  • which container images should be running and exposing which ports
  • which commands and arguments the various images should be passed when they are started
  • environment variables to pass to the various containers
  • configuration files that can be mounted as volumes
Images would typically have a default configuration file in the right place, or be able to work without one. A docker-compose configuration can then override that default with a custom configuration file, as needed.

One last responsibility of portable Docker images is their definition of a basic HEALTHCHECK. This means an image has to ship enough basic tooling to, for example, load a /ping path on its own API and verify a 200 OK response is coming out. In the case of classic containers like PHP FPM or a WSGI Python container, this implies some tooling will be embedded into the image to talk to the main process through that protocol rather than through HTTP.

It's a pity to reinvent the lifecycle management of the container (being started, then healthy or unhealthy after a series of probes), whereas we can define a simple command that both docker-compose or actual orchestrators like Kubernetes can execute to detect the readiness of the new containers after deploy. I used to ship smoke tests with the configuration files to use, but these have largely been replaced by polling for an health status on the container itself.

Image size

Multi-stage builds are certainly the tool of choice to keep images small: perform expensive work in separate stages, and whenever possible only copy files into the final stage rather than executing commands that use the filesystem and bloat the image with their leftover files.

A consolidated RUN command is also a common trick to bundle together different processes like apt-get update and rm /var/lib/apt/lists/* so that no intermediate layers are produced, and temporary files can be deleted before a snapshot is taken.

To find out where this optimization is needed however, some introspection is needed. You can run docker inspect over a locally built image to check its Size field and then docker history to see the various layers. Large layers are hopefully being shared between one image and the next if you are deploying to the same server. Hence it pays to verify that if the image is big, most of its size should come from the ancestor layers and they should seldom change.

A final warning about sizes is related to images with many small files, like node_modules/ contents. These images may exhaust the inodes of the host filesystem well before they fill up the available space. This doesn't happen when deploying source code to the host directly as files can be overwritten, but every new version of a Docker image being deployed can easily result in a full copy of folders with many small files. Docker's prune commands often help by targeting various instance of containers, images and other leftovers, whereas df -i (as opposed to df -h) diagnoses inodes exhaustion.

Underlying nodes

Shipping most of the stack in a Docker image makes it easier to change it as it's part of an immutable artifact that can be completely replaced rather than a stateful filesystem that needs backward compatibility and careful evolution. For example, you can just switch to a new APT repository rather than transition from one to another by removing the old one; only install new packages rather than having to remove the older ones.

The host VMs become leaner and lose responsibilities, becoming easier to test and less variable; you could almost say all they have to run is a Docker daemon and very generic system software like syslog, but nothing application-specific apart from container dependencies such as providing a folder for config files to live on. Whatever Infrastructure as Code recipes you have in place for building these VMs, they will become easier and faster to test, with the side-effect of also becoming easier to replace, scale out, or retire.

An interesting side effect is that most of the first stages of projects pipelines lost the need for a specific CI instance where to deploy. In a staging environment, you actually need to replicate a configuration similar to production like using a real database; but in the first phases, where the project is tested in isolation, the test suite can effectively run on a generic Jenkins node that works for all projects. I wouldn't run multiple builds at the same time on such a node as they may have conflicts on host ports (everyone likes to listen on localhost:8080), but as long as the project cleans up after failure with docker-compose down -v or similar, a new build of a wholly different project can be run with practically no interaction.

Transition stages

After all this care in producing good images and cleaning up the underlying nodes, we can look at the stages in which a migration can be performed.

A first rough breakdown of the complete migration of a service can be aligned on environment boundaries:
  1. use containers to run tests in CI (xUnit tools, Cucumber, static checking)
  2. use containers to run locally (e.g. mounting volumes for direct feedback)
  3. roll out to one or more staging environments
  4. roll out to production
This is the path of least resistance, and correctly pushes risk first to less important environments (testing) and only later to staging and production; hence you are free to experiment and break things without fear, acquiring knowledge of the container stack for later on. I think it runs the risk of leaving some projects halfway, where the testing stages have been ported but production and staging still run with the host-checks-out-source-code approach.

A different way to break this down is perform the environment split by considering the single processes involved. For example, consider an application with a server listening on some port, a cli interface and a long-running process such as a queue worker:
  1. start building an image and pulling it on each enviroment, from CI to production
  2. try running CLI commands through the image rather than the host
  3. run the queue worker to the image rather than the host
  4. stop old queue worker
  5. run the server, using a different port
  6. switch the upper layer (nginx, a load balancer, ...) to use the new container-based server
  7. stop old server
  8. remove source code from the host
Each of these slices can go through all the environments as before. You will be hitting production sooner, which means Docker surprises will propagate there (it's still not as stable as Apache or nginx); but issues that can only be triggered in production will happen on a smaller part of your application, rather than as a big bang of the first production deploy of these container images.

If you are using any dummy project, stub or simulator, they are also good candidates for being switched to a container-based approach first. They usually won't get to production however, as they will only be in use in CI and perhaps some of the other testing environments.

You can also see how this piece-wise approach lets you run both versions of a component in parallel, move between one and the other via configuration and finally remove the older approach when you are confident you don't need to roll back. At the start using a Docker image doesn't seem like a huge change, but sometimes you end up with 50 modified files in your Infrastructure as Code repository, and 3-4 unexpected problems to get them through all the environments. This is essentially Branch by Abstraction applied to Infrastructure as Code: a very good idea for incremental migrations applied to an area that normally needs to move at a slower pace than application code.

ShareThis