This is a fairly basic example of what is possible with Ansible but it’s something I’ve been putting off for some time. I’m never going to accomplish large scale deployments if I can’t handle small ones first.

This reason for this post is because I was appalled at how I saw my users updating their sites and realized that my current process was no better. Nobody should have to deploy their site with the ‘drag & drop’ method. Here’s an example of this workflow.

  • Download the current version of the site to a local directory.
  • Make some changes to the site.
  • Upload the new version of the site to the remote directory.
  • Check to see if everything works as expected.
  • Go back to step 2 if something is broken.
  • Repeat every time you want to make a change.

My Ideal Workflow

Here’s the workflow I eventually want out of this series.

  • Clone the current version of my site from my repository.
  • Make some changes to the site.
  • Test the changes in my development environment.
  • When everything works then commit my changes.
  • Push my changes to my repository.

And then behind the scenes a totally automated process takes over to actually build the site from my source and then place the result on my web server.

  • My repository notifies my CI server that new changes have been pushed.
  • My CI server pulls the latest changes and builds the site.
  • If the site builds successfully then deploy the result to the production server.

What We’ll Accomplish

I want to have something actually working so we’re going to condense some of the steps and make the deployment process more manual by forking the process at step 5.

  • Run an Ansible playbook to notify my remote server to clone the current repository, build the site, and deploy it to production.

Setting Up

Typically I have a deploy user on all of my servers for this purpose. I designate /var/deploy as the working directory for all of my deployment jobs so in this case we’re going to use /var/deploy/estheruary.build and /var/deploy/estheruary.staging for this process. We need to make sure both of these directories are present.

  vars:
    build: /var/deploy/estheruary.build
    staging: /var/deploy/estheruary.staging

  tasks:
    - name: create build and staging directories
      file: >
        dest={{ item }}
        owner=deploy
        group=deploy
        mode=0750
        state=directory
      become: yes
      with_items:
        - "{{ build }}"
        - "{{ staging }}"

Next we need to make sure that all the packages we need to run jekyll are on the server. This isn’t the best solution because we shouldn’t have our development packages in the production environment. Later we will avoid this problem by using Docker on our CI server to create a consistent build environment.

We’re going to use Bundler to download all of our Ruby gems but we need all of the following packages for bundle install to complete successfully. I did not anticipate that a static site generator needed so many native extensions.

  tasks:
    - name: install development packages
      package: name={{ item }} state=installed
      with_items:
        - rubygem-bundler
        - ruby-devel
        - "@Development Tools"
        - "@C Development Tools and Libraries"
        - redhat-rpm-config
        - zlib-devel
      become: yes

Straight from the Source

Now that we have a suitable build environment we need to acquire the source from our remote repository. Luckily Ansible has a git module for precisely this purpose.

  vars:
    source: gogs@git.inspiredby.es:joey.chagnon/estheruary.git

  tasks:
    - name: clone the build tree
      git: >
        repo={{ source }}
        dest={{ build }}
        accept_hostkey=yes
        clone=yes
        update=yes
      register: estheruary_deploy_updated

The reason we’re registering the result of this module will become apparent soon. Technically speaking we could handle this with a handler (i.e. notify) but I don’t like to rely on handlers for control flow for the same reason you shouldn’t use exceptions for control flow.

If You Build It

Now that we have the source of our site into the build directory we’re ready to generate the site into our staging area; bundler and Jekyll make this process very easy.

  tasks:
    - block:
      - name: install jekyll
        shell: >
          chdir={{ build }}
          bundle install --frozen

      - name: build the website
        shell: >
          chdir={{ build }}
          bundle exec jekyll build --destination {{ staging }}
        environment:
          PATH: "{{ ansible_env.PATH }}:{{ ansible_env.HOME }}/bin"

The --frozen option ensures that Gemfile.lock isn’t automatically updated.

Note: This might be fixed with a configuration file later but when running via Ansible bundler doesn’t pick up bundler’s path addition so we have to add it manually.

Shipping it Out

This one is trivial since we’re building the site on our production server. It’s just a simple matter of syncing the staging directory with the production location and making sure that the resulting files have the correct SELinux contexts.

  vars:
    target: /var/www/html

  tasks:
      - name: deploy the website
        command: rsync -uax --delete {{ staging }}/ {{ target }}/
        become: yes

      - name: change ownership of the files
        file: >
          dest={{ target }}
          owner=apache
          group=apache
          recurse=yes
          state=directory
        become: yes

      - name: fix selinux permissions
        command: restorecon -R {{ target }}
        become: yes
      when: estheruary_deploy_updated | changed

Actually Deploy a New Site

Once you have your deployment host set up in your inventory the only thing remaining is to run the playbook.

ansible-playbook plays/estheruary-deploy.yml -K

Wrapping Up

Success! If you’re reading this then our simple deployment pipeline has worked at least once. Stay tuned for the next installment where we separate out the build and deployment steps into their own environment.