Building Immutable Servers with Packer | Linux
A long time ago, sysadmins were responsible for constantly caring for and feeding their infrastructure. Then came technologies like Puppet, Chef, and Ansible, which have made it so much easier to maintain a desired state. With the rise of AWS, Google, and Azure, there has been a shift away from using configuration management to maintain infrastructure and toward the use of more ephemeral and disposable servers. These servers are called Immutable Servers because they are fixed. If you need to make updates, or you are done using them, you simply throw them away and spin up a replacement.
Deploying onto an existing server and overwriting what was there before can make rolling back your code a bit of a hassle. Instead, using immutable servers, sysadmins can deploy a whole new batch of instances using a pre-baked image that replaces the old batch of instances.
The creation process has several steps. A new instance gets booted up either from a configurable base image or from a previously built image. To get the instance to the desired state, it needs to be configured. This is where your configuration management tools come in.
The application will be baked in with all of its dependencies and middleware. A snapshot of the instance is then saved, and a test instance is then booted up using the image that was created. Next, automated tests are run against the test instance to ensure that everything is configured properly and that the application functions as intended. Once the image is validated, use your CI or CD tools to execute the deployment process. The new instances will replace the old instances using a deployment strategy like Blue-Green or Canary Deployment.
Now that you have a better idea of what immutable servers are and how the creation process works, let’s look at how to use Packer to create a Docker image. I’m going to assume you are running CentOS 7 and that you already have Docker installed. The first thing you will need to do is download the Packer binary into /usr/local/bin:
yum install -y wget unzip cd /usr/local/bin wget https://releases.hashicorp.com/packer/1.2.5/packer_1.2.5_linux_amd64.zip unzip packer_1.2.5_linux_amd64.zip
With Packer downloaded, navigate to the directory where you will be writing your Packer file. You can test to make sure Packer is working by executing:
packer --version
The Packer template that we are going to create will:
(1) contain two user variables
(2) use the Docker builder and the shell provisioner to bake in our code and its dependencies
(3) use a post-processor to create the correct tags.
Use your favorite IDE or text editor to create the Packer template packer.json. Packer templates are JSON documents, so everything will be wrapped in curly braces. We are going to be using two variables, repository and tag, with our post-processor later. User variables are key-value pairs, and we need to assign defaults when they’re defined. If you don’t want to assign a default, you can supply an empty string.
User variables are called with the {{user}} function. Note that the variable name is nested between back ticks (`), not single quotes (‘). For example:
{{user `var_name`}}
Here is what the variable section will look like when added to the Packer template:
{ "variables": { "repository": "la/express", "tag": "1.0" } ... }
Next, we will focus on the builder section. The builder section creates the machine that the image will be built from. It is an array because you have the ability to specify multiple builders. In this example, we will be using the Docker builder. Here is what we will add to our template:
{ ... "builders": [ { "type": "docker", "author": “Your Name", "image": "node", "commit": true, "changes": [ "EXPOSE 3000" ] } ] ... }
Let’s break this down. The type specifies the type of builder we will be using. Since we are creating a Docker image we are going to use the docker type. author will be used to set the maintainer of the image. image tells Packer what our base image will be. We will be using the node image from Docker Hub. When commit is set to true, the image will be committed rather than exported. changes allows us to overwrite some of the Docker image’s metadata. In this instance, we want to make sure that the exposed port is set to 3000 because that’s the port our application will be running on.
Next, we will set up our provisioner. Provisioners are how Packer configures a machine image after it boots up. In this case, we will update the operating system packages, download a tag of the source code, and then configure the application dependencies. Below is what we will add to the Packer template next:
{ ... "provisioners": [ { "type": "shell", "inline": [ "apt-get update -y && apt-get install curl -y", "mkdir -p /var/code", "cd /root", "curl -L https://github.com/linuxacademy/content-nodejs-hello-world/archive/v1.0.tar.gz -o code.tar.gz", "tar zxvf code.tar.gz -C /var/code --strip-components=1", "cd /var/code", "npm install" ] } ] ... }
Much like builder, the provisioners parameter is an array that allows us to use multiple provisioners to configure the image. Our configuration is pretty simple, so the shell provisioner is all we need to use. If you have a more sophisticated configuration, you can use configuration management tools like Puppet, Chef, or Ansible in conjunction with the shell. The inline property takes an array of shell commands that will be executed in order of definition.
The last section is post-processors. Post-processors are also arrays and are executed after the image has been built and provisioned. They are typically used to perform actions not related to the building and provisioning of the image. These include uploading artifacts, building packages, and much more. We will be using the Docker Tag post-processors to tag our finished Docker image.
{ ... "post-processors": [ { "type": "docker-tag", "repository": "{{user `repository`}}", "tag": "{{user `tag`}}" } ] ... }
In the future, we could have a CI/CD tool like Jenkins kick off the build, so we want the image to be tagged dynamically. This is where the user variables that we created earlier come in.
Now that we’ve covered all the moving parts, here is what the completed Packer template will look like:
packer.json: { "variables": { "repository": "la/express", "tag": "1.0" }, "builders": [ { "type": "docker", "author": "Travis Thomsen", "image": "node", "commit": true, "changes": [ "EXPOSE 3000" ] } ], "provisioners": [ { "type": "shell", "inline": [ "apt-get update -y && apt-get install curl -y", "mkdir -p /var/code", "cd /root", "curl -L https://github.com/linuxacademy/content-nodejs-hello-world/archive/v1.0.tar.gz -o code.tar.gz", "tar zxvf code.tar.gz -C /var/code --strip-components=1", "cd /var/code", "npm install" ] } ], "post-processors": [ { "type": "docker-tag", "repository": "{{user `repository`}}", "tag": "{{user `tag`}}" } ] }
Before you execute the build, it’s always a good idea to validate the template first. Execute the Packer validate
command:
packer validate packer.json
If there are any errors, go fix them and run the validate
command again. Once your template comes back clean, it’s time to execute the build. When executing packer build
, we will use the -var
flag to set the variables that are defined in the template:
packer build -var “repository=some_repository_name” -var “tag=some_tag_number” packer.json
After you execute the build, you should have an image that’s ready to be used. Get a list of the Docker images to validate that the creation of the image was successful.
docker images
If you see an image that has the tags you used when building the image, then your job is done! Now go forth and put this knowledge to good use.
For the latest Docker content, check out the Docker online training tool we released in July as part of #TheMostContentInHistory content launch!
The post Building Immutable Servers with Packer appeared first on Linux Academy Blog.