Monday, August 18, 2014

Create a base docker image (RHEL) from vagrant

We'll be creating a RHEL 6.5 base docker image using vagrant and VirtualBox. This is a private image, but the process will be the same with any image you have. This will also work with any other yum-oriented linux distro.

Note: I had trouble creating this image from a VM based on a similar image hosted on VMWare, so that is why I decided to build this in vagrant. Also, vagrant can be run from anywhere, which makes this a much more portable solution.

I won't go through the installation process for vagrant and VirtualBox as it's different for each platform, but both of these need to be installed prior to the next step.

We need to create a Vagrantfile to start our instance. This will be a very simple instance, but feel free to use any that you already have available.

Vagrant.configure("2") do |c|
  c.vm.box = "rhel65-1.0.0"
  c.vm.box_url = "http://example.com/rhel65/1.0.0/rhel65-1.0.0.box"
  c.vm.hostname = "default-rhel65-100.vagrantup.com"
  c.vm.synced_folder ".", "/vagrant", disabled: true
  c.vm.provider :virtualbox do |p|
  end
end

Make sure you change the url to point to your .box file.

Now, open a terminal to the directory where you saved the Vagrantfile we just created and type:

vagrant up



This will create the VM and start it up based on the settings from the Vagrantfile. Please refer to the vagrant docs for additional configuration options, but none are needed for creating the docker image.

Next, we'll need to login on the vagrant image by issuing the command:

vagrant ssh

My instance logs me in as the vagrant user with sudoer powers. We will need sudo or root access to create the base image. This is required to access and read all of the system files that need to be copied to the image.

We need to install docker so our base image can actually be stored and pushed to a repo after it's created. You'll notice in the script later that docker commands are called from the script to commit the image with the name provided. We'll install the EPEL and then docker-io.

sudo rpm -ivh http://mirror.pnl.gov/epel/6/i386/epel-release-6-8.noarch.rpm
sudo yum install -y docker-io

Inside the vagrant image, we need to clone the docker repo and cd to the docker/contrib directory:

sudo yum install -y git # I had to install git first as it wasn't in the image I'm using
git clone https://github.com/docker/docker.git
cd docker/contrib

This is where the magic script is stored. I have added it here in case it breaks in the future, but always try the version in the repo as long as it exists. The file we need is mkimage-yum.sh

    #!/usr/bin/env bash
    #
    # Create a base CentOS Docker image.
    #
    # This script is useful on systems with yum installed (e.g., building
    # a CentOS image on CentOS).  See contrib/mkimage-rinse.sh for a way
    # to build CentOS images on other systems.

    usage() {
        cat <eoopts basename="" name="">
    OPTIONS:
      -y <yumconf>  The path to the yum config to install packages from. The
                    default is /etc/yum.conf.
    EOOPTS
        exit 1
    }

    # option defaults
    yum_config=/etc/yum.conf
    while getopts ":y:h" opt; do
        case $opt in
            y)
                yum_config=$OPTARG
                ;;
            h)
                usage
                ;;
            \?)
                echo "Invalid option: -$OPTARG"
                usage
                ;;
        esac
    done
    shift $((OPTIND - 1))
    name=$1

    if [[ -z $name ]]; then
        usage
    fi

    #--------------------

    target=$(mktemp -d --tmpdir $(basename $0).XXXXXX)

    set -x

    mkdir -m 755 "$target"/dev
    mknod -m 600 "$target"/dev/console c 5 1
    mknod -m 600 "$target"/dev/initctl p
    mknod -m 666 "$target"/dev/full c 1 7
    mknod -m 666 "$target"/dev/null c 1 3
    mknod -m 666 "$target"/dev/ptmx c 5 2
    mknod -m 666 "$target"/dev/random c 1 8
    mknod -m 666 "$target"/dev/tty c 5 0
    mknod -m 666 "$target"/dev/tty0 c 4 0
    mknod -m 666 "$target"/dev/urandom c 1 9
    mknod -m 666 "$target"/dev/zero c 1 5

    yum -c "$yum_config" --installroot="$target" --setopt=tsflags=nodocs \
        --setopt=group_package_types=mandatory -y groupinstall Core
    yum -c "$yum_config" --installroot="$target" -y clean all

    cat > "$target"/etc/sysconfig/network <<EOF
    NETWORKING=yes
    HOSTNAME=localhost.localdomain
    EOF
    
    # effectively: febootstrap-minimize --keep-zoneinfo --keep-rpmdb
    # --keep-services "$target".  Stolen from mkimage-rinse.sh
    #  locales
    rm -rf "$target"/usr/{{lib,share}/locale,{lib,lib64}/gconv,bin/localedef,sbin/build-locale-archive}
    #  docs
    rm -rf "$target"/usr/share/{man,doc,info,gnome/help}
    #  cracklib
    rm -rf "$target"/usr/share/cracklib
    #  i18n
    rm -rf "$target"/usr/share/i18n
    #  sln
    rm -rf "$target"/sbin/sln
    #  ldconfig
    rm -rf "$target"/etc/ld.so.cache
    rm -rf "$target"/var/cache/ldconfig/*

    version=
    if [ -r "$target"/etc/redhat-release ]; then
        version="$(sed 's/^[^0-9\]*\([0-9.]\+\).*$/\1/' "$target"/etc/redhat-release)"
    fi

    if [ -z "$version" ]; then
        echo >&2 "warning: cannot autodetect OS version, using '$name' as tag"
        version=$name
    fi

    tar --numeric-owner -c -C "$target" . | docker import - $name:$version
    docker run -i -t $name:$version echo success

    rm -rf "$target"
We'll now execute this script which will create a docker image and commit it to our local docker instance, but first we need to start our docker instance.

sudo service docker start
sudo ./mkimage-yum.sh rhel

The script will now copy all of the data it needs to a tmp directory, create the image, commit the image using the name provided, in this case rhel, and the version from /etc/redhat-release, and then clean up the tmp directory. Once this is complete, we'll see our docker image in our local docker instance. The last few lines of output show the image id and "name":


Then we can see it in our local list of images, the "name" is made of the REPOSITORY and TAG:


We can also see this in our containers list:


We'll now push it to a private registry just to show the basics of that interaction. We'll use our server at ipfacecobld26 on port 5000, but first we need to commit it with our registry as part of the REPOSITORY:


We used the CONTAINER ID from the previous step to commit the image with the REPOSITORY set as ipfacecobld26:5000/rhel and the tag as 6.5. This returns the IMAGE ID of the new image. We can now push this image to our server:


Notice that it pushed two images. The second image is the one we just tagged, and the first is the original image. This shows how docker "stacks" images on top of each other. Every image in the stack has to be pushed to the registry for the top image to work. We now have our own RHEL 6.5 image that others can pull down and use to run long-running VM's, short-lived test-kitchen tests, or to build new images with additional functionality.