Categories
Programming Swift

Deploying Swift to Ubuntu

In this article, I’m going describe how to create a Linux environment where we can deploy a Swift-backed server-side app. In a past article, I created a Notes app with both a client and server component and shared business logic, all written in Swift. Now, we need to create a remote (or virtual) environment where we can safely and automatically deploy the server-side app, have it compile and run.

For this project, I’ve chosen Ubuntu 17.04 as our Linux distribution. This version is not an LTS version, but it’s recent, easy to work with and includes a numbre of online tutorials. I have also broken down the process into four logical steps for easy reference:

  1. Provision the Linux machine
  2. Create the user accounts
  3. Customizing the environment
  4. Deploying the app

The first three steps are fairly manual with one exception: in Vagrant, provisioning is free. The NotesApp project includes a Vagrant file and a script for easy reference.

The application, the Vagrant configuration and all scripts can be found the NotesApp repo.

Without further ado, let’s begin the process.

1. Set up the machine

Time to configure the machine with all the packages we’ll need to build and run our Swift app. Fortunately, most of the tools we need for Swift on the server side are already present on an Ubuntu box. All we are missing are a few packages for OpenSSH, Swift’s compiler and HTTP server packages.

Login as the admin user and run the following commands. If you are the root user, you probably don’t need sudo.

# Install additional packages

sudo apt update
sudo apt-get install -y git libssl-dev libcurl4-openssl-dev nginx supervisor clang libicu-dev

# Download and install swift

curl -sSL https://swift.org/builds/swift-4.0-release/ubuntu1610/swift-4.0-RELEASE/swift-4.0-RELEASE-ubuntu16.10.tar.gz > swift.tar.gz

tar xzf swift.tar.gz
sudo mv swift-4.0-RELEASE-ubuntu16.10/usr /usr/lib/swift
sudo chown -R root:root /usr/lib/swift
sudo chmod a+x /usr/lib/swift/bin/*
sudo chmod -R a+r /usr/lib/swift/lib
sudo ln -s ../lib/swift/bin/swift /usr/bin/swift

rm -fr swift*
swift --version

As I mentioned earlier, if you are running this step on a Vagrant virtual machine, these calls can be easily added to a script and bootstraped as the provision script in the configuration. I have already done so in Scripts/provision.sh file.

2. Creating the user accounts

We’ll be creating three accounts on the server:

  • admin: The account responsible for administration (also a sudo user)
  • deploy: An account responsible for deploying code to the server and compiling it
  • app: The account under which the application is run

The deploy account is added to the app’s group because we will be using the app group as a way for the deploy account to share the compiled executable with the app.

sudo adduser --gecos "" admin
sudo adduser --gecos "" deploy
sudo adduser --gecos "" --disabled-login --disabled-password app

# Prevents the welcome screen from appearing
sudo -i -u admin touch /home/admin/.hushlogin
sudo -i -u deploy touch /home/deploy/.hushlogin

# Makes the deploy user part of the app group and gives it write access to the app's home directory.

sudo usermod -aG sudo admin
sudo usermod -aG app deploy
sudo chmod -R g+rwx /home/app

If you have a personal SSH key, this will be a good time to add it to the authorized keys for both the admin and deploy. Without this step, you will need to enter your credentials every time you wish to SSH into the server.

Setting up an SSH key

If you do not have an SSH key on your local machine, this is a good time to generate one so that you may use it in the future with your server or services like Github.

First, run the command below on your computer.

[ -f ~/.ssh/id_rsa ] || ssh-keygen -f ~/.ssh/id_rsa echo ~/.ssh/id_rsa.pub

The output of the echo command will be your public SSH key. It will look like ssh-rsa [various numbers and letters] [you@yourmachine] Use this string anywhere you wish to connect via SSH without the use of credentials.

Paste your SSH key in the string section of this first command.

# Run this on the remote server
PUB_SSH_KEY="paste the key here"

And run the following commands to allow for password-less entry as either the admin user or the deploy user.

sudo -i -u admin mkdir /home/admin/.ssh
sudo -i -u admin touch /home/admin/.ssh/authorized_keys
sudo bash -c "echo \"$PUB_SSH_KEY\" >> /home/admin/.ssh/authorized_keys"

sudo -i -u deploy mkdir /home/deploy/.ssh
sudo -i -u deploy touch /home/deploy/.ssh/authorized_keys
sudo bash -c "echo \"$PUB_SSH_KEY\" >> /home/deploy/.ssh/authorized_keys"
PUB_SSH_KEY=""

You should now be able to freely SSH from your computer as either the admin or the deploy user.

3. Customizing the environment

This next step is all about customizing the Ubuntu machine to host our app. This step is a series of customizations of the configurations for the various services we’ll need to secure the machine and enable HTTP hosting.

If are you unfamiliar with editing text files through the command line, you may want to look up nano and vi online. The first is a rather simple editor for text files. The second is a more powerful, albeit more complex editor. Either will suffice for our needs.

First, I edit the SSH configuration file to secure access to the server. I do not modify the default SSH port, but it is recommended for production machines.

The configuration below only allows the vagrantadmin, and deploy users SSH privilege. Root-level access should in no way be allowed via SSH.

> sudo vim /etc/ssh/sshd_config
PermitRootLogin no
AllowUsers vagrant admin deploy
DenyUsers root
DenyGroups root

Next, I configure supervisor. This tool allows the execution of the Notes application as a user of our choosing and the ability to run that application in the background. Without supervisor, or a similar tool, a user will manually have to log into the server and start the application from the command line.

You’ll note that the ownership of the supervisor HTTP server includes the deploy group. This is done so that our deployuser may restart the service once a new version of the app is pushed to the server.

> sudo vim /etc/supervisor/supervisord.conf
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0770
chown=root:deploy

Next, I add a server supervisor configuration for the app we are running. Note that the user is app. I am restricting the application’s access to the permissions afforded to this user.

> sudo vim /etc/supervisor/conf.d/notes_app.conf
[program:NotesServer]
autostart=true
autorestart=true
command=/home/app/build/release/NotesServer
user=app
stdout_logfile=/var/log/supervisor/NotesServer-stdout.log
stderr_logfile=/var/log/supervisor/NotesServer-stderr.log

Finally, I configure NGINX. This famous webserver will proxy all requests from the outside world to the application.

> sudo vim /etc/nginx/sites-enabled/notes_app.conf
upstream app {
  server localhost:8080;
}

server {
  listen 80;
  server_name 127.0.0.1; # for production use the name of the server instead.

  location / {
    proxy_set_header HOST $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://app;
    proxy_redirect off;
  }
}

Once all is done, I restart all the services we customized. This is the last step. It will prevent further access to the server for the root account, so it’s important that the admin user is also a sudo user. Otherwise, there will be no way to further adminstrate the machine.

sudo service nginx restart
sudo service ssh restart
sudo service supervisor restart

4. Deploy to an environment

With all the accounts created, it’s now time to start deploying the app to the remote server. I have moved all of the commands to a single script, Scripts/deploy.sh that can be run from the command line on your machine.

In short, here is what the script does:

  1. It copies all of NotesServer and NotesShared on the current machine to the server using rsync
  2. It instantiates a git repo in the NotesShared directory (necessary to get Swift’s package system working when compiling)
  3. It modifies the NotesServer’s Package.swift file to include the NotesShared directory
  4. It compiles the NotesServer application
  5. It links the compiled application to the app’s home directory
  6. It restarts supervisor to use the new application’s executable

Here is the full script.

#
# Configuration variables
#
DEPLOY_USER=deploy
SSH_HOST=127.0.0.1
SSH_PORT=2222

while getopts :h:p: opts; do
   case ${opts} in
      h)
        SSH_HOST=${OPTARG}
        ;;
      p)
        SSH_PORT=${OPTARG}
        ;;
      \?)
        echo "Invalid option -$OPTARG" >&2
        exit 1
        ;;
   esac
done

# Directory with the different code bases
DEPLOY_HOME=/home/deploy
DEPLOY_DIR=`date  +%Y%m%dT%H%M%S`
DEPLOY_ROOT="$DEPLOY_HOME/$DEPLOY_DIR"
DEPLOY_SHARED_ROOT="$DEPLOY_ROOT/NotesShared"
DEPLOY_SERVER_ROOT="$DEPLOY_ROOT/NotesServer"
DEPLOY_BUILD_PATH="$DEPLOY_SERVER_ROOT/.build"

APP_HOME=/home/app
APP_LINK_PATH="$APP_HOME/build"

echo "Deploying to $SSH_HOST:$SSH_PORT"

# Copies both the shared and server code to the remote server
rsync -av -e "ssh -p $SSH_PORT" NotesShared NotesServer \
	--exclude=*.xcodeproj \
	--exclude=.build \
	$DEPLOY_USER@$SSH_HOST:$DEPLOY_ROOT

# Compiles the code on the remote server.
# The git code is necessary because of the way swift packages work: # they require the presence of a git repo to link one package
# to another. So this script creates a v1.0.0 git release for the
# shared code on the server and links that package to the server's
# package.

ssh -T $DEPLOY_USER@$SSH_HOST -p$SSH_PORT << EOSSH

echo "-- Initializing Git in the shared code root"
cd $DEPLOY_SHARED_ROOT
git config --global user.email "$DEPLOY_USER@localhost"
git config --global user.name "deploy"
git init
git add .
git commit -am "NotesShared"
git tag 1.0.0

echo "-- Building the server"
cd $DEPLOY_SERVER_ROOT
sed -i 's/dependencies:\s*\[/dependencies: [\n    .Package(url: "\.\.\/NotesShared", majorVersion: 1),/g' Package.swift
swift build -c release

echo "-- Stopping supervisor"
supervisorctl stop all

echo "-- Linking the app"
[ -e $APP_LINK_PATH ] && rm $APP_LINK_PATH
ln -s $DEPLOY_BUILD_PATH $APP_LINK_PATH
chown -R deploy:app $DEPLOY_BUILD_PATH
chmod 0750 $DEPLOY_BUILD_PATH

echo "-- Restarting supervisor"
supervisorctl start all

exit
EOSSH

Once the deploy script is successfully run, you can access the notes app via it’s IP address/domain name (127.0.0.1:8000 if using the Vagrant configuration)

In Closing

There are a few things that I left out for another post. Specifically, there is currently no way of storing data to the server in a permanent way (e.g. using a relational database or document store), there is also no SSL on the connection between the app and the server, and there is no directory on the server for binary assets.

The real challenge of this process was linking the NotesServer directory to the NotesShared directory for compilation. Using the right packages for sharing NotesShared between app and server also proved tricky as they had to use no more than Foundation.

Having said all that, I am happy with the outcome and the ease with which the deployment takes place.


The application, the Vagrant configuration and all scripts can be found the NotesApp repo.