--- - 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 {{ 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; if ($host = impress-2020-box.openneo.net) { return 301 https://$host$request_uri; } } server { server_name impress-2020-box.openneo.net; 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: 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! 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 handlers: - name: Restart nginx become: yes systemd: name: nginx state: restarted