Skip to content

Instantly share code, notes, and snippets.

@eritbh
Last active February 13, 2022 17:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save eritbh/e57d8ac91a5709c6c08435ec17e2a273 to your computer and use it in GitHub Desktop.
Save eritbh/e57d8ac91a5709c6c08435ec17e2a273 to your computer and use it in GitHub Desktop.
Setting up a new deploy user and automating deployment tasks through Github Actions

Continuous deployment with Github Actions and systemd services

This is a collection of templates/scripts I use to set up CD systems for my projects.

For each project I deploy, I create a user and a systemd service. The user account's home directory is where the project lives, and the systemd service defines how it's run and ensures that it stays running after failure or reboot. The user is given sudo permission only to interact with its own service, via a /etc/sudoers.d supplement. The user account isn't accessible via password auth, but it does have an SSH key that can be used to log into the account and automate updates.

deploysetup.sh automates this process. It creates a new user, configures its sudo and SSH permissions, generates an SSH key for it, and creates a template systemd service. After running the script, all I have to do is clone the project into the deploy user's home directory and configure the systemd service to run the project.

The actual CD is handled by a Github Actions workflow. This workflow is responsible for SSHing into the server as the deploy user, performing update tasks (checking out new code from git, updating local dependency installations, building artifacts, running database migrations, etc.), and restarting the service.

deploy.yml is a template for this workflow which is configured to trigger on every push to the main branch. After copying it to the .github/workflows folder of a repository, I script the necessary update tasks and copy information about the deploy server (hostname, user, private key) into repo secrets. Then, the workflow is triggered, it will connect to the server as the deploy user and carry out the script I've given it.

# A sample workflow that lets you automatically checkout changes from the main branch
# and restart the service running on your server. Assumes that you have the repo cloned
# to a folder in the deploy user's home directory. Drop this file in your repo's
# `.github/workflows` folder and fill in your own logic for udpating the service before
# it runs, then push to the `main` branch to trigger the workflow.
#
# Note that this workflow doesn't define how the service is actually run - actually
# running your application is done through the systemd service file. This script is
# only responsible for performing update tasks (checking out new changes, installing
# updated dependencies, migrating databases) and then telling systemd to restart the
# service.
#
# Expects some secrets:
# - SSH_DEPLOY_USER
# The username of the deploy account
# - SSH_DEPLOY_KEY
# The private key used to SSH into the deploy user's account
# - SSH_DEPLOY_IP
# The IP address or hostname of the server we want to SSH into
name: Build and deploy
# Update every time we push to the main branch
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# Lets us SSH into the server using our key
- name: Start SSH agent
uses: webfactory/ssh-agent@v0.4.0
with:
ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}
- name: Pull latest changes to server
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_IP }} /bin/bash << 'EOF'
# Causes this script to exit early and report a failure if any command errors
set -e
# Checkout latest changes from our repo, overwriting any local changes
cd "$HOME/my-project"
git fetch --all
git reset --hard origin/master
# Do other update work here - update dependencies, run migrations, whatever you need to do
# Finally, restart the service to make the new stuff go live
sudo systemctl restart discord-mod-bot.service
EOF
# Sets up a systemd service skeleton for your application and creates a deploy user for
# automating deployments with an SSH keypair. The user will have its password disabled, so
# it will only be accessible via the generated key.
#
# Run this file with:
#
# sudo bash deploysetup.sh service-name
#
# Or with more options:
#
# sudo SERVICENAME=my-service SERVICEUSER=my-service-user SERVICEDESC="My service" bash deploysetup.sh
#!/bin/bash
# simple input: call with one argument, the name of the new service (user will be same as service name)
# advanced input: call after setting SERVICENAME, SERVICEUSER, and SERVICEDESC
: ${SERVICENAME:=$1} ${SERVICEUSER:=$SERVICENAME} ${SERVICEDESC:="The $SERVICENAME service"}
if [ -z $SERVICENAME ]; then
echo "Need to provide a service name"
exit 1
fi
# Create deploy user with disabled password
adduser "$SERVICEUSER" --gecos "" --disabled-password --quiet
echo "Created deploy user $SERVICEUSER, access this account with:"
echo " sudo su - $SERVICEUSER"
# Generate keypair for deploy user and add as authorized key
mkdir -p "/home/$SERVICEUSER/.ssh"
ssh-keygen -q -N "" -t rsa -b 4096 -C "Deploy key for $SERVICENAME" -f "/home/$SERVICEUSER/.ssh/deploy_rsa"
cat "/home/$SERVICEUSER/.ssh/deploy_rsa.pub" >> "/home/$SERVICEUSER/.ssh/authorized_keys"
echo "Generated keypair for $SERVICEUSER at /home/$SERVICEUSER/.ssh/deploy_rsa"
# Fix .ssh directory permissions for new user
chown -R "$SERVICEUSER" "/home/$SERVICEUSER/.ssh"
chgrp -R "$SERVICEUSER" "/home/$SERVICEUSER/.ssh"
chmod 700 "/home/$SERVICEUSER/.ssh"
chmod 600 "/home/$SERVICEUSER/.ssh/deploy_rsa"
chmod 600 "/home/$SERVICEUSER/.ssh/deploy_rsa.pub"
chmod 600 "/home/$SERVICEUSER/.ssh/authorized_keys"
# Disable SSH password auth for this user
cat <<-SSHDCONFIG >> "/etc/ssh/sshd_config"
Match User $SERVICEUSER
PasswordAuthentication no
SSHDCONFIG
echo "Disabled password authentication for $SERVICEUSER"
# Create skeleton systemd service
cat <<-SERVICEFILE > "/etc/systemd/system/$SERVICENAME.service"
[Unit]
Description=$SERVICEDESC
After=network.target
# Set where the service lives
ConditionPathExists=/home/$SERVICEUSER
[Service]
Type=simple
LimitNOFILE=1024
Restart=on-failure
RestartSec=10
User=$SERVICEUSER
# Set how the service runs
Environment=MYVAR=something
ExecStart=echo Put your stuff here
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=$SERVICENAME
[Install]
WantedBy=multi-user.target
SERVICEFILE
systemctl daemon-reload
echo "Created skeleton systemd service at /etc/systemd/system/$SERVICENAME.service"
# Create sudoers supplement for service user
cat <<-SUDOERS > "/etc/sudoers.d/$SERVICENAME"
# Sudo rules for automatic deployment of $SERVICENAME by user $SERVICEUSER.
$SERVICEUSER ALL=NOPASSWD: /bin/systemctl stop $SERVICENAME.service
$SERVICEUSER ALL=NOPASSWD: /bin/systemctl start $SERVICENAME.service
$SERVICEUSER ALL=NOPASSWD: /bin/systemctl restart $SERVICENAME.service
%$SERVICEUSER ALL=PASSWD: /bin/su - surveysite
SUDOERS
echo "Created sudoers supplement at /etc/sudoers.d/$SERVICENAME"
echo
echo "Next steps:"
echo "- Log in as the deploy user and set up your service"
echo "- Edit the systemd service file to run the service correctly"
echo "- Enable systemd service to start on boot"
echo "- Copy generated keypair for use in deploy scripts"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment