A simple setup for a Build and Deploy system using GitHub Actions Dec 12 2020
I've been using GitHub Actions on a few projects now, and I find them like the future of what bash scripting was back in the day. I feel I can do pretty much everything in a quick and concise way. In this post, I'll show you the setup I use for a simple Build and Deploy GitHub Action.
This post is not an in-depth view on GitHub Actions, but let's review some basic concepts, so we know what we are talking about.
Table of Contents
GitHub Actions
If you haven't used GitHub Actions before, they are small workflows you can define for your repository that can be triggered by different events. The following types of events can trigger the workflows:
- Scheduled - similar to CRON, you schedule an event to a specific time/date.
- Manual event - call via GitHub API.
- Webhook events - Events related to the life cycle of your repository.
If you want to learn more about it, check the events documentation.
Now we need to define where our workflow will run, in GitHub lingo, they are called Runners. You can define your own runners, but we'll use GitHub-hosted runners, the limits are enough for small projects.
To define the runner's action and type, we use a .yml
file, which, if you are familiar with Infrastructure as Code, you'll feel at home. This .yml
file is known as a metadata file.
We have three types of Actions:
- JavaScript - Run one action; the actions are decoupled from the environment.
- Composite - Joins multiple run actions.
- Docker - The action runs on a defined environment, i.e. a container. The environment is coupled with the action.
You can learn more about defining the metadata file by reading the metadata documentation.
OK, that should be enough theory to get us started.
The build and deploy architecture.
Let's define what we want to accomplish. We want to be able to build our final product using the GitHub-Hosted runner, and when it is ready, we want it to push it to a server via SSH automatically. This simple schema can be used for static sites, react applications, etcetera. Anything really, we can use this schema for anything that can be built, packaged, and published somewhere else.
This is what our action will do in broad strokes:
- Copy our repository code to the runner.
- Build the project.
- Copy the built product to the public server.
We are going to use rsync(1)
to copy the built product to the public server. That means that we need to create an ssh key-pair and add our public key to the ~/.ssh/authorized_keys
file on the public server. If you are unfamiliar with the process, read my post Understanding SSH Keys and using Keychain to manage passphrase on macOS, especially the section called "Key pairs".
Because we don't want to hard-code our private information on the metadata file we are going to make use of GitHub Secrets. Of course, you have to be careful who can connect to your server, and which privileges they have. I would suggest removing all privileges not related to putting the files in a specific directory. This way, we try to minimise the damage someone can do if they login with that user to the server.
That is the general gist of our build and deploy architecture. Let's start implementing it.
Using GitHub Secrets for private information
If you are following along, now is the time where we would create our SSH key-pair if you don't have the keys already. I will use the following command to generate the key-pair:
1
$ ssh-keygen -t rsa -b 4096 -C " key for GitHubAction" -f ~/.ssh/id_github
The command will generate two files, the private key (id_github
) and the public key (id_github.pub
). Copy the content of the public key and copy it to your public server's ~/.ssh/authorized_keys
file. If you are in macOS, you can use the following command to copy the public key to the clipboard:
1
$ cat id_github.pub | pbcopy
Then connect via ssh to the server and add it to your ~/.ssh/authorized_keys
file. If the file doesn't exist, create it. You could also use ssh-copy-id(1)
command:
1
$ ssh-copy-id -i id_github user@YOURSERVER.com
Whichever method you chose make sure you can log in via ssh with the private key.
Create the following secrets in the GitHub repository. You can do this by going to the Secret section inside the repository Settings:
- DEPLOY_HOST - Server Host to deploy to.
- DEPLOY_PORT - Server port to connect over SSH.
- DEPLOY_USER - User to connect with.
- DEPLOYKEY - Private Key (the public key should be added to the server's authorizedkeys)
- DEVENVFILE - The contents of the .env file to use on the server
The DEV_ENV_FILE
is only required if you want to include it on your public server, I'll use a React app as a demo, but you can modify it to suit your needs.
Creating the metadata file
GitHub repositories use the .github
directory in the root of your repository to keep track of files that relate specifically to GitHub. For example, if you add the file pull_request_template.md
to your .github
directory, that file will be used as a template for your pull requests.
Create .github
folder in your repository if you don't have it already, and create the directory workflows
inside it. Inside the workflows
directory, we'll store our actions metadata files. I'll name my file deploy-to-server.yml
(you can give it another name if you want), and add the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
name: "Deploy to Server"
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: "Build deployment code for standalone"
env:
remote_host: ${{ secrets.DEPLOY_HOST }}
remote_port: ${{ secrets.DEPLOY_PORT }}
remote_user: ${{ secrets.DEPLOY_USER }}
remote_key: ${{ secrets.DEPLOY_KEY }}
dev_env_file: ${{ secrets.DEV_ENV_FILE }}
local_dir: "build/"
remote_dir: "/var/html"
run: |
npm install
CI=false npm run build
mkdir ~/.ssh
echo "$remote_key" > ~/.ssh/id_github
chmod 600 ~/.ssh/id_github
echo "$dev_env_file" > .env
chmod 600 .env
rsync -avzr --delete -e "ssh -p ${remote_port} -i ~/.ssh/id_github -o StrictHostKeyChecking=no" ${local_dir} ${remote_user}@${remote_host}:${remote_dir}
rm ~/.ssh/id_github
The on:
section specifies which events will trigger this action. In my case, I decided to use workflow_dispatch
, so I can run the action manually from the Actions option in the GitHub web interface of the repository.
The Action will run on the GitHub-Hosted runner based on ubuntu-latest
. It includes the rsync(1)
command. The action also uses
a predefined action checkout
that will get our code from the master branch into the runner. You can learn more about it from the GitHub Checkout repository, and about other actions on the GitHub Toolkit repository.
We define environment variables extracting the information from the GitHub secrets and finally run the series of commands to compile the code, set up the SSH keys, and send the files to the server using rsync
.
Running the Action
Once you have the files ready your repository should have something similar to the following structure for the .github
directory:
1
2
3
.github/
└── workflows
└── deploy-to-server.yml
You can now commit the code and push it to GitHub. Once in your GitHub repository, you can run the action manually by going to the Actions
section on GitHub's web interface. Under Workflows you'll see the Deploy To Server
workflow, select it and you'll be able to click Run workflow
. Run it, and if everything worked fine, you'd see a green tick next to the workflow.
And that's it, congratulations on your simple build and deploy pipeline.
Final Thoughts
I hope you can see how this approach could easily be adapted to any other build and deploy systems. As I mentioned before, it feels like GitHub Actions are the bash scripts of the CI/CD pipeline options. GitHub actions are easy to set up, and you don't need to set up a Jenkins server for such a simple build and deploy pipeline.
Just remember to keep your private key secure and also to limit the commands the user can run when it logs in. In the end, you are setting up the credentials that will allow someone to connect to your server using ssh.
I hope you find the post useful.
Related topics/notes of interest
- GitHub Action's general documentation.
- Event triggers documentation.
- Usage Limits for GitHub-Hosted Runners.
- Metadata documentation.