In this post we will be setting up a WordPress installation on Linode using 1Password to store our secrets, Terraform to manage the server resources, and Docker (WordPress + Nginx) for running the site.
Table of Contents
1Password
1Password is a password and secrets manager that offers powerful CLI tooling in addition to a pleasant UI. Follow the setup instructions for the CLI here and then setup the 1Password SSH-agent as described here.
Terraform
In order to use Terraform with 1Password we need to wrap terraform
with a script.1 Put the following in tfproxy.sh
#!/bin/sh
op run --env-file="./linode.env" -- terraform "$@"
Allow the file to be executed with chmod +x tfproxy.sh
. Then populate linode.env
with
AWS_ACCESS_KEY_ID = "op://My Vault/linode_terraform_token/username"
AWS_SECRET_ACCESS_KEY = "op://My Vault/linode_terraform_token/password"
TF_VAR_linode_cli_token = "op://My Vault/Linode Personal Access Token/token"
Bootstrapping with Linode
In the previous step we specified a few passwords that are going to be used by Terraform. Before we can populate them we need to setup an object storage bucket to store the Terraform state which you can do here. For this post we assume the location is us-east-1
and we recommend a name such as tf-state-myusername
.
Now we need to populate 1Password “My Vault” vault with the Linode keys.
linode_terraform_token
username and password should be set to point to a bucket token that can be generated here.Linode Personal Access Token
token points to a token generated from here.
Then create a Terraform file called main.tf
Terraform {
required_providers {
linode = {
source = "linode/linode"
version = "2.13.0"
}
ansible = {
source = "ansible/ansible"
version = "~> 1.1.0"
}
}
backend "s3" {
skip_s3_checksum = true
skip_credentials_validation = true
endpoint = "https://us-east-1.linodeobjects.com"
skip_requesting_account_id = true
bucket = "<my bucket>"
key = "infra/state.json"
region = "us-east-1"
}
}
provider "linode" {
token = var.linode_cli_token
}
Notice that we need to include the settings for skip_s3_checksum
, skip_credentials_validations
and skip_requesting_account_id
for Terraform to work with Linode as a backend.
Now run ./tfproxy.sh init
.
Creating the Machine
Add the following to main.tf
resource "linode_instance" "wordpress" {
label = "wordpress"
image = "linode/ubuntu22.04"
region = var.region
type = "g6-nanode-1"
authorized_keys = [var.ssh_key]
backups_enabled = true
}
output "wordpress_ip" {
value = resource.linode_instance.wordpress.ipv4
}
and create a file terraform.tfvars
with your public SSH key which you can find using ssh-add -l
or (for 1Password on macOS) SSH_AUTH_SOCK=~/Library/Group\ Containers/2BUA8C4S2C.com.1password/t/agent.sock ssh-add -l
ssh_key = "ssh-ed25519 AAAAA foo@example.com"
To create the server first run ./tfproxy.sh plan
and if everything looks good, ./tfproxy.sh apply
.
Ansible
Now we want to provision the server created by Terraform with Ansible. We’ve already added the ansible
provider to main.tf
so now we just need to provision an ansible_host
resource by adding the following to main.tf
resource "ansible_host" "wordpress" {
name = tolist(resource.linode_instance.wordpress.ipv4)[0]
groups = ["wordpress"]
}
Rerun ./tfproxy.sh plan
and ./tfproxy.sh apply
.
Now add the following to inventory.yml
plugin: cloud.terraform.terraform_provider
binary_path: "./tfproxy.sh"
And view the inventory using ansible-inventory -i inventory.yml --list
{
"_meta": {
"hostvars": {}
},
"all": {
"children": [
"ungrouped",
"wordpress"
]
},
"wordpress": {
"hosts": [
"XX.XX.XX.XX"
]
}
}
Docker
Create an Ansible playbook called docker.yml
and put the following in it:
- name: Install docker
tags: setup
hosts: wordpress
remote_user: root
tasks:
# https://medium.com/@GarisSpace/how-to-install-docker-using-ansible-01a674086f8c
- name: Update and upgrade all packages to the latest version
ansible.builtin.apt:
update_cache: true
upgrade: dist
cache_valid_time: 3600
- name: Install required packages
ansible.builtin.apt:
pkg:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- software-properties-common
- name: Add Docker GPG apt Key
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
state: present
- name: Print architecture variables
ansible.builtin.debug:
msg: "Architecture: {{ ansible_architecture }}, Codename: {{ ansible_lsb.codename }}"
- name: Add Docker repository
ansible.builtin.apt_repository:
repo: >-
deb https://download.docker.com/linux/ubuntu {{ ansible_lsb.codename }} stable
filename: docker
state: present
- name: Install Docker and related packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
update_cache: true
loop:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
- name: Add Docker group
ansible.builtin.group:
name: docker
state: present
- name: Add user to Docker group
ansible.builtin.user:
name: "{{ ansible_user }}"
groups: docker
append: true
- name: Enable and start Docker services
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- docker.service
- containerd.service
- name: Install pip
apt:
name: python3-pip
- name: install python docker
pip:
name: docker
- name: install python docker-compose
pip:
name: docker-compose
Then create a wordpress.yml
playbook with the following:
---
- import_playbook: docker.yml
- name: Setup wordpress
tags: compose
remote_user: root
hosts: wordpress
tasks:
- name: create wp-content folder
ansible.builtin.file:
path: /srv/wp-content
state: directory
- name: update wp-content folder
ansible.posix.synchronize:
src: wp-content/
dest: /srv/wp-content
- name: copy nginx config
ansible.builtin.copy:
src: nginx/
dest: /srv/nginx/
- name: set wp-content permissions
ansible.builtin.file:
path: /srv/wp-content
owner: 1002
group: 1002
recurse: yes
- name: create wordpress compose
ansible.builtin.template:
src: wordpress_compose.yml.j2
dest: /srv/wordpress_compose.yml
- name: start the server
community.docker.docker_compose:
project_src: /srv
files:
- wordpress_compose.yml
- name: reload nginx
ansible.builtin.shell: docker compose -f wordpress_compose.yml exec nginx nginx -s reload
args:
chdir: /srv
Now make a directory called wp-content
.You can put any themes or plugins you want in there later. Also create a path for Nginx configurations: mkdir -p nginx/conf
. Put the following in nginx/conf/app.conf
:
server {
listen 80;
listen [::]:80;
server_name YOURDOMAIN.com www.YOURDOMAIN.com;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://YOURDOMAIN.com$request_uri;
}
}
Now’s a good time to point an A
DNS record for your domain to the server and replace YOURDOMAIN
with, well, your domain.
You also need a Docker compose template file called wordpress_compose.yml.j2
version: "3.9"
services:
nginx:
image: nginx:stable-alpine
volumes:
- ./nginx/conf:/etc/nginx/conf.d/:ro
- ./etc/letsencrypt:/etc/letsencrypt:ro
- ./certbot/data:/var/www/certbot
restart: always
ports:
- 80:80
- 443:443
depends_on:
- wordpress
certbot:
image: certbot/certbot:latest
depends_on:
- nginx
command: >-
certonly --reinstall --webroot --webroot-path=/var/www/certbot
--email you@example.com --agree-tos --no-eff-email
-d YOURDOMAIN.com
volumes:
- ./etc/letsencrypt:/etc/letsencrypt
- ./certbot/data:/var/www/certbot
db:
image: mysql:8.0.23
ports:
- 3306:3306
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: {{ lookup('community.general.onepassword', 'wp_mysql_root', field='password', vault='My Vault') }}
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: {{ lookup('community.general.onepassword', 'wp_mysql', field='password', vault='My Vault') }}
wordpress:
depends_on:
- db
image: wordpress:latest
user: 1002:1002
ports:
- 8080:80
restart: always
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: {{ lookup('community.general.onepassword', 'wp_mysql', field='password', vault='My Vault') }}
volumes:
- type: bind
source: /srv/wp-content/
target: /var/www/html/wp-content
volumes:
db_data: {}
Then generate usernames and passwords for wp_mysql
and wp_mysql_root
in 1Password.
What’s going on here? This playbook runs the Docker setup playbook, copies over wp-content, sets the wp-content
user to the users inside the Docker container (very important to allow for WordPress to modify the folder), fills in a template for docker compose
, starts the compose, and then refreshes Nginx.
Run the playbook to completion.
ansible-playbook -i inventory.yml wordpress.yml
SSL
Once the services have started you should have an SSL key now. To be sure, check the log output:
ssh root@my-ip
cd /srv
docker compose -f wordpress_compose.yml logs
If the SSL certificate was successfully obtained you can now tell Nginx to use it by adding the following to the bottom of nginx/conf/app.conf
:
server {
listen 443 default_server ssl http2;
listen [::]:443 ssl http2;
server_name YOURDOMAIN.com;
ssl_certificate /etc/letsencrypt/live/YOURDOMAIN.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOURDOMAIN.com/privkey.pem;
proxy_redirect off;
location / {
# https://blog.ldev.app/running-wordpress-behind-ssl-and-nginx-reverse-proxy/
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_pass http://wordpress:80/;
}
}
Notice that we cannot add this until the certificate is generated because otherwise Nginx will not start. Update the configuration and have Nginx reload using the following command (which skips installing Docker):
ansible-playbook -i inventory.yml wordpress.yml --skip-tags setup
And that’s it!
Start the discussion at forum.stereowrench.com