Introducing packer: building immutable infrastructure in the cloud

martin.bach's picture

After having spent a bit of time with Packer to create Vagrant base boxes it was time to focus on the cloud. I have referenced Packer multiple times in my cloud talks as a popular way to create immutable infrastructure and/or custom images, and I wanted to share how you could potentially make use of the technology

Be aware that this is a post about cloud technology. If you are following along, be aware that you will incur cost.

The case for using Packer

Saying it with my words, Packer is a tool allowing me to build immutable infrastructure. It does so by using a base image a cloud provider offers, spinning it up temporarily and installing the software you want to use. The result is then converted to a custom image for later use.

Think for example you would like to deploy Oracle 19.8.0 binaries across your cloud infrastructure. One way to get there would be to create a VM, followed by running Ansible playbooks to install Oracle 19.8.0. That’s a perfectly fine approach! However, bear in mind that every execution of an Ansible playbooks might fail, and it certainly takes a bit of time for the playbook to finish.

The alternative is to create a custom image with Oracle 19.8.0 already installed. f you want to be absolutely sure all of your deployments are identical, Packer might be the tool for you. Deploying a custom image should take a lot less time than the VM + Ansible approach, too. And once you validated the custom image by means of rigorous regression testing, it should provide a lot of confidence. A long time ago many of us tried to achieve the same goal by using custom RPMs for example, a much less reliable approach compared to what we can do these days.

Oh, and by the way: regardless which way you choose to deploy Oracle software, you have to always ensure you are license compliant.

Let’s give it a try!

In this post I won’t install Oracle 19.8.0, I’ll stick with XE to keep it reasonably simple. The overall process is identical though, which should allow you to plug your Ansible playbooks into the Packer template with ease.

My build environment is based on Packer 1.6 running on top of Ubuntu 20.04 LTS. I am using Oracle Cloud Infrastructure.

Building Oracle XE in Packer

The process is similar to what I described in my first post about Packer, so please head over there for additional background. Unlike building a Virtualbox VM based on a local ISO image, Packer has to know which base OS image to use. It also has to be able to spin it up, so a Virtual Cloud Network as well as a subnet with appropriate security lists are needed. I have written how to create these via Terraform in a separate post.

Most examples about Oracle OCI and Packer don’t appear overly secure to me: you find username/password combinations used pretty much everywhere. I prefer to stick to SSH keys instead.

When configuring the JSON template for Packer you can make use of an existing Oracle Cloud CLI configuration file, or use variables very similar to those the Terraform provider for OCI requires as well. I don’t need a local installation of the OCI CLI since the cloud shell became GA so I went with the latter approach.

To save me some work I’m using the same shell variables in Packer as I do in Terraform. Packer 1.6 can read environment variables when invoked, but as far as I understand it only does so in the “variables” section. This leads to funny looking code, but it works. It might of course be so that I’m wrong, and I’ll happily stand corrected. Please let me know!

Packer Template

Let’s get started with the Packer JSON template. It is essentially a faithful implementation of the documented options of the oci-oracle builder.

 
{
    "variables": {
        "oci_tenancy_ocid": "{{env `TF_VAR_tenancy_ocid`}}",
        "oci_user_oci": "{{env `TF_VAR_user_ocid`}}",
        "oci_compartment_ocid": "{{env `TF_VAR_compartment_ocid`}}",
        "oci_api_key_fingerprint": "{{ env `TF_VAR_fingerprint`}}",
        "oci_api_key_file": "{{ env `TF_VAR_private_key_path`}}",
        "oci_region": "eu-frankfurt-1"
    },
    "builders": [
        {
            "type": "oracle-oci",
            "tenancy_ocid": "{{user `oci_tenancy_ocid`}}",
            "user_ocid": "{{user `oci_user_oci`}}",
            "compartment_ocid": "{{user `oci_compartment_ocid`}}",
            "fingerprint": "{{ user `oci_api_key_fingerprint`}}",
            "key_file": "{{user `oci_api_key_file`}}",

            "region": "{{user `oci_region`}}",
            "availability_domain": "IHsr:EU-FRANKFURT-1-AD-1",
            "subnet_ocid": "ocid1...",

            "base_image_ocid": "ocid1.image.oc1.eu-frankfurt-1.aaaaaaaahxue6crkdeevk75bzw63cmhh3c4uyqddcwov7mwlv7na4lkz7zla",
            "image_name": "oracle-xe-ol7.8",
            "shape": "VM.Standard.E2.1.Micro",
            "ssh_username": "opc",
            "ssh_agent_auth": true,
            "ssh_timeout": "10m",
            "metadata" : {
                "ssh_authorized_keys" : "ssh-rsa ... key"
            }
        }
    ],
    "provisioners": [
        {
            "type": "ansible",
            "user": "opc",
            "playbook_file": "ansible/xe.yml",
            "extra_arguments": [ "-v" ]
        }
    ]
} 

Let’s have a look at the various sections in the Template.

Variables

The first section, “variables”, allows me to read environment variables. I’m using the same environment variables for Packer and Terraform, hence the somewhat strange looking names. The variables section populates Packer variables I prefixed “oci”.

Builders

The builders section is where the actual action starts. I need to provide the usual suspects (tenancy OCID, user OCID, API key location and fingerprint etc) as per the documentation. The first part of the “builders” section should be self-explanatory. Well maybe apart from the fact I couldn’t use environment variables in this part of the template, hence the reference to my user variables declared earlier. Again, if there is a better way of doing this, please let me know.

The middle part of the code is concerned with resource location. As I live in Germany the easiest option is to use EU-Frankfurt-1. One of the pre-requisites I need to complete before Packer can go and do its magic is to define a public “build” subnet. I have an automatically, pre-created separate build network (VCN) for this purpose. All I need is to grab the OCID for my subnet and provide it to Packer.

The third part of the builders section is where all the fun starts:

  • I chose the base image to be the latest Oracle Linux 7.8 image at the time of writing. More information about Oracle Cloud IaaS images and their corresponding OCIDs can be found in the official documentation reference
  • The new custom image to be created is to be named oracle-xe-ol7.8
  • I would like the VM shape for the build to be VM.Standard.E2.1.Micro

As with my Vagrant base box I’ll use SSH authentication rather than a more insecure username/password combination. To do so I need to use the local ssh agent to store and forward my key to the VM. I’m providing that exact key via the metadata directive. Once the VM has been created, opc’s authorized_keys file contains my SSH key allowing me to connect.

Provisioners

In comparison to the earlier section this is rather uneventful. The only action it performs is to call the Ansible playbook responsible for the installation of Oracle XE. The playbook is too boring to show it here, all it does is update all RPMs before it installs the Oracle XE RPM.

Thanks to the SSH settings provided earlier no passwords have to go over the wire. If the provisioner fails, you most likely didn’t add your SSH key to the agent. Like I did ;)

==> oracle-oci: Waiting for SSH to become available...
==> oracle-oci: Error waiting for SSH: Packer experienced an authentication error when trying to connect via SSH. 
 This can happen if your username/password are wrong. You may want to double-check your credentials as 
 part of your debugging process. original error: ssh: handshake failed: ssh: unable to authenticate, attempted methods 
 [publickey none], no supported methods remain
==> oracle-oci: Terminating instance 

This is super easy to fix: once the prompt returns you add the key to the agent using ssh-add and restart the build.

End result

After a little while, Packer build will finish and create a new custom image with its own OCID for use in later deployments.

$ ANSIBLE_STDOUT_CALLBACK=debug packer build oracle-xe-oci.json 
oracle-oci: output will be in this color.

==> oracle-oci: Creating temporary ssh key for instance...
==> oracle-oci: Creating instance...
==> oracle-oci: Created instance (ocid1.instance.oc1.eu-frankfurt-1.ant...).
==> oracle-oci: Waiting for instance to enter 'RUNNING' state...
==> oracle-oci: Instance 'RUNNING'.
==> oracle-oci: Instance has IP: 11.22.33.44.
==> oracle-oci: Using ssh communicator to connect: 11.22.33.44
==> oracle-oci: Waiting for SSH to become available...
==> oracle-oci: Connected to SSH!
==> oracle-oci: Provisioning with Ansible...
    oracle-oci: Setting up proxy adapter for Ansible....
==> oracle-oci: Executing Ansible: ansible-playbook -e packer_build_name="oracle-oci"...
    oracle-oci: Using /etc/ansible/ansible.cfg as config file
    oracle-oci:
    oracle-oci: PLAY [all] *********************************************************************

[...]

    oracle-oci:
    oracle-oci: PLAY RECAP *********************************************************************
    oracle-oci: default : ok=11   changed=9    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    oracle-oci:
==> oracle-oci: Creating image from instance...
==> oracle-oci: Image created.
==> oracle-oci: Terminating instance (ocid1.instance.oc1.eu-frankfurt-1.aaa)...
==> oracle-oci: Terminated instance.
Build 'oracle-oci' finished.

==> Builds finished. The artifacts of successful builds are:
--> oracle-oci: An image was created: 'oracle-xe-ol7.8' (OCID: ocid1.image.oc1.eu-frankfurt-1.aaa) in region 'eu-frankfurt-1' 

As you can see, a new custom image has been created. You can read more about using custom images in a later post.

Summary

Compared to my first attempt at using Packer to build a Vagrant base box, using Packer with Oracle Cloud Infrastructure turned out to be a lot easier. The only tricky bit is the use of SSH, however that’s quickly overcome if you ever used the OCI Terraform provider and know about the metadata directive.

To prevent automated spam submissions leave this field empty.