Create setup.yml deploy script

Yay it's working! We set up the box, install Ruby, upload a placeholder app, set it up as a service, and get it hooked up to nginx!

Next, we'll add the script to upload the latest version of the site. We just need to slot it into `/srv/impress/current`, run `bundle install`, and that should basically be that! (Oh, and we need to compile production assets—I wonder if it's useful to do that on the dev machine instead of on the target? That might save us from needing to install Node. Or maybe we'll have to anyway!)
This commit is contained in:
Emi Matchu 2023-08-16 17:17:25 -07:00
parent 6b8fc6407e
commit 3dd5d26332
10 changed files with 366 additions and 86 deletions

View file

@ -1,85 +0,0 @@
require "bundler/capistrano"
require "rvm/capistrano"
set :application, "newimpress.openneo.net"
set :repository, "git://github.com/matchu/openneo-impress-rails.git"
set :deploy_to, "/home/rails/impress"
set :user, "rails"
set :branch, "master"
default_run_options[:pty] = true
set :scm, :git
# Or: `accurev`, `bzr`, `cvs`, `darcs`, `git`, `mercurial`, `perforce`, `subversion` or `none`
role :web, application
role :app, application, :memcached => true
role :db, application, :primary => true
set :bundle_without, [:development, :test]
set :rvm_ruby_string, 'ruby-1.9.3-p484' # Or whatever env you want it to run in.
set :rvm_type, :system
set :rvm_install_type, :head
set :rvm_bin_path, "/usr/local/rvm/bin"
namespace :deploy do
task :start, :roles => :app do
run "touch #{current_release}/tmp/restart.txt"
sudo "monit -g impress_workers start"
end
task :stop do
sudo "monit -g impress_workers stop"
end
task :restart do
run "touch #{current_release}/tmp/restart.txt"
sudo "monit -g impress_workers restart"
end
desc "Link shared files"
task :link do
links = {
"#{shared_path}/app/views/static/_announcement.html" => "#{release_path}/app/views/static/_announcement.html",
#"#{shared_path}/config/aws_s3.yml" => "#{release_path}/config/aws_s3.yml",
"#{shared_path}/config/database.yml" => "#{release_path}/config/database.yml",
#"#{shared_path}/config/openneo_auth.yml" => "#{release_path}/config/openneo_auth.yml",
#"#{shared_path}/config/initializers/secret_token.rb" => "#{release_path}/config/initializers/secret_token.rb",
#"#{shared_path}/config/initializers/stripe.rb" => "#{release_path}/config/initializers/stripe.rb"
"#{shared_path}/public/beta.html" => "#{release_path}/public/beta.html",
"#{shared_path}/public/javascripts/analytics.js" => "#{release_path}/app/assets/javascripts/analytics.js",
"#{shared_path}/public/swfs/outfit" => "#{release_path}/public/swfs/outfit",
"#{shared_path}/.rvmrc" => "#{release_path}/.rvmrc"
}
links.each do |specific_shared_path, specific_release_path|
run "rm -rf #{specific_release_path} && ln -nfs #{specific_shared_path} #{specific_release_path}"
end
end
end
namespace :memcached do
desc "Start memcached"
task :start, :roles => [:app], :only => {:memcached => true} do
sudo "/etc/init.d/memcached start"
end
desc "Stop memcached"
task :stop, :roles => [:app], :only => {:memcached => true} do
sudo "/etc/init.d/memcached stop"
end
desc "Restart memcached"
task :restart, :roles => [:app], :only => {:memcached => true} do
sudo "/etc/init.d/memcached restart"
end
desc "Flush memcached - this assumes memcached is on port 11211"
task :flush, :roles => [:app], :only => {:memcached => true} do
run "echo 'flush_all' | nc localhost 11211"
end
desc "Symlink the memcached.yml file into place if it exists"
task :symlink_configs, :roles => [:app], :only => {:memcached => true }, :except => { :no_release => true } do
run "if [ -f #{shared_path}/config/memcached.yml ]; then ln -nfs #{shared_path}/config/memcached.yml #{latest_release}/config/memcached.yml; fi"
end
end
before "deploy:symlink", "memcached:flush"
after "deploy:update_code", "deploy:link"

22
deploy/README Normal file
View file

@ -0,0 +1,22 @@
Dress to Impress is deployed to a VPS server. We use this Ansible Playbook to
automate the environment setup!
We expect to be deploying to Ubuntu 20.04 LTS, initially with nothing
installed. The user you deploy with should have sudoers access. That should be
all it takes!
First, run `yarn deploy:setup` in the app root, to run the `setup.yml`
playbook. This will prompt you for your root password, to set up system
dependencies. It should be safe to re-run this, including if you add a new
dependency to the playbook, because the steps are non-destructive and Ansible
will skip steps that are already satisfied.
Then, to deploy a new version of the app, run `yarn deploy`. This will build
the app from the code on your machine, then send the source and build output
to the remote machine, and switch it to be the new production version. Nice!
Note that the setup script references a file named `production.env`, which is
gitignored because it contains sensitive information, like database passwords.
You should create a `production.env` file in the local `deploy/files`
directory, to be copied to the remote server and used as its environment
variables.

1
deploy/files/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/production.env

View file

@ -0,0 +1,16 @@
# These are the SSH public keys that allow a user to log in and setup or deploy.
#
# It's dangerous to add a new key to this file! When you run
# `yarn deploy-setup`, it will copy these keys to the deploy server, which will
# allow the owner of these keys to log into the deploy server in the future.
#
# But the keys themselves aren't necessarily sensitive data, except for the name
# at the end, which might tell a reader about who is allowed to log in and what
# devices they own.
#
# When a computer tries to log in, it perform a cryptographic challenge that
# proves it owns this key - but that requires the *private* key, whereas this is
# the *public* key. That's why it's secure to publish these!
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID2/yLvpetD14BVK+Zd88ZofOxIfLRdl4FI2pdV+fmy3 Matchu's Desktop (Leviathan) WSL
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKFwWryq6slOQqkrJ7HIig7BvEQVQeH19hFwb+9VpXgz Matchu's Laptop (Ebon Hawk)

View file

@ -0,0 +1,2 @@
source 'http://rubygems.org'
gem 'puma', '~> 6.3'

View file

@ -0,0 +1,15 @@
GEM
remote: http://rubygems.org/
specs:
nio4r (2.5.9)
puma (6.3.0)
nio4r (~> 2.0)
PLATFORMS
x86_64-linux
DEPENDENCIES
puma (~> 6.3)
BUNDLED WITH
2.4.18

View file

@ -0,0 +1,3 @@
run lambda { |env|
[200, {}, ["Hello world! The server is up - now it's time to deploy the app for real!"]]
}

3
deploy/inventory.cfg Normal file
View file

@ -0,0 +1,3 @@
# There is currently only one impress box in our Ansible inventory!
[webserver]
beta.impress.openneo.net

302
deploy/setup.yml Normal file
View file

@ -0,0 +1,302 @@
---
- 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: beta.impress.openneo.net
tasks:
- 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: 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
create_home: false
- 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: 4d4678bc1ed89aa6900c0ea0da23495445dbcf50
- name: Check if Ruby 3.1.4 is already installed
stat:
path: /opt/ruby-3.1.4
register: ruby_dir
- name: Install Ruby 3.1.4
command: "/opt/ruby-build/bin/ruby-build 3.1.4 /opt/ruby-3.1.4"
when: not ruby_dir.stat.exists
- name: Add Ruby 3.1.4 to the global PATH, for developer convenience
lineinfile:
dest: /etc/profile
line: 'PATH="/opt/ruby-3.1.4/bin:$PATH" # Added by impress deploy setup script'
- name: Create the app folder
file:
path: /srv/impress
owner: impress
group: impress
mode: "755"
state: directory
- name: Create the app versions folder
become_user: impress
file:
path: /srv/impress/versions
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-3.1.4/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-3.1.4/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/production.env
src: files/production.env
mode: "600"
- name: Create service file for impress
copy:
dest: /etc/systemd/system/impress.service
content: |
[Unit]
Description=Dress to Impress webapp
[Service]
User=impress
Restart=always
WorkingDirectory=/srv/impress/current
ExecStart=/opt/ruby-3.1.4/bin/bundle exec puma --port=3000
EnvironmentFile=/srv/impress/production.env
; Some security directives, adapted from Akkoma's service file, they seem like sensible defaults!
; Use private /tmp and /var/tmp folders inside a new file system namespace, which are discarded after the process stops.
PrivateTmp=true
; The /home, /root, and /run/user folders can not be accessed by this service anymore. If your Akkoma user has its home folder in one of the restricted places, or use one of these folders as its working directory, you have to set this to false.
ProtectHome=true
; Mount /usr, /boot, and /etc as read-only for processes invoked by this service.
ProtectSystem=full
; Sets up a new /dev mount for the process and only adds API pseudo devices like /dev/null, /dev/zero or /dev/random but not physical devices.
PrivateDevices=true
; Ensures that the service process and all its children can never gain new privileges through execve().
NoNewPrivileges=true
; Drops the sysadmin capability from the daemon.
CapabilityBoundingSet=~CAP_SYS_ADMIN
[Install]
WantedBy=multi-user.target
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 beta.impress.openneo.net"
- name: Add impress config file to nginx
copy:
content: |
server {
server_name {{ impress_hostname }};
listen 80;
if ($host = {{ impress_hostname }}) {
return 301 https://$host$request_uri;
}
}
server {
server_name {{ impress_hostname }};
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/{{ impress_hostname }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ impress_hostname }}/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
root /srv/impress/current/public;
# Try serving static files first. If not found, fall back to the app.
try_files $uri/index.html $uri @app;
location @app {
proxy_pass http://127.0.0.1:3000;
}
}
dest: /etc/nginx/sites-available/impress.conf
notify:
- Restart 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:
- Restart nginx
handlers:
- name: Restart nginx
systemd:
name: nginx
state: restarted
- name: Reload systemctl
command: systemctl daemon-reload
- name: Restart impress
systemd:
name: impress
state: restarted

View file

@ -28,6 +28,7 @@
}, },
"scripts": { "scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text", "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets --loader:.js=jsx --loader:.png=file --loader:.svg=file --loader:.min.js=text",
"build:production": "yarn build --minify" "build:production": "yarn build --minify",
"deploy:setup": "echo $'Setup requires you to become the root user. You\\'ll need to enter the password for your account on the remote web server below, and you must be part of the `sudo` user group.' && ansible-playbook -K -i deploy/inventory.cfg deploy/setup.yml"
} }
} }