Why Habitat? The Supervisor and Run Lifecyle

This is the third post in our series, Why Habitat? You can catch up with Part 1 and Part 2.

In our previous posts, we talked about packaging your applications and compared results between packaging using a Dockerfile vs using Habitat. The core of Habitat packaging is the plan.sh with allows us to define the build lifecycle of our application. To actually run our application, we need to define the run lifecycle.

Habitat and the Run Lifecycle

Habitat makes managing the run lifecycle very easy through the use of a couple different concepts. If you think about what’s needed to run most applications, you need 3 things: the config with which to start the application, the lifecycle events (start, stop, restart, etc), and some sort of a process supervisor.

If you look back at the Node.js example we used in the previous blog post, you’ll see that we’ve defined the config and the lifecycle events directly inside our Habitat plan.

michael@ricardo:habitat$ ls
alt_plan.sh   config/       default.toml  hooks/        plan.sh
michael@ricardo:habitat$ ls config/
config.json
michael@ricardo:habitat$ ls hooks/
init  run
michael@ricardo:habitat$

Managing the Run Lifecycle

In order to have a running application, you need a subsystem to monitor the application process. The subsystem is responsible for ensuring required services are available before the application starts. Typically, in many modern architectures, some responsibility for resilience also depends on application developers coding with unreliable backing services in mind (think the Netflix chaos model). With Habitat, all of those responsibilities are the domain of the supervisor (hab-sup).

The hab-sup is responsible for ensuring the application you packaged is running, that its environment is ready before the application starts, and for managing a number of events that might occur while the application is operational. In Habitat, we refer to these as lifecycle events and they are common events you would expect: init, run, reconfigure, health_check, and file_updated. You can learn more about these hooks from the Habitat documentation. For now, let’s look at our plan’s run hook to see how we start node.

#!/bin/sh

cd {{pkg.svc_var_path}}

exec node server.js 2>&1

The purpose of these hooks is to define what should happen when your application encounters various environmental situations. Typically, this type of management is seen as being external to the needs of your application and is managed by a third-party tool (such as an orchestrator or config management framework). Habitat takes a holistic approach to managing your application lifecycle. So, these hooks are provided as a way for you to define how your application should behave when situations change. If you need to execute commands to alter behavior, hooks are what you use to do that.

Managing Configuration

Changes in environment during your application lifecycle may need more than just arbitrary commands to get the desired behavior you want. Sometimes, you need to alter the app config your service launched with to account for new situations. Because that’s a fundamental part of managing your entire application lifecycle, Habitat gives you constructs for that too. Habitat gives you a way to bundle in all the mechanisms you need to automatically alter behavior when you need it. That’s what we mean when we say “automation that travels with the app”.

Habitat packages the config of the application along with the application artifacts we want to run. This is important because it allows us to ensure the config and the artifact are shipped and managed together as the dependent units we know they are, but never really treated as such until now. The config directory of our plan contains just that; config files we’d like Habitat to manage through the lifecycle of our application. If we look at the config.json document bundled with our application, we see basic settings we can change at application launch.

michael@ricardo:habitat$ cat config/config.json 
{
"message": "{{cfg.message}}",
"port": "{{cfg.port}}"
}
michael@ricardo:habitat$

This file allows us to set the message we want our basic web service to return along with the port we want the application to listen on. This file is standard JSON with the exception of the {{cfg.message}} and {{cfg.port}} fields. Those two parameters are expressions from the  Handlebars templating language. Habitat uses Handlebars because it provides a lightweight and minimal templating language that is fast and efficient. Habitat prioritizes performance at every turn.

What we’re looking at isn’t actually a file, it’s a template that will render our config file. So we need to get the actual values for the expressions we’ve defined. Habitat provides three options for setting these values. First, Habitat allows you to define default values in the plan with a default.toml file. You can see our defined default values in our Node.js example plan.

michael@ricardo:habitat$ cat default.toml 
# Default values that can be updated.

# Message of the Day
message = "Hello, World!"

# The port number that is listening for requests.
port = 8080
michael@ricardo:habitat$

These are the values Habitat will use by default when the application starts. If you want to override these values you can override them using environment variables. For example, if we want to override the default port our sample app listens on, we simply need to set the environment variable like so:

HAB_MYTUTORIALAPP='port=8081'

This is easy if you’re running a Habitat packaged application in a Docker container; simply pass this environment variable when the container starts. For Habitat based applications running on other systems, you need to define the environment variable before the application starts. Using the default.toml or setting environment variables is an easy way to set the config of a Habitat package. However, there are times when that’s not optimal. For example, when you need to set values for things like secrets (database passwords, SSL keys, etc).

Since storing secrets in static files or setting them via environment variables is a bad practice, the Habitat supervisor allows you to set configuration values through a RESTful API as well. If you start a Habitat application, or a Habitat built container, you can see that the supervisor starts by listening on the network. 

michael@ricardo:habitat$ docker run -p 8080:8080 -it mfdii/mytutorialapp
hab-sup(MN): Starting mfdii/mytutorialapp
hab-sup(TP): Child process will run as user=hab, group=hab
hab-sup(GS): Supervisor 172.17.0.3: 545a5976-0c37-44ae-b213-26f00053a737
hab-sup(GS): Census mytutorialapp.default: 6560187c-b52d-429c-adcf-7b9a6c1ed592
hab-sup(GS): Starting inbound gossip listener
hab-sup(GS): Starting outbound gossip distributor
hab-sup(GS): Starting gossip failure detector
hab-sup(CN): Starting census health adjuster
hab-sup(SC): Updated config.json
hab-sup(TP): Restarting because the service config was updated via the census
mytutorialapp(SV): Starting
mytutorialapp(O): Running on http://0.0.0.0:8080

The supervisor also generates any configuration files upon launch, which you can see in the above output. Like we could with environment variables, we can inject config through the supervisor’s API.

[11][default:/src:0]# cat /tmp/config.toml 
port = 8081
[12][default:/src:0]# hab config apply --peer 172.17.0.3 mytutorialapp.default 1 /tmp/config.toml 
» Applying configuration
↑ Applying configuration for mytutorialapp.default into ring via ["172.17.0.3:9634"]
Joining peer: 172.17.0.3:9634
Configuration applied to: 172.17.0.3:9634
★ Applied configuration
[13][default:/src:0]# 

To demonstrate this we can create a toml file and then upload that toml file to the supervisor. If we look at our running application, we will see that the supervisor saw the configuration change and took the appropriate action.

Writing new file from gossip: /hab/svc/mytutorialapp/gossip.toml
hab-sup(SC): Updated config.json
mytutorialapp(SV): Stopping
hab-sup(SV): mytutorialapp - process 61 died with signal 15
hab-sup(SV): mytutorialapp - Service exited
mytutorialapp(SV): Starting
mytutorialapp(O): Running on http://0.0.0.0:8081

Our config was updated and the application was restarted. Our config change was automatically applied without having to rebuild our application’s artifacts. We also can avoid storing sensitive configuration values in static files or in environment variables.

Bringing it All Together

If we take a step back and look at what we’ve done over the last 3 blog posts, you’ll see that Habitat allows you to bundle the application with the instructions to effectively operate into one artifact. The only way we can effectively manage the entire lifecycle of our application in an autonomous and distributed manner is to ensure that the development aspects of our application (the build lifecycle and its resulting artifact) work in tandem  with the operational components (config and the run lifecycle). A benefit of coupling those two historically separate, yet critically dependent aspects, is that when you deploy a new habitat package, you also deploy all of the required lifecycle management components to run the application in one step.

In the next part of our series, we’ll talk more about the supervisor and the benefits it gives you (service groups, topologies, and more). If you want to get a sneak peek you can watch our recorded webinar on “Simplifying Container Management with Habitat”.

Michael Ducy

Former Chef Employee