Showing posts with label containers. Show all posts
Showing posts with label containers. Show all posts

December 2, 2014

Day 2 - Running Applications at Scale with Kubernetes

Written by: Kelsey Hightower (@kelseyhightower)
Edited by: Tom Purl (@tompurl)

In this post we’ll take a look at Linux containers, built using the Docker image format, as an application packaging and distribution mechanism. This means utilizing containers as an alternative to RPMs, Debs, and tarballs. Then we’ll turn our attention to the workflow provided by Kubernetes for managing applications.

Normally when we talk about scale we tend to think about providing more compute resources to run applications, but today I’m talking about the ability to truly separate concerns between building applications and running them in production. Today I want to talk about scaling how we ship applications.

Containers for application packaging

The key to automation is to simplify things before you automate them. I’m a big fan of self-contained binaries like the ones produced by the Go compiler. It’s hard to argue against the convenience of distributing a single artifact vs the “deploy and pray” method offered by many of today’s packaging solutions.

Each language community has attempted to solve the packing problem in various ways specific to their platform needs. As a result we have gems, bundler, pip, virtualenv, npm, and the list goes on and on. The drawback to those solutions? They don’t allow you to express the OS dependencies required by many of the applications we build and ship today. With Docker containers we effectively gain the ability to build self-contained binaries for any application stack.

To be clear, Docker’s image format does not solve the packaging problem, it just moves it around a bit. The end results are pretty good. We can now rely on a single artifact to universally represent any application. The Docker container has become the new lego brick for building larger systems.

Now I can focus on more interesting problems.

The Datacenter is the Computer

With containers I can easily ship applications between machines and start to think of a cluster of machines as a single computer. Each machine acts as another CPU with the ability to execute applications and runs an operating system, but the goal is not to interact with the local OS directly. Instead we want to treat the local OS as firmware for the underlying hardware resources.

The only thing missing now is a scheduler.

The Linux kernel does a fantastic job of scheduling applications on a single system. Chances are if we run multiple applications on a single system the kernel will attempt to use as many CPU cores as possible to ensure that our applications run in parallel.

When it comes to a cluster of machines the job of scheduling applications becomes an exercise for the sysadmin. Today for many organizations scheduling is handled by the fine men and women running Ops. Unfortunately, human schedulers require humans to keep track of where applications can possibly run. Sometimes this means using spreadsheets or a configuration management tool. Either way these tools don’t offer robust scheduling that can react to real time events. This is where Kubernetes fits in.

If the datacenter is the computer then Kubernetes would be it’s operating system.

Application scheduling with Kubernetes

Kubernetes is a declarative system for scheduling Docker containers. First we need to get on the same page regarding what I mean by declarative. In Kubernetes we don’t write infrastructure as code. I know this goes against what many have been taught with regards to thinking about infrastructure, but let me try to explain.

Kubernetes introduces the concept of a Pod, a collection of Linux containers, which represents a single application. Kubernetes also provides a specification for declaring to the cluster how Pods should be deployed. As a result we only need to express our applications like this:

id: helloController
apiVersion: v1beta1
kind: ReplicationController
desiredState:
  replicas: 2
  # replicaSelector identifies the set of Pods that this
  # replicaController is responsible for managing
  replicaSelector:
    name: hello
  # podTemplate defines the 'cookie cutter' used for creating
  # new pods when necessary
  podTemplate:
    desiredState:
      manifest:
        version: v1beta1
        id: hello
        containers:
          - name: hello
            image: quay.io/kelseyhightower/hello:1.0.0
            ports:
              - containerPort: 80
    # Important: these labels need to match the selector above
    # The api server enforces this constraint.
    labels:
      name: hello

The above configuration represents the hello application as a Kubernetes pod composed of a single container, quay.io/kelseyhightower/hello:1.0.0, listening on port 80.

Once we have written this configuration and feed it to the Kubernetes API the various components that make up Kubernetes will take the following steps:

  • Locate nodes capable of running the Pod.
  • Chosen nodes will download and run the quay.io/kelseyhightower/hello:1.0.0 container.
  • A process will monitor the state of the Pods to ensure 2 copies are running at all times.

At this point we have declared to the system that we require 2 instances of the hello pod to be running at all times. If instead we required 1000 instances of the hello app, we could adjust the configuration, then add more machines to the cluster. Once the additional compute resources show up the Kubernetes scheduler would deploy our pods to them until the desired number of pods were running.

If one of the nodes running a pod were to go offline, then a new pod will be created and scheduled to the next eligible node in the cluster. Kubernetes enforces our definition at all times. This is what makes Kubernetes a declarative system.

It’s all about workflow

The dream work flow is that code gets checked in, some tests would run, and things would start magically running in production. For those that have tried pulling this off, you know that’s not an easy thing to do.

Kubernetes offers a step in the right direction. Code gets checked in and then containers are built and pushed to registries. Once this happens the team or person in charge for that service can simply update the Kubernetes configuration to pick up the new container. That workflow looks like this:

kubecfg --image "quay.io/kelseyhightower/hello:2.0.0" rollingupdate helloController

So far I’ve only provided a high level view of Kubernetes, but it only the beginning. Kubernetes provides integration with DNS, load balancers, and even centralized logging. The ideas behind Kubernetes are solid, and anything missing can be easily added to build the right system for your needs.

What’s in it for Sysadmins?

Tools like Kubernetes feel like the system I’ve always tried to build. Some see Kubernetes as a PaaS like Heroku, but that’s far from the case. Kubernetes is a pluggable system that provides a platform for managing application containers at scale.

I just want to be clear, Kubernetes is not some magical system that paints rainbows and trains ponies. It’s a well defined system that captures the experience of fellow sysadmins into an open source project we can all reuse and contribute to. The idea is that Ops would keep Kubernetes and the underlying machines up and running to provide a complete platform for running any application or job required by the team. I like to think of Kubernetes as Ops with an API.

If you would like to learn more about Kubernetes checkout the official website, or take it for a spin with the Intro to Kubernetes Tutorial

December 1, 2014

Day 1 - Docker in Production: Reality, Not Hype

Written by: Bridget Kromhout (@bridgetkromhout)
Edited by: Christopher Webber (@cwebber)

Why Docker?

When I started talking with DramaFever in summer 2014 about joining their ops team, one of many appealing factors was that they’d already been running Docker in production since about October 2013 (well before it even went 1.0). Cutting (maybe bleeding) edge? Sounds fun!

But even before I joined and we were acquired by SoftBank (unrelated events! I am not an acquisition magnet, even if both startups I worked at in 2014 were acquired), DramaFever was already a successful startup, and important technology stack decisions are not made by running a Markov text generator against the front page of Hacker News.

So, why Docker? Simply put, it makes development more consistent and deployment more repeatable. Because developers are developing locally all using the same containers, integration is much easier when their code moves on to their EC2-based personal dev environment, the shared dev environment, QA, staging, and production. Because a production instance is serving code from a container, every new autoscaled instance that has any code at all is going to have the correct code.

As renowned infosec professional Taylor Swift says, “haters gonna hate”. And I’ve been guilty of “get off my lawn” snark about the recent hype, pointing out that containerization isn’t exactly new. We’ve had FreeBSD chroot jails and Solaris Zones (pour one out for Sun Microsystems) for ages. But the genius of Docker is that it provides just enough training wheels for LXC that everyone can use it (for rapidly increasing values of everyone).

Our Own Private Registry

We’re using a local copy of the registry backed by an S3 bucket accessible to those with developer IAM credentials. (If you don’t use AWS, that just means it uses a shared storage location that our devs can access without needing production keys.) Apparently people usually go with a centralized private registry; instead, we traded a SPOF for S3’s eventual consistency. We start the local registry on a host via upstart, and there are a few configuration items of interest:

# this goes in /etc/default/docker to control docker's upstart config
DOCKER_OPTS="--graph=/mnt/docker --insecure-registry=reg.example.com:5000"

Since on some instances we are pulling down multiple Docker images that can be hundreds of megabytes in size, and running or stopped containers also take up room on disk, we use --graph=/mnt/docker to set the root of the docker runtime to the ephemeral disk instead of to the default /var/lib/docker.

With Docker 1.3’s improved security requiring TLS, this means we need to allow our localhost-aliased non-TLS registry.

The docker registry upstart job (used on all EC2 instances) runs these commands:

docker pull public_registry_image
docker run -p 5000:5000 --name registry \
-v /etc/docker-reg:/registry-conf \
-e DOCKER_REGISTRY_CONFIG=/registry-conf/config.yml \
public_registry_image

That second line may require some explanation:

# this is the local port we'll run the registry on
docker run -p 5000:5000 \  

# giving the container a name makes it easier to identify
--name registry \  

# we're mounting in the directory holding a config file that specifies
AWS credentials, etc. Unlike mount(1), this creates the directory it's
mounting to.
-v /etc/docker-reg:/registry-conf \  

# defining the config file location
-e DOCKER_REGISTRY_CONFIG=/registry-conf/config.yml

# the publicly-registered image we're launching this local registry from
public_registry_image

To run locally, we pull the image and then run like this (with DFHOME being where we have the source code checked out):

docker run -d -p 5000:5000 --name docker-reg -v ${DFHOME}:${DFHOME} -e 
DOCKER_REGISTRY_CONFIG=${DFHOME}/config/docker-registry/config.yml
public_registry_image

docker build; docker push

Weekly Jenkins jobs build a base container for the main django app and another that mimics our RDS environment with a containerized, all-data-fixtures-loaded MySQL.

We do trunk-based development with developers submitting pull requests. After being peer-reviewed and merged to master, the new code is available for Jenkins to poll GitHub and build. If all tests pass, then it’s time for exciting post-build action! (What? If you’ve gotten this far in a post about container strategy, then you probably agree with me that this stuff is exciting.)

While all our Go microservices are built essentially the same way, let’s focus on the main django app. Its Dockerfile starts from the weekly base build, as that speeds things up a bit:

FROM our-local-repo-alias:5000/www-base

We keep a number of Dockerfiles around, and in this case, since we have both a base build and a master build for www, we have multiple Dockerfiles in this github repository. Since it’s not possible to pass a file to docker build, it’s necessary to rename the file:

mv 'Dockerfile-www' Dockerfile; sudo docker build -t="67cd893" .

Jenkins builds the new layers for the www master image, tags it with the git SHA, then tests it. Only if it passes the tests do we retag it as dev and then push it to our private docker repository.

sudo docker push our-local-repo-alias:5000/www:'dev'

When we’re ready to cut a release, we build the www-QA job from the release branch. After testing, that same container is re-tagged for staging, then production, and new autoscaling instances will pick it up (giving us the flexibility to do blue/green deploys, which we’re just starting to explore).

Docker in a Mac-using Dev World

Before summer 2014, we were using Vagrant for local development. Building a new image with a local chef-solo provisioner took 17 minutes to install everything, and the local environment diverged enough from the production environment to be annoying. Moving all development into Docker containers proved very effective, especially as we worked through some of the inevitable gotchas and corner cases.

For the local developer environment, we’re using boot2docker, and we’re just about to move back to mainline from a fork that Tim Gross, our head of operations, wrote to get around VirtualBox shared folder mount issues present in previous versions of boot2docker.

One issue we’ve noticed using boot2docker on Mac laptops is that when they wake from sleep, the clock in the VM can be skewed. This means the local registry doesn’t work, since it relies on S3 and S3 expects a correct clock.

$ boot2docker ssh sudo date -u
Mon Nov 24 16:09:02 UTC 2014

$ date -u
Tue Nov 25 01:43:49 UTC 2014

$ docker pull our-local-repo-alias:5000/mysql
Pulling repository our-local-repo-alias:5000/mysql
2014/11/24 19:44:31 HTTP code: 500

Ry4an Brase, our head of back-end development, came up with this delightful incantation:

$ boot2docker ssh sudo date --set \"$(env TZ=UTC date '+%F %H:%M:%S')\"

Adding that to the utils sourced by all our various wrapper scripts (so that devs don’t need to remember a lot of docker syntax to go about their daily lives) seemed like a better alternative than having slackbot deliver it as a reply to all local registry questions.

Containerizing Front-End Dev

I created a container for front-end development which allows us to replicate a front-end environment on Jenkins, using angular, npm, grunt, and bower; you know, the sort of mysterious tools that are inordinately fond of $CWD and interactive prompts.

There are a number of Dockerfiles out there for this; here’s what I found helpful to know (for values of “know” that include “asking Ryan Provost, our head of front-end development” and “mashing buttons until it works”).

Although it defies all logic, node is already old enough to have legacy something. (Insert rant about you kids needing to get off my lawn with your skinny jeans and your fixies.)

RUN apt-get install -y nodejs nodejs-legacy npm

You need a global install of these three; they can’t come from your package.json:

RUN npm install -g grunt-cli@0.1.13
RUN npm install -g bower@1.3.8
RUN npm install -g phantomjs@1.9.7-14

And bower doesn’t want to be installed as root, and sometimes will ask questions that expect an interactive answer:

ADD bower.json /var/www/dependencies/bower.json
RUN cd /var/www/dependencies && bower install --allow-root 
--config.interactive=false --force

The nice thing about having this container is that it allows someone without all the right versions of the front-end tools installed to try out running such parts of the site locally, and it also allows a more replicable deployment of something that will definitely be the same between all the environments as opposed to the “it works on their laptop” fun we all know and love.

Getting the Logs Out

A certain Docker Orthodoxy treats a container as entirely apart from the host instance. Since we aren’t using containers for isolation, we approach this a little differently. Tim blogged about Docker logs when DramaFever first started using Docker.

On EC2 we want to ship logs to our ELK stack, so we mount in a filesystem from the host container:

-v /var/log/containers:/var/log

On local developer machines we want to be able to use a container for active development, editing code locally and running it in the containerized environment. We use the -v flag to mount the developer’s checked-out code into the container, effectively replacing that directory as-shipped:

-v ${DFHOME}/www:/var/www

We still want logs, too, so we expose those for the dev here:

-v ${DFHOME}/www/run:/var/log

Totally Weird Bugs for $1,000, Alex

On an instance where the docker runtime root disk filled up, the container images became corrupt. Even after a reboot, they started yielding inconsistent containers whose behavior would vary over time. For example, a running container (invoked with /bin/bash) would have the ls command, and then a few minutes later, it would not. Eventually, a docker run would lead to errors like these:

Error response from daemon: Unknown filesystem type on /dev/mapper/
docker-202:16-692241-81e4db1aaf5ea5ec70c2ef8542238e8877bbdb4b0
7b253f67b888e713a738dc2-init

Error response from daemon: mkdir /var/lib/docker/devicemapper/mnt/
9db80f229fdf9ebb75ed22d10443c90003741a6770f81db62 f86df881cfb12ae-init/
rootfs: input/output error

It’s likely that we’re seeing one of the devicemapper bugs that seem to plague docker. Replacing the local volume in question was a reasonable workaround.

About Those Race Conditions

A much more prevalent (and annoying) place we’ve run into docker race conditions is in the Jenkins builds. We’d been seeing builds fail with messages like this:

Removing intermediate container 4755dce8cfcc
Step 5 : ADD /example/file /example/file
2014/11/18 18:46:59 Error getting container init rootfs 
a226d3503180de091fde2a410e2b037fde94237dd2171d49a866d43ff03e724c from 
driver devicemapper: Error mounting '/dev/mapper/docker-9:127-14024705-
a226d3503180de091fde2a410e2b037fde94237dd2171d49a866d43ff03e724c-init'
on '/var/lib/docker/devicemapper/mnt/
a226d3503180de091fde2a410e2b037fde94237dd2171d49a866d43ff03e724c-
init': no such file or directory

I added the Naginator plugin so it would retry failed jobs if they’d failed with the most common strings we’d see:

(Cannot destroy container|Error getting container init rootfs)

While that’s an acceptable workaround, I still plan to change what gets reported to Slack, since it’s annoying to have to click on the broken build to find out if it’s just Docker again.

Cron Zombies

A few weeks ago, a developer noticed an unwelcome new message in interactive use on QA:

Error: Cannot start container appname: iptables failed: iptables -t 
nat -A DOCKER -p tcp -d 0/0 --dport 8500 ! -i docker0 -j DNAT --to-
destination 172.17.0.7:8500:  (fork/exec /sbin/iptables: cannot
allocate memory)

On specific instances (such as QA) that aren’t part of the production autoscaling groups, we run cron jobs that invoke a container and give it arguments. A look at the process table showed that multiple days of docker run commands started by cron and the python processes they’d spawned were still running. docker ps disagreed, though; the containers weren’t running anymore, so they weren’t getting cleaned up by these cron jobs:

# remove stopped containers
@daily docker rm `sudo docker ps -aq`
# remove images tagged "none" 
@daily docker rmi `sudo docker images | grep none | awk -F' +' 
'{print $3}'`

At the time, we were starting the cron containers with docker run -i -a stdout -a stderr.

Changing the container-invoking cron jobs to instead use docker run -it cleared it up. It appears a controlling tty was necessary for them to successfully signal their child processes to exit.

Containerize All the Things?

We’re actually running just about everything in containers currently - including the more static bits of our infrastructure. Do Sentry, Jenkins, Graphite, and the ELK stack actually benefit from being in containers? Possibly not; at the time of containerizing all the things, it was the closest thing we had to a configuration management system.

But while it’s excellent for releasing software, it’s a giant hassle sifting through all the changes in a monolithic “config” repo to figure out why the graphite container no longer builds to a working state. Now that we’re using Chef and Packer to drive our AMI creation, we’ll likely move to using Chef cookbooks to manage our next iteration on those infrastructure pieces.

Just because it’s possible to run everything inside a container doesn’t mean it’s useful. While we are no longer using containers as our main method of capturing all configuration, we continue to see great value in using them for consistency throughout development and repeatability of deployments.

Not (Just) Hype

The core of devops is empathy, and it’s important to remember there are people behind all the software we’re discussing. Nathan LeClaire of Docker took to Twitter recently, talking about how it feels to have your project called “marketing BS”. (Let’s pause for a moment while I feel guilty about everything I’ve ever said about MongoDB. I don’t think I ever called it marketing BS, but I’ve definitely made “web scale” jokes.)

Given the recent announcements out of AWS re:Invent about EC2 Container Service, it’s safe to say that containers are about as mainstream as they’re going to get. Do I think ECS is going to be ready for prime time immediately? Anyone who read my sysadvent post from last year about HBase on EMR (Amazon’s training wheels for Hadoop) is saying “lolnope” right about now.

But containers are definitely not just for the Googles of the world anymore, and they’re increasingly no longer just for those of us who are willing to chase devicemapper bugs down a rabbit-hole into GitHub issue land. Docker is the real deal, it works in production, and if you’d like to go stream some dramas powered by it from our site or native apps, you can do that today. (If you’d like to read more about containers, stay tuned for an exciting post tomorrow…)

Sound Like Fun?

If this sounds like exactly the sort of fun you enjoy having at work, we’re hiring ops and dev folks at DramaFever. We’re remote-friendly with NYC and Philly offices. You can read more about our positions on the DramaFever careers page or contact me via email or on twitter. I’d love to talk with you!

December 13, 2013

Day 13 - Controlling a cluster of servers with Serf

Written By: Darron Froese (@darron)
Edited By: Shaun Mouton (@sdmouton)

First - Docker and Dependancy Management

This year's introduction of Docker has been huge for sysadmins everywhere. Whether or not you already understand what Docker can do for you - let me assure you - it has the potential to change how we think, work and build services.

At nonfiction, we host a large number of web applications for customers. Some of those web applications were developed for a specific purpose and because they're often not business critical, they don't get a lot of regular updates. They don't have the budget or the desire to continue working on them year after year - upgrading as techonology matures. As a result, we have a number of pretty old web applications that work pretty well but are not based on current technology.

From a sysadmin perspective, deploying these old applications can be pretty complicated - the dependancies can be pretty hairy and are downright fickle. Although we often us Heroku for many applications, we stil end up having:
  1. Ruby 1.8.x Server
  2. Ruby 1.9.x Server
  3. Specialized application Server for "that" project.
  4. Node.js 0.8.x Server
  5. Really old RHEL Server for that 11 year old PHP 4.x application. (Yes - really.)
That's not awesome at all.

More servers - especially servers that run a very low number of low-traffic apps - seems like a waste of resources, money and time. We are paying for too much capacity in order to get cleaner dependency management. For example, here's the cpu usage for an old Ruby 1.8.x application server:
CPU for the last month for an app server
What a waste.

Docker changes all of that.

With Docker you can run containers for any of your applications and run all of those applications (and more) on a single server. No crazy gem / Ruby version problems - everything in it's own self-contained container. No more "I don't have python 3.3 on that server." or "Sorry - can't compile that version of Node on that old box - gotta move it."

We like Docker so much, that we're building a new service with it providing the backend infrastructure. Our backend is named octohost and is available on Github.

Enter - Serf

The Serf website bills it thusly:
Serf is a decentralized solution for service discovery
and orchestration that is lightweight, highly available,
and fault tolerant.

In short, Serf is a system built to pass messages around and trigger events from server to server - some examples are listed on the website. Instead of building your own messaging system or inventing a new daemon, you can connect a number of servers together using Serf and use it to trigger "events".

We're going to use it to connect some Docker servers together:
  1. Compile server - this server compiles the software into a Docker container and pushes it to the Registry server.
  2. Registry server - this server receives and stores the container. (We are cheating and using the regular Docker INDEX for this.)
  3. Web server - this server pulls the container once it's ready to download and makes it available on the web.
Serf has the concept of Roles where you can tell a particular member of the cluster that it's a "{insert-role-here}" and only the events that apply to that role will be executed.

We're going to create some roles for our servers:
  1. build
  2. serve
  3. master
Let's launch these servers:
ec2-run-instances --key dfroese-naw -g sg-1a3b0e2a --user-data-file user-data-file/master ami-38204508 --region us-west-2
Once we have the IP for that server, we'll launch the others and get them to join the serf cluster:

ec2-run-instances --key dfroese-naw -g sg-1a3b0e2a --user-data-file user-data-file/build ami-38204508 --region us-west-2

ec2-run-instances --key dfroese-naw -g sg-1a3b0e2a --user-data-file user-data-file/serve ami-38204508 --region us-west-2
We're using Amazon's User Data system to:
  1. Set the system's Serf role.
  2. Download the Serf event handlers.
  3. Activate those handlers.
  4. Join the cluster by connecting to the first 'master' system.
  5. Any additional setup for that role as needed. Take a look at the user-data-files here.
Now that we've got the systems connected - let's send some test events.

serf event role-check

When that event is sent, each system executes /etc/serf/handlers/role-check.sh - this is some of the output:

2013/12/02 23:07:21 Requesting user event send: role-check. Coalesced: true. Payload: ""
2013/12/02 23:07:22 [INFO] agent: Received event: user-event: role-check
2013/12/02 23:07:22 [DEBUG] Event 'user' script output: ip-10-250-69-116 role is master
2013/12/02 23:07:22 [DEBUG] Event 'user' script output: ip-10-225-185-80 role is serve
2013/12/02 23:07:22 [DEBUG] Event 'user' script output: ip-10-227-14-222 role is build
You can also watch what's going on through the entire cluster:
serf monitor

2013/12/02 23:07:21 Requesting user event send: role-check. Coalesced: true. Payload: ""
2013/12/02 23:07:21 [DEBUG] serf-delegate: messageUserEventType: role-check
2013/12/02 23:07:21 [DEBUG] serf-delegate: messageUserEventType: role-check
2013/12/02 23:07:21 [DEBUG] serf-delegate: messageUserEventType: role-check
2013/12/02 23:07:21 [DEBUG] serf-delegate: messageUserEventType: role-check
2013/12/02 23:07:21 [DEBUG] serf-delegate: messageUserEventType: role-check
2013/12/02 23:07:22 [INFO] agent: Received event: user-event: role-check
2013/12/02 23:07:22 [DEBUG] Event 'user' script output: ip-10-250-69-116 role is master
2013/12/02 23:07:23 [INFO] serf: EventMemberFailed: ip-10-225-185-80 10.225.185.80
2013/12/02 23:07:24 [INFO] agent: Received event: member-failed
2013/12/02 23:07:27 [INFO] Responding to push/pull sync with: 10.250.65.99:33341
2013/12/02 23:07:27 [INFO] serf: EventMemberJoin: ip-10-225-185-80 10.225.185.80
2013/12/02 23:07:28 [INFO] agent: Received event: member-join
2013/12/02 23:07:51 [INFO] Initiating push/pull sync with: 10.225.185.80:7946

Now let's do something useful.

Let's tell the Docker server cluster to:
  1. Compile a git repo.
  2. Push it to the Docker INDEX
  3. Have another server pull that container.
  4. Then launch it so it's viewable from the web.
Here's how we kick it off:
serf event build https://github.com/darron/sysadvent-harp.git,sysadvent/harp-example
Which outputs:

Event 'build' dispatched! Coalescing enabled: true
2013/12/03 21:07:23 [INFO] Initiating push/pull sync with: 10.249.27.132:7946
2013/12/03 21:07:23 [DEBUG] serf-delegate: messageUserEventType: build
2013/12/03 21:07:23 [DEBUG] serf-delegate: messageUserEventType: build
2013/12/03 21:07:23 [DEBUG] serf-delegate: messageUserEventType: build
2013/12/03 21:07:23 [DEBUG] serf-delegate: messageUserEventType: build
2013/12/03 21:07:24 [INFO] agent: Received event: user-event: build
2013/12/03 21:08:51 Requesting user event send: pull. Coalesced: true. Payload: "sysadvent/harp-example"
2013/12/03 21:08:51 [DEBUG] Event 'user' script output: Build: https://github.com/darron/sysadvent-harp.git as sysadvent/harp-example in /tmp/tmp.WcrVsKOn1m
The build starts:

Cloning into '/tmp/tmp.WcrVsKOn1m'...
/usr/bin/docker build -t sysadvent/harp-example /tmp/tmp.WcrVsKOn1m
Uploading context 317440 bytes
Step 1 : FROM octohost/nodejs
---> 62108a2c615f
Step 2 : ADD . /srv/www
---> 5b01d85275dd
Step 3 : RUN cd /srv/www; npm install
---> Running in b136131fcb72
npm WARN package.json Harp@1.0.0 No repository field.
npm http GET https://registry.npmjs.org/harp
npm http 200 https://registry.npmjs.org/harp

### removed lots of npm output

harp@0.8.13 node_modules/harp
├── mime@1.2.9
├── async@0.2.9
├── mkdirp@0.3.4
├── commander@1.1.1 (keypress@0.1.0)
├── fs-extra@0.3.2 (jsonfile@0.0.1, ncp@0.2.7, rimraf@2.0.3)
├── jade@0.27.7 (commander@0.6.1, coffee-script@1.4.0)
├── less@1.3.1
├── connect@2.7.0 (fresh@0.1.0, cookie-signature@0.0.1, debug@0.7.4, pause@0.0.1, cookie@0.0.5, bytes@0.1.0, crc@0.2.0, formidable@1.0.11, qs@0.5.1, send@0.1.0)
└── terraform@0.4.12 (lru-cache@2.3.0, marked@0.2.8, ejs@0.8.4, coffee-script@1.6.3, jade@0.28.2, stylus@0.33.1, less@1.3.3)
---> af20e73caec4
Step 4 : EXPOSE 5000
---> Running in 2fce89dcbaa9
---> 688c52c7f926
Step 5 : CMD cd /srv/www; /usr/bin/node server.js
---> Running in 193e082814c4
---> 5a69fee1c103
Successfully built 5a69fee1c103
The build server pushes the built container to the registry and kicks off the pull:

Login Succeeded
The push refers to a repository [sysadvent/harp-example] (len: 1)
Sending image list
Pushing repository sysadvent/harp-example (1 tags)

### Lots of output removed.
Then the server in the 'serve' role pulls the container:

Event 'pull' dispatched! Coalescing enabled: true
2013/12/03 21:08:51 [DEBUG] serf-delegate: messageUserEventType: pull
2013/12/03 21:08:52 [DEBUG] serf-delegate: messageUserEventType: pull
2013/12/03 21:08:52 [INFO] agent: Received event: user-event: pull
2013/12/03 21:08:52 [DEBUG] Event 'user' script output: Pull: sysadvent/harp-example
Pulling repository sysadvent/harp-example
5a69fee1c103: Pulling image (latest) from sysadvent/harp-example5a69fee1c103: Pulling image (latest) from sysadvent/harp-example, endpoint: https://cdn-registry-1.docker.io/v1/5a69fee1c103: Pulling dependent layers

### More output removed.
So that it can run it:

2013/12/03 21:10:16 [DEBUG] serf-delegate: messageUserEventType: run
2013/12/03 21:10:16 [DEBUG] serf-delegate: messageUserEventType: run
2013/12/03 21:10:16 [DEBUG] serf-delegate: messageUserEventType: run
2013/12/03 21:10:17 [INFO] agent: Received event: user-event: run
2013/12/03 21:10:23 [INFO] Responding to push/pull sync with: 10.231.7.223:60001
2013/12/03 21:10:24 [WARN] Potential blocking operation. Last command took 46.701165ms
2013/12/03 21:10:29 [DEBUG] Event 'user' script output: Run: sysadvent/harp-example
At the end of this process, the site was available at:
http://harp-example.54.202.94.50.xip.io/

Which looked like this:


To sum up

Serf is a new tool that has been added to our toolboxes as sysadmins.

It's very powerful, simple to setup and can be extended in almost limitless ways.

Give Serf a try. My example Serf handlers are all available here. You can even use the same AMI that I used for this article - ami-38204508.

Let me know if you've got any questions!

December 14, 2010

Day 14 - FreeBSD Jails

This article was written by Wesley Shields

The first day of SysAdvent talked about Linux Containers (LXC), and how they are an "operating system level virtualization", as opposed to "platform virtualization" choices like Xen or VMWare. Today, I'll focus on jails in FreeBSD and how they achieve a similar goal.

Background

If you think of a traditional OS it looks something like this:

Among other things, the kernel controls access to hardware, makes sure processes are not stomping all over each other's memory, does the necessary access control checks for actions, and also ensures that packets land at the appropriate sockets. However, even if everything is perfect, a sufficiently privileged process can cause a lot of havoc if it misbehaves.

Let's say you have a process running as root that gets compromised and is now running arbitrary code of the attacker's choosing. Typically, this comes in the form of a shell listening on a socket, or a connect-back shell; both of which are very bad. Through this arbitrary execution of code, the attacker can do whatever he/she is allowed to through the access controls in place. In most cases (where things like mandatory access controls are not in place) this is effectively game over for the system administrator.

Before I go any further, I should probably explain how jails are different from what most people are familiar with. To be consistent with the earlier article I'll call it "platform virtualization." That solution looks something like this:

There are different approaches but it is essentially the same goal. Insert a small "virtual machine monitor" (VMM) layer - often called a hypervisor - that brokers access to the real hardware and emulates whatever hardware the systems administrator wants to the OS running on top of it. Modern chips have support for helping do this in hardware (AMD calls it "AMD-V" and Intel calls it "VT-x"). Just about every chip shipping now has this built in.

"Platform virtualization" has many benefits to it. You can choose what hardware to provide to the guest operating system. The VMM is hopefully small enough that it can be properly secured and verified. Finally, you can decide which operating system to run as the guest. The fact that it is virtualized should be transparent - with the exception of needing driver support for whatever hardware is emulated, which every major OS has support for.

There are some drawbacks to this approach. It can be very resource intensive as the more virtual machines you spin up the more hardware and state has to be kept in memory. With modern hardware this is becoming less of a problem, but, for some environments, it may still hold true.

Enter Jails

Jails are best thought of as a means to contain and isolate processes from each other, even if those processes are privileged.

In this case, we are running multiple processes, but the kernel has been modified to limit the resources that each process can affect or view. This is the concept upon which jails are built: The name of the game is process isolation and containment, not virtualization.

The Details

I'm going to skip over the details of how jails are created and what that means from a data structure standpoint and skip straight to how to set up a jail and how to use it. My examples will be from a fairly recent development snapshot ("current," if you are familiar with FreeBSD terminology) that is not yet a finished release, so some of the things I will describe are not completely accurate to all versions of FreeBSD but the concepts are the important part.

Jails have been around in FreeBSD for a long time now. They were first introduced in FreeBSD 4.0 (10 years ago). Since that release, jails have been refined and extended to support many of the things people want from them. Recent releases of FreeBSD include the ability for IPv6, hierarchical jails, resource utilization limits, and even virtual network stacks (which is out of scope for this article).

Setup

For the purposes of this article, I'm going to use the term host to indicate the FreeBSD base operating system upon which the jails will run.

A jail only requires a handful of things in order to operate. The most important of which is a working userland. Usually, people run the same version of the userland inside a jail as the one that is running on the host, but it doesn't have to be this way. If you want to run an older userland in a jail, it will likely work because backwards compatibility in the kernel is usually preserved.

Actually getting a working userland is outside of the scope of this article. There are many ways to pick from: building your own, using your existing install, or installing the binaries straight from release media. The means of getting the binaries on disk is up to you. You also don't need a full world (FreeBSD's term for the base OS), if you know exactly what you are doing you can populate it with just what you need. There are also other tricks you can do involving null mounting in other paths. For the purposes of this article I've installed an exact copy of my host to /jails/test (minus any package installations).

Starting Jails

With a world installed to /jails/test, I don't need anything else installed in order to start a jail. Everything you need is provided by the base FreeBSD install. Starting a jail manually is done using the jail(8) command.

wxs@ack wxs % sudo jail /jails/test test 192.168.1.100 /bin/sh 
# id
uid=0(root) gid=0(wheel) groups=0(wheel),5(operator)
# 

The arguments to the jail command are pretty straight forward. It takes a path where the root of the jail should live, a hostname, an IP address and a command inside the jail to run. Once I'm inside the jail you can see that I am automatically the root user.

From outside of the jail, on the host, you can use the jls(8) command to list existing jails.

wxs@ack wxs % jls
   JID  IP Address      Hostname                      Path
     4  192.168.1.100   test                          /jails/test
wxs@ack wxs % 

The one catch is that while the jail says it has an IP address, the host OS knows nothing about that IP address. In order to have your jail respond to an IP address you must add it to a network interface by adding an alias:

wxs@ack wxs % sudo jail -r 4 # Kill existing jail, so it can get the new IP
wxs@ack wxs % sudo ifconfig bge0 alias 192.168.1.100 netmask 255.255.255.255
wxs@ack wxs % sudo jail /jails/test/ test 192.168.1.100 /bin/sh
#

And now inside our jail we can see that we have an IP address.

# ifconfig bge0 | grep inet
        inet 192.168.1.100 netmask 0xffffffff broadcast 192.168.1.100
# 

So, at this point, we just need a working devfs inside our jail and we should have a normal, contained, system.

wxs@ack wxs % sudo mount -t devfs devfs /jails/test/dev
wxs@ack wxs % mount | grep jails
data/jails on /jails (zfs, local, noatime)
devfs on /jails/7/dev (devfs, local, multilabel)
devfs on /jails/test/dev (devfs, local, multilabel)
wxs@ack wxs % 

You might want your jail to be accessible from the network, so let's run an SSH daemon! I'll permit root logins for this jail, just to make my life easier, but you can add users inside jails as normal. Also, be sure to set your root password before you expose any network services. From the root shell you have after running the jail(8) command, do this:

# sed -i '.bak' -e 's/^#PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config
# /etc/rc.d/sshd onestart
[... Lots of output about host key generation ...]
# sockstat -4l | grep 22
root     sshd       61052 3  tcp4   192.168.1.100:22      *:*
# 

And outside the jail:

wxs@ack wxs % ssh root@192.168.1.100
The authenticity of host '192.168.1.100 (192.168.1.100)' can't be
established.
RSA key fingerprint is f9:87:e1:41:3c:27:56:fd:5a:0e:c9:0b:c5:9a:d5:15.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.1.100' (RSA) to the list of known hosts.
Password:
Last login: Mon Dec  6 01:43:39 2010 from 192.168.1.100
FreeBSD ?.?.?  (UNKNOWN)

Welcome to FreeBSD!

[... MOTD ...]
test#

A keen eye would spot the weirdness '?.?.?' in the MOTD above. Normally that is cleared up when the computer boots, but since we didn't really "boot" this jail, that step never happened. Let's explore what it takes to get a jail to start automatically upon boot.

Booting Automatically

One thing you must do when starting a jail is make sure the host services are set to listen on only IP addresses that belong to the host, and not the jail. Failure to do this will cause your host services to listen on IP addresses that should be for the jail. This can have unintended consequences such as exposing host services to places they shouldn't be. For now I'm assuming you know how to do that.

Like most things in FreeBSD they are controlled with settings in /etc/rc.conf. There are actually a whole bunch of settings available, but here's the simple set I'm using:

wxs@ack head % fgrep test /etc/rc.conf  
jail_list="test"
jail_test_rootdir="/jails/test"
jail_test_hostname="test"
jail_test_interface="bge0"
jail_test_ip="192.168.1.100"
jail_test_devfs_enable="YES"
wxs@ack head % 

Using this configuration, the 'test' jail will boot and start automatically.

Booting Manually

You can use the /etc/rc.d/jail script to boot a jail manually provided that the appropriate settings are set in /etc/rc.conf. Another option is to set the IP address alias manually and mount devfs manually then call /etc/rc yourself:

wxs@ack wxs % sudo jail -r 2 # Kill existing jail...
wxs@ack wxs % sudo jail /jails/test test 192.168.1.100 /bin/sh /etc/rc
/etc/rc: WARNING: $hostname is not set -- see rc.conf(5).
Creating and/or trimming log files.
Starting syslogd.
ELF ldconfig path: /lib /usr/lib /usr/lib/compat
32-bit compatibility ldconfig path: /usr/lib32
Clearing /tmp (X related).
Updating motd:.
Starting cron.

Sun Dec 12 16:16:37 UTC 2010
wxs@ack wxs % jls
   JID  IP Address      Hostname                      Path
     3  192.168.1.100   test                          /jails/test
wxs@ack wxs % 

Restrictions

So if jails are all about isolation and containment, what can and what can't you do inside a jail? The general rule of thumb is that if it affects the host or other jails it is restricted by default. There are knobs you can turn to allow these things, but in the interests of not breaking the security model, they are turned off by default. Exactly what is restricted and what knobs are available is highly dependent upon the version of FreeBSD you are running. As more and more things are being designed to work better with jails, the set of restricted operations is shrinking. For example, in earlier releases root inside a jail was not allowed to change any network stack configuration information. With the addition of virtualized network stacks in newer releases of FreeBSD this restriction is gone, provided the jail is using a virtualized stack.

For more information on this it is best to read the documentation available.

Trade offs

Jails are a great way of getting "operating system level virtualization" on FreeBSD, but like anything else, they come with a series of trade-offs which must be considered prior to implementation.

A kernel level compromise does break the isolation provided by jails. In a "platform virtualization" solution it would require a bug in the hypervisor for that to happen. Jails are not necessarily any more or less secure than a "platform virtualization" solution as it is going to come down to implementation details. Bugs do happen in both worlds.

Another trade-off is that a jail can not emulate arbitrary hardware like a VMM can. If you want to add a new virtual disk to your VM in a "platform virtualization" solution it is a simple operation - the physical disk doesn't really exist as it is just a file on the filesystem of the host. In a jail you can not emulate arbitrary hardware.

As a jail is really just isolating processes from each other, it is important to realize that root on the host has complete control over every jail. This is an important thing to keep in mind when setting up a jail environment. Root on the host should be trusted and controlled far more than root in any of the jails.

Lastly, a user on the host can get access to things inside one of the jails if the UIDs are the same. For example, on one of my hosts my UID is 1001, and inside one of the jails a different user (user1) has UID 1001. From the viewpoint of user1, only he has access to his files inside the jail. From my viewpoint, outside of the jail, the files are owned by me. The host is going to use it's copy of /etc/passwd to determine ownership of files, which means there can be overlapping information. This is another important consideration to keep in mind when setting up a jail environment.

Uses

There are many uses for jails. Lots of places use them to isolate web hosting environments from each other, to provide root inside a contained system for a customer, and to isolate developers from each other. I personally use them to isolate test environments from each other. As a developer who spends most of his time up in userland, this is a great solution to my need to be able to quickly setup a clean test environment. As you spend more time with jails, you begin to see different opportunities for application.

As more parts of FreeBSD become friendlier to jails you can start to build very interesting things. Virtual network stacks, zfs, multi-IP jails, hierarchical jails and many other things are fertile areas for exploration as a systems administrator. As is often the case, the best way to get familiar with jails is to dive in heads first!

Further Reading