Category Archives: Packer

Using Packer & Ansible to build a LEMP Server with Packer on Cloud Native Infrastructure Powered by OpenStack

In the days where everyone is now using Docker and building containers for every new project, it’s easy to forget that for some projects, it’s total overkill. Sometimes, you may just want to build a server the old way, and create a basic image that you can deploy on your server, and not worry about learning about containers!

Whatever your stance on the above, building a LEMP (Linux, (E)Nginx, MySQL/MariaDB, PHP) server using automated tools such as Packer & Ansible is a great way to get your head around these tools, as well as give you a great reproducible image that you can store in your image repository.

Packer, from HashiCorp is another great tool that allows you to select a builder and provisioner, and push your built image to the destination of your choice. What a great summary in one sentence eh?

To follow this guide, you may wish to view the code here

Firstly, make sure you have packer installed:

brew install packer

The next step is to create the template.json file which tells packer what to use as the builder, and what to use as the provisioner.

 "builders": [
    {
      "type": "openstack",
      "image_name": "centos_lamp_php7",
      "source_image": "0f1785b3-33c3-451e-92ce-13a35d991d60",
      "flavor": "c46be6d1-979d-4489-8ffe-e421a3c83fdd",
      "ssh_keypair_name": "ukcloudos",
      "ssh_private_key_file": "/Users/bobby/.ssh/ukcloudos",
      "use_floating_ip": true,
      "floating_ip_pool": "internet",
      "ssh_username": "centos",
      "ssh_pty" : true
    }
  ], 

This is the element of Packer which tells it to use the Openstack builder. The image_name is the name that you would like the end image to be called whereas the source_image is the image id of the base image.

In terms of provisioning we have the following block:

"provisioners": [
    {
      "type": "shell",
      "inline": [
        "sudo rpm -iUvh http://dl.fedoraproject.org/pub/epel/7/x86_64/e/epel-release-7-8.noarch.rpm",
        "sudo yum -y update",
        "sudo yum -y install ansible",
        "ansible --version"
      ]
    },{
      "type": "ansible-local",
      "playbook_file": "./ansible/playbook.yml",
      "role_paths": [
          "./ansible/roles/init",
          "./ansible/roles/server",
          "./ansible/roles/mongodb",
          "./ansible/roles/php7",
          "./ansible/roles/nginx",
          "./ansible/roles/supervisord",
          "./ansible/roles/redis"
      ],
      "group_vars": "./ansible/common/group_vars"
    },{
      "type": "shell",
      "inline": [
        "cd /srv && sudo chown -R nginx:nginx .",
        "sudo curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/bin --filename=composer"
      ]
    }
  ]

The first provisioner is just a shell command to install ansible. We need this on the server so that we can then apply the ansible provisioner.

Let’s take a look at the Playbooks we have (ansible/playbook.yml).

---
- hosts: all
  sudo: true
  vars_files:
    - "group_vars/settings.yml"
  roles:
    - init
    - server
    - php7
    - mongodb
    - nginx
    - supervisord
    - redis

In the settings.yml file I’ve placed a list of PHP & PECL packages that we’d like on our LEMP server:

---
php:
    packages: ["php", "php-fpm", "php-common", "php-mbstring", "php-mcrypt", "php-devel", "php-xml","php-mysqlnd", "php-pdo", "php-opcache", "php-bcmath", "php-pear"]
    pecl_packages: ["php-pecl-memcached", "php-pecl-redis", "php-pecl-zip", "php-pecl-xdebug"]

The next step of the Ansible provisioner refers to each role-path, and this will be done sequentially. Each ‘role’ has a sub directory called tasks, which then has a main.yml to execute the particular instructions.

As you can see, the first is init, with the following instructions:

---
- name: Install Remi Repo
  yum: name=http://rpms.famillecollet.com/enterprise/remi-release-7.rpm

- name: Enable Remi Repo
  shell: yum-config-manager --enable remi-php70

Remi is my favourite repository when using CentOS7, it has all the latest packages and can always be trusted to work out of the box, unlike some others.

We then have some the server role which installs some base packages such as wget and vim:

---
- name: Install System Packages
  sudo: yes
  yum: pkg={{ item }} state=latest
  with_items:
    - git
    - wget
    - vim
    - sudo
    - openssl-devel

- name: Configure the timezone
  sudo: yes
  template: src=timezone.tpl dest=/etc/timezone

- name: Allow root to not require password to perform commands
  sudo: yes
  template: src=mysudoers.tpl dest=/etc/sudoers.d/mysudoers

- name: install the 'Development tools' package group
  yum: name="@Development tools" state=present

You can see all the other roles by taking a look at the source code, but the interesting one is the one that installs the PHP stuff:

- name: Install PHP Packages
  sudo: yes
  yum: pkg={{ item }} state=latest
  with_items: '{{php.packages}}'

- name: Install PHP-Pecl Packages
  sudo: yes
  yum: pkg={{ item }} state=latest
  with_items: '{{php.pecl_packages}}'

  #Add templates
- name: Change to custom php.ini
  sudo: yes
  template: src=php-dev.ini.tpl dest=/etc/php-dev.ini

#Add templates
- name: Change to custom php.ini
  sudo: yes
  template: src=php-prod.ini.tpl dest=/etc/php-prod.ini

  #Add templates
- name: Change to custom opcache config
  sudo: yes
  template: src=10-opcache.ini.tpl dest=/etc/php.d/10-opcache.ini

  #Add templates
- name: Change to custom php-fpm config
  sudo: yes
  template: src=www.conf.tpl dest=/etc/php-fpm.d/www.conf

You’ll notice with the PHP role that we have some templates. This is where you can define some settings, which you may or may not want to later configure using other tools such as puppet or consul. It also has a template to configure php-fpm to work seamlessly with the nginx template:

server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;

    root  /srv/web;
    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~*  \.(jpg|jpeg|png|gif|ico|css|js|woff)$ {
       expires 365d;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_read_timeout 180;
        include fastcgi_params;
    }
}

As you can see, it has everything out of the box that is needed for a working LEMP server.

To run packer, all we need to do is head to the root of the project and type:

$ packer build ./packer/template.json

Packer will run through the motions of grabbing the CentOS base image from OpenStack, install Ansible, and then run our playbooks, and save the provisioned image in Glance with our chosen image name.

You can then head over to your GUI and view the newly created image and use it as you please! Alternatively, you can see the images with this OpenStack CLI command:

openstack image list

The true beauty of this is that you have an automated way to provision your servers. You can place this code in Jenkins and make nightly builds or you can run it manually whenever you need.

If you have any questions about using Packer or Ansible with Cloud Native Infrastructure just send me a tweet: @bobbyjason

Share