Use GNU-Make as a Task Runner

Need a simple task runner without the hassle of installing dependencies?

Posted by on February 2, 2021

GNU-Make has been around for a long time. Some 40 years or so. Primarily used as a tool to compile and package software.

Why not use it as a development tasks and deployment tool? It's pre-installed and ready to run! I didn't come up with this idea, nor is it new, but I haven't seen it used in web development and thought I'd give it a try. Turns out, it works pretty good. Much better than shell scripts; however, not as complete as a dedicated task-runner like gulp.

Getting Started

My goal was to manually install as little as possible. I did end up installing these manually:

  1. Virtualbox (and the extension pack)
  2. Vagrant
  3. vagrant-hostupdater plugin. A very helpful plugin maintained by a talented
  4. Python 3.9 (although this could be considered optional) developer Chris Smith.

I suppose I could even automate that as well.

Also, I didn't want to install languages and tools globally. So, I automated the creation of a python virtual environment to install ansible, which is a python based automation tool. To do this, I created a requirements.txt file that contained all the python dependencies I need. For now, it's just ansible, so my file is rather slim.

python_requirements.txt:

ansible

With everything in place, I was ready to create the beginnings of my Makefile to create the virtual python environment (venv) and activate it. Then install my python dependencies.

Makefile:

VENV_ACTIVATE = source venv/bin/activate
venv/bin/activate:
    @echo "Creating Python virtual environment..."
    test -f venv/bin/activate || python3.9 -m venv venv
    touch $@

venv/requirements_stamp: venv/bin/python python_requirements.txt
    venv/bin/python -m pip install --upgrade pip -r python_requirements.txt
    vagrant plugin install vagrant-hostsupdater
    touch venv/requirements_stamp

venv: venv/bin/activate check-python-version venv/requirements_stamp

Next, I needed to install any ansible dependencies:

ansible-install-dependencies:
    $(VENV_ACTIVATE); ansible-galaxy install -r requirements.yml --ignore-certs
    $(VENV_ACTIVATE); ansible-galaxy collection install -r requirements.yml --ignore-certs

init: venv whoami ansible-install-dependencies

Now we can run this via terminal with make init

Control Vagrant and Ansible

So far I'm using it to automate set-up of Vagrant boxes and deployment of ansible. I'm passing variables via the environment to Vagrant which in turn passes some of them on-to Ansible.

Makefile:

vagrant-up: init
    $(VENV_ACTIVATE); \
    export PLAYBOOK=${ANSIBLE_PLAYBOOK}; \
    export BASEBOX=${BASEBOX}; \
    export FULL_HOSTNAME=${FULL_HOSTNAME}; \
    export SITE_ALIASES=${SITE_ALIASES}; \
    export PROJECT=${PROJECT}; \
    export OWNER=${OWNER}; \
    export SECRETS_FILE=${SECRETS_FILE}; \
    export IP_ADDRESS=${IP_ADDRESS}; \
    export SYNC_FOLDER_HOST=${SYNC_FOLDER_HOST}; \
    export SYNC_FOLDER_TARGET=${SYNC_FOLDER_TARGET}; \
    export VM_NAME=${VM_NAME}; \
    vagrant up ${VAGRANT_PROJECT}

This target allows my to create variable targets, which can set these variables and execute vagrant up.

Makefile:

dev-markshardware-%: PROJECT= markshardware
dev-markshardware-%: DOMAIN_NAME= markshardware.com

dev-markshardware-%: ANSIBLE_PLAYBOOK= "playbooks/com.markshardware.yml"

dev-markshardware-vagrant-%: VAGRANT_PROJECT= "localmake"
dev-markshardware-vagrant-%: BASEBOX= "bento/ubuntu-20.04"
dev-markshardware-vagrant-%: SITE_ALIASES= "markshardware.com.local"
dev-markshardware-vagrant-%: FULL_HOSTNAME= "markshardware"
dev-markshardware-vagrant-%: OWNER= "markedphp"
dev-markshardware-vagrant-%: SECRETS_FILE= "password_file"
dev-markshardware-vagrant-%: IP_ADDRESS= "192.168.33.18"
dev-markshardware-vagrant-%: SYNC_FOLDER_HOST= "code/markshardware.com.dev"
dev-markshardware-vagrant-%: SYNC_FOLDER_TARGET= "/home/markedphp/var/www/markshardware.com.local"
dev-markshardware-vagrant-%: VM_NAME= "markshardware"

dev-markshardware-vagrant-up: vagrant-up

These environment variables need to be used by the Vagrantfile.

Vagrantfile:

# -*- mode: ruby -*-
# vi: set ft=ruby :
BASE_BOX= ENV["BASEBOX"] ? "bento/ubuntu-20.04" : ENV["BASEBOX"]
MAKE_PLAYBOOKS= ENV.has_key?("PLAYBOOK") ? ENV["PLAYBOOK"].split(',') : []
SITE_ALIASES= (ENV.has_key?("SITE_ALIASES")) ? ENV["SITE_ALIASES"].split(',') : []
FULL_HOSTNAME= ENV["FULL_HOSTNAME"]
OWNER = ENV.has_key?("OWNER") ? ENV["OWNER"] : "vagrant"
SECRETS_FILE = ENV.has_key?("SECRETS_FILE") ? ENV["SECRETS_FILE"] : nil
IP_ADDRESS = ENV["IP_ADDRESS"]
SYNC_FOLDER_HOST= ENV["SYNC_FOLDER_HOST"]
SYNC_FOLDER_TARGET= ENV["SYNC_FOLDER_TARGET"]
VM_NAME = defined?(ENV["VM_NAME"]) ? ENV["VM_NAME"] : FULL_HOSTNAME
PROJECT = ENV["PROJECT"]

Now, I have all the variables I need to run vagrant with an ansible provisioner.

Vagrant.configure("2") do |config|
    config.vm.define "localmake" do |machine|
        config.hostsupdater.aliases = SITE_ALIASES
        config.vm.box = BASE_BOX
        config.vm.hostname = PROJECT
        config.vm.network "private_network", ip: IP_ADDRESS
        config.vm.synced_folder SYNC_FOLDER_HOST, SYNC_FOLDER_TARGET, create: true, type: "nfs", linux__nfs_options: ['no_subtree_check','no_root_squash','async']
        machine.vm.provider :virtualbox do |provider|
            provider.name = VM_NAME
        end
        MAKE_PLAYBOOKS.each do |make_playbook|
            machine.vm.provision "ansible" do |provisioner|
                provisioner.extra_vars = {
                    vagrant_env: "dev",
                    full_hostname: FULL_HOSTNAME,
                    server: { owner: OWNER }
                }
                unless SECRETS_FILE.nil?
                    provisioner.vault_password_file = SECRETS_FILE
                end
                provisioner.playbook = make_playbook
            end
        end
    end
end

Create Servers

Since this is utilizing the awesome power of ansible, I can create servers in AWS, Digital Ocean, and anywhere else there is an ansible plugin just by creating a playbook. E.g. make prod-markshardware-create-server. Of course, I need to create the corresponding Makefile target and update the local environment variables accordingly.

Deploy to Production

Not only this, but my Makefile also handles deployment to production, ssh keys 'n' all! I can spin up a server and run this all using make. e.g. make prod-markshardware-deploy. Of course, I need to create the corresponding Makefile target and update the local environment variables accordingly.


← Return