impress-2020/deploy/playbooks/setup.yml
Emi Matchu 448561604c Future-proof our nginx config for IPv6
Today I learned that nginx requires a special invocation to listen to
IPv6 addresses as well as IPv4. On some of my other projects, this was
causing Let's Encrypt certificate renewal to fail, because Let's
Encrypt prefers to connect over IPv6 when an AAAA record is present, so
its challenges were always returning 404, because nginx wasn't
listening on IPv6.

This shouldn't be affecting impress-2020 in production, because we
don't have an AAAA record right now. But I'm just making this change in
all my projects, to make sure this doesn't bite me in the future!
2024-02-13 08:52:52 -08:00

350 lines
11 KiB
YAML

---
- name: Set up the environment for the impress-2020 app
hosts: webserver
vars:
email_address: "emi@matchu.dev" # TODO: Extract this to personal config?
tasks:
- name: Copy authorized SSH keys
copy:
dest: ~/.ssh/authorized_keys
src: ../authorized-ssh-keys.txt
- name: Disable root SSH login
become: yes
lineinfile:
dest: /etc/ssh/sshd_config
regexp: ^#?PermitRootLogin
line: PermitRootLogin no
- name: Disable password-based SSH authentication
become: yes
lineinfile:
dest: /etc/ssh/sshd_config
regexp: ^#?PasswordAuthentication
line: PasswordAuthentication no
- name: Install fail2ban firewall with default settings
become: yes
apt:
update_cache: yes
name: fail2ban
- name: Configure ufw firewall to allow SSH connections on port 22
become: yes
community.general.ufw:
rule: allow
port: "22"
- name: Configure ufw firewall to allow HTTP connections on port 80
become: yes
community.general.ufw:
rule: allow
port: "80"
- name: Configure ufw firewall to allow HTTP connections on port 443
become: yes
community.general.ufw:
rule: allow
port: "443"
- name: Enable ufw firewall with all other ports closed by default
become: yes
community.general.ufw:
state: enabled
policy: deny
- name: Install unattended-upgrades
become: yes
apt:
update_cache: yes
name: unattended-upgrades
- name: Enable unattended-upgrades to auto-upgrade our system
become: yes
copy:
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
dest: /etc/apt/apt.conf.d/20auto-upgrades
- name: Configure unattended-upgrades to auto-reboot our server when necessary
become: yes
lineinfile:
regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot ".*";$
line: Unattended-Upgrade::Automatic-Reboot "true";
dest: /etc/apt/apt.conf.d/50unattended-upgrades
- name: Configure unattended-upgrades to delay necessary reboots to 3am
become: yes
lineinfile:
regex: ^(//\s*)?Unattended-Upgrade::Automatic-Reboot-Time ".*";$
line: Unattended-Upgrade::Automatic-Reboot-Time "03:00";
dest: /etc/apt/apt.conf.d/50unattended-upgrades
- name: Configure the system timezone to be US Pacific time
become: yes
community.general.timezone:
name: America/Los_Angeles
- name: Create the app versions folder
become: yes
file:
path: /srv/impress-2020/versions
owner: "{{ ansible_user_id }}"
group: "{{ ansible_user_id }}"
state: directory
- name: Add Nodesource apt key
become: yes
apt_key:
id: 9FD3B784BC1C6FC31A8A0A1C1655A0AB68576280
url: https://deb.nodesource.com/gpgkey/nodesource.gpg.key
- name: Add Node v16 apt repository
become: yes
apt_repository:
repo: deb https://deb.nodesource.com/node_16.x focal main
- name: Install Node v16
become: yes
apt:
update_cache: yes
name: nodejs
- name: Install Yarn
become: yes
npm:
name: yarn
global: yes
- name: Check for a current app version
stat:
path: /srv/impress-2020/current
register: current_app_version
- name: Check whether we already have a placeholder app
stat:
path: /srv/impress-2020/versions/initial-placeholder
register: existing_placeholder_app
when: not current_app_version.stat.exists
- name: Create a placeholder app, to run until we deploy a real version
command:
chdir: /srv/impress-2020/versions
cmd: yarn create next-app initial-placeholder
when: |
not current_app_version.stat.exists and
not existing_placeholder_app.stat.exists
- name: Build the placeholder app
command:
chdir: /srv/impress-2020/versions/initial-placeholder
cmd: yarn build
when: not current_app_version.stat.exists
- name: Set the placeholder app as the current version
file:
src: /srv/impress-2020/versions/initial-placeholder
dest: /srv/impress-2020/current
state: link
when: not current_app_version.stat.exists
- name: Install pm2
become: yes
npm:
name: pm2
global: yes
- name: Create pm2 startup script
# The current user is going to become the pm2 owner of the app server
# process. They'll be able to manage it without `sudo`, including during
# normal deploys, and run `pm2 monit` from their shell to see status.
become: yes
command: "pm2 startup systemd -u {{ ansible_user_id }} --hp /home/{{ ansible_user_id }}"
- name: Create pm2 ecosystem file
copy:
content: |
module.exports = {
apps: [
{
name: "impress-2020",
cwd: "/srv/impress-2020/current",
// Instead of `yarn start`, we specify the `next` binary
// directly, because it helps pm2 monitor our app correctly.
// https://github.com/vercel/next.js/discussions/10675#discussioncomment-34615
script: "./node_modules/.bin/next",
args: "start --port=3000",
instances: "max",
exec_mode: "cluster",
}
]
}
dest: "~/ecosystem.config.js"
# Create a temporary backup file, so we can use it to delete the old
# version of the services. (This is important if e.g. a service is
# removed or renamed, in which case deleting from the *new* config file
# wouldn't include it.)
backup: yes
register: pm2_ecosystem_file
- name: Delete old pm2 services if config file changed
command: "pm2 delete {{ pm2_ecosystem_file.backup_file | quote }}"
when: pm2_ecosystem_file is changed and pm2_ecosystem_file.backup_file is defined
- name: Delete old pm2 config file if it changed
file:
path: "{{ pm2_ecosystem_file.backup_file }}"
state: absent
when: pm2_ecosystem_file is changed and pm2_ecosystem_file.backup_file is defined
- name: Start pm2 services
command: "pm2 start ~/ecosystem.config.js"
- name: Save pm2 startup script
command: pm2 save
- name: Install nginx
become: yes
apt:
update_cache: yes
name: nginx
- name: Install core snap
become: yes
community.general.snap:
name: core
- name: Install certbot as a snap
become: yes
community.general.snap:
name: certbot
classic: yes
- name: Set up certbot
become: yes
command: "certbot certonly --nginx -n --agree-tos --email {{ email_address }} --domains impress-2020-box.openneo.net"
- name: Add impress-2020 config file to nginx
become: yes
copy:
content: |
server {
server_name impress-2020-box.openneo.net;
listen 80;
listen [::]:80;
if ($host = impress-2020-box.openneo.net) {
return 301 https://$host$request_uri;
}
}
server {
server_name impress-2020-box.openneo.net;
listen 443 ssl;
listen [::]:443 ssl;
ssl_certificate /etc/letsencrypt/live/impress-2020-box.openneo.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/impress-2020-box.openneo.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
ssl_session_cache shared:SSL:10m; # https://superuser.com/q/1484466/14127
# TODO: Serve static files directly, instead of through the proxy
location / {
proxy_pass http://127.0.0.1:3000;
}
}
dest: /etc/nginx/sites-enabled/impress-2020
notify:
- Restart nginx
- name: Create cron log files directory
file:
path: /srv/impress-2020/cron-logs
state: directory
- name: Set up modeling cron jobs
cron:
name: model-needed-items
minute: "*/10" # Every 10 minutes
job: "cd /srv/impress-2020/current && yarn model-needed-items 2>&1 > /srv/impress-2020/cron-logs/model-needed-items.log"
# Idk why the app has been so memory leaky lately, but let's just restart it
# like *constantly* tbh.
- name: Set up cron job to restart the app
cron:
name: restart-impress-2020
minute: "*/8" # Every 8 minutes
job: "pm2 restart impress-2020"
- name: Install dependencies for the npm module node-canvas
become: yes
apt:
update_cache: yes
name:
- build-essential
- libcairo2-dev
- libpango1.0-dev
- libjpeg-dev
- libgif-dev
- librsvg2-dev
- name: Install Playwright system dependencies
# NOTE: I copied the package list from the source list for
# `npx playwright install-deps`, which I couldn't get running in
# Ansible as root, and besides, I prefer manually managing the
# package list over running an npm script as root!
# TODO: We're using Puppeteer now, should this list change in some way?
become: yes
apt:
update_cache: yes
name:
# Tools
- xvfb
- fonts-noto-color-emoji
- ttf-unifont
- libfontconfig
- libfreetype6
- xfonts-cyrillic
- xfonts-scalable
- fonts-liberation
- fonts-ipafont-gothic
- fonts-wqy-zenhei
- fonts-tlwg-loma-otf
- ttf-ubuntu-font-family
# Chromium
- fonts-liberation
- libasound2
- libatk-bridge2.0-0
- libatk1.0-0
- libatspi2.0-0
- libcairo2
- libcups2
- libdbus-1-3
- libdrm2
- libegl1
- libgbm1
- libglib2.0-0
- libgtk-3-0
- libnspr4
- libnss3
- libpango-1.0-0
- libx11-6
- libx11-xcb1
- libxcb1
- libxcomposite1
- libxdamage1
- libxext6
- libxfixes3
- libxrandr2
- libxshmfence1
- name: Enable user namespace cloning for Chromium sandboxing
become: yes
ansible.posix.sysctl:
name: kernel.unprivileged_userns_clone
value: "1"
handlers:
- name: Restart nginx
become: yes
systemd:
name: nginx
state: restarted