HashiCorp Terraform enables you to safely and predictably create, change, and improve infrastructure. It is an open source tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
In computing, Ansible is an open-source software provisioning, configuration management, and application-deployment tool. It runs on many Unix-like systems, and can configure both Unix-like systems as well as Microsoft Windows. It includes its own declarative language to describe system configuration.
I’m not going to get into the advantages of having both your project infrastructure and configuration in code here, but Terraform and Ansible are great tools for doing both of these.
Installing Ansible and Terraform
Start by installing Ansible and Terraform. On MacOSX you can do that using Homebrew, or simply by downloading the binaries from the official websites.
brew install terraform
brew install ansible
After you have Terraform and Ansible accessible, install terraform-inventory. This is a Go application that generates a dynamic inventory file from your Terraform state. This way, you will be able to use the Terraform newly created server information as your Ansible host.
You might want to install this manually because homebrew does not always have the latest version available.
brew install terraform-inventory
DigitalOcean Api keys and SSH key
The next step would be to create a DigitalOcean Read Write Token for Terraform to use when connecting to the DigitalOcean api. You can do this in your Account > Api > Personal access tokens. Keep the token safe and well away any versioned code.
In addition to this, in order to store the Terraform state in a secure manner we will be using DigitalOcean Spaces. You will need to create a Space and a Spaces Access Key that Terraform will be using to store its State.
More information on this can be found in this excellent post by Joseph D. Marhee: https://medium.com/@jmarhee/digitalocean-spaces-as-a-terraform-backend-b761ae426086
In addition to the access key and token you will need a ssh key that will be used to connect to the droplets.
ssh-keygen -t rsa -b 4096
Upload the public key to DigitalOcean and retrieve the key fingerprint, it will be required when you run terraform plan or apply.
Terraform Files
Create a provider.tf file containing the DigitalOcean backend configuration and set it up with the DataCenter your droplets and spaces will be in.
variable "digitalocean_token" {}
variable "digitalocean_ssh_fingerprint" {}
provider "digitalocean" {
token = "${var.digitalocean_token}"
version = "~> 1.0"
}
terraform {
backend "s3" {
endpoint = "ams3.digitaloceanspaces.com"
region = "us-west-1"
key = "terraform.tfstate"
skip_requesting_account_id = true
skip_credentials_validation = true
skip_get_ec2_platforms = true
skip_metadata_api_check = true
}
}
Now create a jenkins.tf file containing the following configuration.
resource "digitalocean_tag" "jenkins" {
name = "example:jenkins"
}
# Create a new Droplet using the SSH key
resource "digitalocean_droplet" "example_jenkins" {
name = "example-jenkins"
image = "ubuntu-18-04-x64"
region = "ams3"
size = "s-1vcpu-1gb"
ipv6 = true
private_networking = true
ssh_keys = ["${var.digitalocean_ssh_fingerprint}"]
tags = ["${digitalocean_tag.jenkins.name}"]
}
This will create a 1CPU 1GB RAM Ubuntu 18.04 64b Droplet with the name example-jenkins. The Droplet will be created in the Amsterdam DigitalOcean DataCenter, but you can change this to whatever one you need. The ssh key attached to the droplet will be the one you pass in when asking Terraform to create the droplets.
Assuming you want to point a domain or subdomain to the new droplet, you can use Terraform for that as well. The following configuration will add example.org as a domain in your DigitalOcean DNS management and then add a jenkins.example.org A record pointing to that IP.
resource "digitalocean_domain" "exampleorg" {
name = "example.org"
}
resource "digitalocean_record" "exampleorg" {
name = "jenkins"
type = "A"
domain = "${digitalocean_domain.exampleorg.name}"
value = "${digitalocean_droplet.example_jenkins.ipv4_address}"
}
The last bit of infrastructure configuration we will want to add is to set the droplet firewall to only allow specific ports open. (22 for SSH, 80 and 443 for web access and 8080 which is the default port Jenkins runs on)
resource "digitalocean_firewall" "jenkins" {
name = "firewall-jenkins"
droplet_ids = ["${digitalocean_droplet.example_jenkins.id}"]
inbound_rule = [
{
protocol = "tcp"
port_range = "22"
},
{
protocol = "tcp"
port_range = "80"
},
{
protocol = "tcp"
port_range = "443"
},
{
protocol = "tcp"
port_range = "8080"
}
]
}
Init Terraform, Plan and Apply
Initialize terraform with the correct access key
terraform init \
-backend-config="access_key=AAABBBCCCDDDEEE" \
-backend-config="secret_key=AAABBBCCCDDDEEE" \
-backend-config="bucket=AAABBBCCCDDDEEE"
# Run terraform plan to check what the script will actually do.
terraform plan
# Run terraform apply to create the droplet
terraform apply
After this hopefully you should have an Ubuntu droplet set up to accept SSH connections and the domain jenkins.example.org pointing to it. Let’s move on to provisioning it.
Provisioning the droplet with Ansible
In order to provision the droplet we will need to run an Ansible playbook.
Create a an Ansible requirements.yml file
---
- src: geerlingguy.git
- src: geerlingguy.java
- src: geerlingguy.jenkins
- src: hispanico.nginx-revproxy
- src: hispanico.letsencrypt-nginx-revproxy
- src: geerlingguy.mysql
- src: geerlingguy.docker
- src: geerlingguy.ansible
- src: geerlingguy.pip
Create a jenkins.yml playbook either in this directory.
---
- hosts: example_jenkins
become: true
any_errors_fatal: true
gather_facts: no
pre_tasks:
- name: 'install python2'
raw: sudo apt-get -y install python
- name: Set the java_packages variable (Ubuntu).
set_fact:
java_packages:
- openjdk-8-jdk
- name: update cache
apt:
update_cache: true
- name: Install apache2-utils
apt:
name: apache2-utils
state: present
- hosts: example_jenkins
become: true
any_errors_fatal: true
gather_facts: yes
vars:
jenkins_hostname: jenkins.example.org
roles:
- role: geerlingguy.java
- role: geerlingguy.jenkins
become: yes
vars:
jenkins_admin_username: admin
jenkins_admin_password: admin
jenkins_package_state: present
- role: hispanico.nginx-revproxy
- role: hispanico.letsencrypt-nginx-revproxy
vars:
nginx_revproxy_sites:
jenkins.example.org:
domains:
- jenkins.example.org
upstreams:
- { backend_address: 127.0.0.1, backend_port: 8080 }
letsencrypt: true
letsencrypt_email: 'test@example.org'
ssl: true
- role: geerlingguy.pip
vars:
ansible_install_method: pip
ansible_install_version_pip: "2.7.0"
- role: geerlingguy.ansible
- role: geerlingguy.docker
HTTPS
The Jenkins service exposes the UI on port 8080 by default. In order to access Jenkins via HTTPS the Ansible playbook contains an nginx proxy running on the same server. These excellent roles add an nginx proxy service on the same droplet and generate a SSL Certificate using Let’s Encrypt.
hispanico/ansible-nginx-revproxy
Run the Ansible provisioning script:
After terraform has finished deploying the droplet, run the following commands.
ansible-galaxy install -r requirements.yml
ansible-playbook --inventory=`which terraform-inventory` --private-key=id_rsa --user=root jenkins.yml
In order to provision the droplet, you should be able to add a provisioner declaration in your jenkins.tf file like the one below. I personally have not been able to get this to work using terraform-inventory so I’ve kept the scripts completely separate.
provisioner "local-exec" {
command = <<EOT
ls -al
ansible-galaxy install -r requirements.yml
ansible-playbook --inventory=`which terraform-inventory` --private-key=id_rsa --user=root jenkins.yml
EOT
}
When the Ansible script finishes, you should have Jenkins installed in the new Droplet.
Accessing http://jenkins.example.org:8080 should give you access to the Jenkins UI. Accessing https://jenkins.example.org should give you the Jenkins UI via https.