Deploy feature branches

Showcasing Gitlab CI, Nginx, lego.

If you use pull requests or merge requests as part of your workflow; making and deploying feature branches can be helpful for sharing testable versions.

For this example lets take a look at deploying a web app to a web-server, with Lets Encrypt on a wildcard certificate.

A static typescript-based web app. Need to install dependencies on the build server, run tests, build app. Need to point the domain to the feature distribution on a web-server.

https://my-feature-branch.feature.example.com

To get a wildcard domain the post on using lego and DNS to authenticate your domain. If you use certbot you could use the a python plugin for your DNS service.

lego --email name@example.com --domains *.feature.example.com run

See --dns options for your DNS service to automate this. Also --http and --tcp which also might work well to validate your domain if you already have a primary domain (for example https://example.com, or http://example.com/.well-known/acme-challenge) setup on this domain.

lego, by default, stores your certificates in ./.lego/certificates/. You can define where you’d like these to go.

Then add to your web-server. Nginx, for example, supports regex in the site name, which you can use as a capture to modify where to look for files.

Note: Consider this regex as a vector to access other files, but limited to the running Nginx user (usually www-data). Limit to certain characters in your regex.

server {
        server_name ~^(.*)\.feature\.example\.com$;

        listen 443 ssl http2;
        listen [::]:443 ssl http2;

        access_log /var/log/nginx/wildcard.access.log main_ext;

        root /usr/lib/$1;

        location / {
                try_files $uri $uri/ =404;
        }

        ssl_certificate /home/letsencrypt/.lego/certificates/_.example.com.crt;
        ssl_certificate_key /home/letsencrypt/.lego/certificates/_.example.com.key;
}

Now you need somewhere to point to in the deployment.

This regex gives you a feature match (generous matching) with $1. You can use this in your root path to point to where you deployed you feature branch.

Showcasing Gitlab runner CI but most build systems working with git should allow you to do jobs based on branch name or type of branch (merge_request, pull_request, feature/.*).

For example, gitlab-ci.yml

feature-build:
  stage: build
  script:
		- make
    - tar -czf $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-build.tar.gz release/target
  artifacts:
    paths:
      - $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-build.tar.gz
  only:
    - merge_requests

We create a build using make and then tarball it up into an artifact using $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME.

deploy_feature_site:
  stage: deploy
  image: kroniak/ssh-client
  before_script:
    - mkdir -p ~/.ssh
    - echo -e "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
    - chmod 600 ~/.ssh/*
  script:
    - echo "Deploy $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-build.tar.gz"
    - scp $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-build.tar.gz
      user@example.com:/www/builds/
    - ssh user@example.com "mkdir -p
      /www/builds/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME && tar -xf
      /www/builds/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-build.tar.gz --directory
      /www/builds/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
    - ssh user@example.com "mkdir -p
      /www/release/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME/ && ln -sfn
      /www/builds/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME/release/target/$CI_COMMIT_SHORT_SHA/
      /www/release/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME/www"
  environment:
    name: feature/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
    url: https://$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME.feature.example.com
  only:
    - merge_requests

Here we deploy using ssh and scp on a small ssh-client container. You may find deploying into a container and then to a private registry might be easier.

image: kroniak/ssh-client

Needs a SSH_PRIVATE_KEY of your deploy key on the server to be set in the Gitlab Project Settings, Variables.

	before_script
    - mkdir -p ~/.ssh
    - echo -e "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
    - chmod 600 ~/.ssh/*

Then deploy artifact using scp.

- scp $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-build.tar.gz \
  user@example.com:/www/builds/

Deploying artifact into /www/builds but I would suggest making a pattern that works for you.

Deploy to a new directory with the commit so each commit has a fresh release.

- ssh user@example.com "mkdir -p
  /www/builds/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME && tar -xf
  /www/builds/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME-build.tar.gz --directory
  /www/builds/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
/www/builds/my-feature-branch/release/target/29e09d023

Then link to the release to the newly deployed artifact.

- ssh user@example.com "mkdir -p
  /www/release/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME/ && ln -sfn
  /www/builds/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME/release/target/$CI_COMMIT_SHORT_SHA/
  /www/release/$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME/www"
/www/release/my-feature-branch/www -> /www/builds/my-feature-branch/release/target/29e09d923

Downside of this process is there is no cleanup of deployed artifacts and needs cleaning up of older artifacts. Having a history of artifacts allows for rapid downgrading for failure cases. Another alternative is using containers which handle this invalidation a little better.

Last bit is updating your Nginx config to the place you chose.

	root /www/release/$1/www;

Then reload Nginx.

sudo nginx -s reload

And then you should be able to see your released static web app.

https://my-feature-branch.feature.example.com

Note: Your feature branches are open to the public. Consider how you might want to handle this.