--- - name: Set up the environment for the impress app hosts: webserver become: yes become_user: root vars: email_address: "emi@matchu.dev" # TODO: Extract this to personal config? impress_hostname: impress.openneo.net ruby_version: "3.3.5" vars_files: # mysql_root_password, mysql_user_password, mysql_user_password_2020, # dev_ips - files/setup_secrets.yml tasks: - name: Set hostname to impress.openneo.net hostname: name: impress.openneo.net - name: Create SSH folder for logged-in user become: no file: name: .ssh mode: "700" state: directory - name: Copy authorized SSH keys to logged-in user become: no copy: dest: ~/.ssh/authorized_keys src: files/authorized-ssh-keys.txt mode: "600" - name: Disable root SSH login lineinfile: dest: /etc/ssh/sshd_config regexp: ^#?PermitRootLogin line: PermitRootLogin no - name: Disable password-based SSH authentication lineinfile: dest: /etc/ssh/sshd_config regexp: ^#?PasswordAuthentication line: PasswordAuthentication no - name: Enable public-key SSH authentication lineinfile: dest: /etc/ssh/sshd_config regexp: ^#?PubkeyAuthentication line: PubkeyAuthentication yes - name: Update the apt cache apt: update_cache: yes - name: Install fail2ban firewall with default settings apt: name: fail2ban - name: Configure ufw firewall to allow SSH connections on port 22 community.general.ufw: rule: allow port: "22" - name: Configure ufw firewall to allow HTTP connections on port 80 community.general.ufw: rule: allow port: "80" - name: Configure ufw firewall to allow HTTPS connections on port 443 community.general.ufw: rule: allow port: "443" - name: Configure ufw firewall to allow MySQL connections from impress-2020 community.general.ufw: rule: allow port: "3306" from_ip: "{{ item }}" loop: - "45.56.112.222" - "2600:3c02::f03c:92ff:fe9a:4615" - name: Configure ufw firewall to allow MySQL connections from known devs community.general.ufw: rule: allow port: "3306" from_ip: "{{ item }}" loop: "{{ dev_ips }}" - name: Enable ufw firewall with all other ports closed by default community.general.ufw: state: enabled policy: deny - name: Install unattended-upgrades apt: name: unattended-upgrades - name: Enable unattended-upgrades to auto-upgrade our system 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 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 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 community.general.timezone: name: America/Los_Angeles - name: Create "impress" user user: name: impress comment: Impress App home: /srv/impress create_home: false shell: /bin/bash - name: Create "impress-deployers" group group: name: impress-deployers - name: Add the current user to the "impress-deployers" group user: name: "{{ lookup('env', 'USER') }}" groups: - impress-deployers append: yes # We use this so the deploy playbook doesn't have to prompt for a root # password: this user just is trusted to act as "impress" in the future. - name: Enable the "impress-deployers" group to freely act as the "impress" user community.general.sudoers: name: impress-deployers-as-impress group: impress-deployers runas: impress commands: ALL nopassword: yes # Similarly, this enables us to manage the impress service in the deploy playbook # and in live debugging without a password. # NOTE: In the sudoers file, you need to specify the full path to the # command, to avoid tricks where you use PATH to get around the intent! - name: Enable the "impress-deployers" group to freely start and stop the impress service community.general.sudoers: name: impress-deployers-systemctl group: impress-deployers commands: - /bin/systemctl status impress - /bin/systemctl start impress - /bin/systemctl stop impress - /bin/systemctl restart impress nopassword: yes - name: Install ACL, to enable us to run commands as the "impress" user apt: name: acl - name: Install ruby-build git: repo: https://github.com/rbenv/ruby-build.git dest: /opt/ruby-build version: v20240917 - name: Check if Ruby {{ ruby_version }} is already installed stat: path: /opt/ruby-{{ ruby_version }} register: ruby_dir - name: Install Ruby {{ ruby_version }} command: "/opt/ruby-build/bin/ruby-build {{ ruby_version }} /opt/ruby-{{ ruby_version }}" when: not ruby_dir.stat.exists - name: Add Ruby {{ ruby_version }} to the global PATH, for developer convenience copy: dest: /etc/profile.d/ruby_path.sh content: PATH="/opt/ruby-{{ ruby_version }}/bin:$PATH" - name: Install system dependencies for impress's Ruby gems apt: name: - libmysqlclient-dev - libyaml-dev - name: Create the app folder file: path: /srv/impress owner: impress group: impress mode: "755" state: directory - name: Add a convenient .bash_profile for when we log in as "impress" copy: owner: impress group: impress dest: /srv/impress/.bash_profile content: | set -a # Export all of the below RAILS_ENV=production EXECJS_RUNTIME=Disabled source /srv/impress/shared/production.env set +a - name: Create the app's "versions" folder become_user: impress file: path: /srv/impress/versions state: directory - name: Create the app's "shared" folder become_user: impress file: path: /srv/impress/shared state: directory - name: Check for a current app version stat: path: /srv/impress/current register: current_app_version - name: Check whether we already have a placeholder app stat: path: /srv/impress/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 become_user: impress copy: src: files/initial-placeholder dest: /srv/impress/versions when: | not current_app_version.stat.exists and not existing_placeholder_app.stat.exists - name: Configure the placeholder app to run in deployment mode become_user: impress command: chdir: /srv/impress/versions/initial-placeholder cmd: /opt/ruby-{{ ruby_version }}/bin/bundle config set --local deployment true when: not current_app_version.stat.exists - name: Install the placeholder app's dependencies become_user: impress command: chdir: /srv/impress/versions/initial-placeholder cmd: /opt/ruby-{{ ruby_version }}/bin/bundle install when: not current_app_version.stat.exists - name: Set the placeholder app as the current version become_user: impress file: src: /srv/impress/versions/initial-placeholder dest: /srv/impress/current state: link when: not current_app_version.stat.exists # NOTE: This file is uploaded with stricter permissions, to help protect # the secrets inside. Most of the app is world-readable for convenience # for debugging and letting nginx serve static files, but keep this safer! - name: Upload the production.env file become_user: impress copy: dest: /srv/impress/shared/production.env src: files/production.env mode: "600" notify: - Reload systemctl - Restart impress - name: Create the shared public-data folder become_user: impress file: dest: /srv/impress/shared/public-data mode: "755" state: directory - name: Create service file for impress template: src: files/impress.service.j2 dest: /etc/systemd/system/impress.service notify: - Reload systemctl - Restart impress - name: Configure impress to run now, and automatically when the system starts systemd: name: impress state: started enabled: true - name: Install nginx apt: name: nginx - name: Install core snap community.general.snap: name: core - name: Install certbot as a snap community.general.snap: name: certbot classic: yes - name: Set up certbot command: "certbot certonly --nginx -n --agree-tos --email {{ email_address }} --domains {{ impress_hostname }}" - name: Add impress config file to nginx template: src: files/sites-available/impress.conf dest: /etc/nginx/sites-available/impress.conf notify: - Reload nginx - name: Enable impress config file in nginx file: src: /etc/nginx/sites-available/impress.conf dest: /etc/nginx/sites-enabled/impress.conf state: link notify: - Reload nginx - name: Add openneo-home config file to nginx template: src: files/sites-available/openneo-home.conf dest: /etc/nginx/sites-available/openneo-home.conf notify: - Reload nginx - name: Enable openneo-home config file in nginx file: src: /etc/nginx/sites-available/openneo-home.conf dest: /etc/nginx/sites-enabled/openneo-home.conf state: link notify: - Reload nginx - name: Install MariaDB apt: name: mariadb-server - name: Install a Python MySQL client, for Ansible to use when configuring apt: name: python3-mysqldb - name: Update MariaDB root password community.mysql.mysql_user: name: root host_all: true password: "{{mysql_root_password}}" - name: Create root's .my.cnf file copy: content: | [client] user=root password='{{ mysql_root_password }}' dest: /root/.my.cnf mode: 0400 - name: Remove test database community.mysql.mysql_db: name: test state: absent login_unix_socket: "{{ login_unix_socket | default(omit) }}" - name: Remove anonymous users community.mysql.mysql_user: name: "" state: absent host_all: true - name: Remove remote root access community.mysql.mysql_query: query: - DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1') - name: Expose MariaDB to the internet (but ufw will block most clients) copy: dest: /etc/mysql/mariadb.conf.d/80-bind-address.cnf content: | [mysqld] skip-networking=0 skip-bind-address notify: Restart MariaDB # This is the best Unicode collation available in our version of MariaDB! # We already specify it for all the tables in `schema.rb`, but also set it # as the default collation for new tables here, too. - name: Set MariaDb's default collation to utf8mb4_unicode_520_ci copy: dest: /etc/mysql/mariadb.conf.d/80-charsets.cnf content: | [mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_520_ci notify: Restart MariaDB - name: Enable slow query logging for MariaDB copy: dest: /etc/mysql/mariadb.conf.d/80-logging.cnf content: | [mysqld] slow-query-log notify: Restart MariaDB - name: Create MySQL databases community.mysql.mysql_db: name: - openneo_impress - openneo_id - name: Create MySQL user openneo_impress community.mysql.mysql_user: name: openneo_impress password: "{{ mysql_user_password }}" priv: "openneo_impress.*:ALL,openneo_id.*:ALL" - name: Create MySQL user impress2020 community.mysql.mysql_user: name: impress2020 password: "{{ mysql_user_password_2020 }}" priv: "openneo_impress.*:ALL,openneo_id.*:ALL" - name: Create the Neopets Media Archive data directory file: path: /var/lib/neopets-media-archive owner: impress group: impress mode: "755" state: directory - name: Remove 10min cron job to run `rails nc_mall:sync` become_user: impress cron: state: absent name: "Impress: sync NC Mall data" minute: "*/10" job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails nc_mall:sync'" - name: Create 10min cron job to run `rails neopets:import:nc_mall` become_user: impress cron: name: "Impress: import NC Mall data" minute: "*/10" job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails neopets:import:nc_mall'" - name: Create weekly cron job to run `rails public_data:commit` become_user: impress cron: name: "Impress: commit public data" weekday: "0" # Sunday hour: "1" # 1:15am minute: "15" # 1:15am job: "bash -c 'source /etc/profile && source ~/.bash_profile && cd /srv/impress/current && bin/rails public_data:commit[scheduled]'" handlers: - name: Reload nginx systemd: name: nginx state: reloaded - name: Restart MariaDB systemd: name: mariadb state: restarted - name: Reload systemctl command: systemctl daemon-reload - name: Restart impress systemd: name: impress state: restarted