genox.ch

22.8.2018
Filed under [object Object]
3a9f72ed-0277-43d6-87dd-1d7e5b859054

Gitlab CI/CD with zeit.co's now

I recently had the opportunity to play around with zeit.co's deployment service (also where this blog is hosted). I used Gitlab's pipelines to set up a docker container deployment workflow that includes automated review and staging builds and a manual step for deploying master to production.

Update: I edited the gitlab pipeline to clean previous deployments and name deployments with prefixed $APPS_DOMAIN in order to have a less confusing list on zeit.co's deployment list. Also, all but production builds now clean previous deployments automatically on a new deployment. The $NOW_REGION variable is no longer necessary, instead all region specific settings are in now.json using "scale".

Update 2: now updated to use their serverless Docker infrastructure. Handle no existing deployments when trying to remove old deployments. Dockerfile: remove devDepencencies after build.

Stages

  • The review stage is active for all branches except master. So whatever your branch is called, it creates a now deployment with a subdomain based on the branch name.
  • Each commit to master triggers a refresh of the staging subdomain.
  • Each staging build can then be manually deployed to replace the current production container.

Gitlab and Now

  • It requires two Gitlab CI/CD variables: a now token and the domain of your app.
  • now region: Gitlab runners are located somewhere in North America and since now uses the (I guess) GeoIP information of the machine that launches a now build, it always goes for a North American datacenter, in my case "sfo". But I want my build and subsequent deployment to end up in europe, so I go for "bru" (Brussels) there.
  • now teams: Since I use a zeit.co account that is linked to a team and the deployment is using the team accounts domain, I need to specify the team name as a parameter for the now binary. If you don't need that, you can remove the variable's content.
  • now domains: For convenience, I also host my domain / zone file on zeit. That allows for some very neat and easy aliasing.

For this to work properly, you need to be able to run now from your local machine (e.g. now.json config or a now-key in package.json and a Dockerfile). If your deployment runs fine locally, it will - quite probably - do so in a Gitlab runner instance.

A note about regions

While testing zeit.co without applying any scaling I noticed that the TTFB is significantly higher than with my previous hosting environment. Somewhere in a constant 800ms range - from brussels. And I guess this is why: containers on now that do not have scaling configured will be frozen after a certain timeout. With scaling configured (e.g. in now.json), TTFB is almost halved. I guess that non-scaling deployments are behind a different load balancer setup than scaled deployments. Or something like that…

I switched off zeit.co's domain CDN to save a few bucks (nobody cares if this blog loads in 400ms or 1.5s…), turned on scaling and voila, the datacenter in Brussels seems to be doing just fine for European traffic. I ran a few tests on pingdom.com via San Jose and the TTFB from the eastern US is about 2.5s. I'm not concerned with that currently but might extend my config to spin up 2 instances in SFO and BRU, just to see what would be possible.

Configurations

now.json: name the project the same as $APP_DOMAIN-production in order for it to pick up aliases. I set the region to "bru1" for europe and set "scale" to minimum 1 instance and maximum 3. Set this according to your requirements.

now.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "name": "genox.ch-production",
  "alias": [
    "genox.ch",
    "www.genox.ch"
  ],
  "type": "docker",
  "scale": {
    "bru1": {
      "min": 0,
      "max": "auto"
    },
    "sfo1": {
      "min": 0,
      "max": "auto"
    }
  },
  "features": {
    "cloud": "v2"
  }
}
.gitlab-ci.yml
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
image: node:8-alpine

stages:
- review
- staging
- production


variables:
  NOW_TEAM: ""
  NOW_SECRET: "--token=${NOW_TOKEN}"
  NOW_BUILD_PARAMS: "--force"

before_script:
- npm install -g now --silent --unsafe-perm

review:
  stage: review
  script:
  - echo "Deploying app to [review] environment as $APPS_DOMAIN-$CI_BUILD_REF_SLUG"
  - HAS_DEPLOYMENTS=$(now $NOW_SECRET $NOW_TEAM ls | grep -q ${APPS_DOMAIN}-${CI_BUILD_REF_SLUG} && echo 1 || echo 0)
  - NOW_DEPLOYMENT=$(now ${NOW_SECRET} ${NOW_TEAM} ${NOW_BUILD_PARAMS} -n ${APPS_DOMAIN}-${CI_BUILD_REF_SLUG})
  - sleep 10
  - now $NOW_SECRET $NOW_TEAM alias set $NOW_DEPLOYMENT $CI_BUILD_REF_SLUG.$APPS_DOMAIN
  - if [[ $HAS_DEPLOYMENTS -gt 0 ]]; then now $NOW_SECRET $NOW_TEAM rm $APPS_DOMAIN-$CI_BUILD_REF_SLUG --safe --yes; fi
  environment:
    name: review/$CI_BUILD_REF_NAME
    url: https://$CI_BUILD_REF_SLUG.$APPS_DOMAIN
    on_stop: stop_review
  only:
  - branches
  except:
  - master

stop_review:
  stage: review
  script:
  - HAS_DEPLOYMENTS=$(now $NOW_SECRET $NOW_TEAM ls | grep -q ${CI_BUILD_REF_SLUG} && echo 1 || echo 0)
  - echo "Removing $APPS_DOMAIN-$CI_BUILD_REF_SLUG from [review] environment"
  - now $NOW_SECRET $NOW_TEAM alias rm $CI_BUILD_REF_SLUG.$APPS_DOMAIN
  - if [[ $HAS_DEPLOYMENTS -gt 0 ]]; then now $NOW_SECRET $NOW_TEAM rm $APPS_DOMAIN-$CI_BUILD_REF_SLUG --safe --yes; fi
  variables:
    GIT_STRATEGY: none
  when: manual
  environment:
    name: review/$CI_BUILD_REF_NAME
    action: stop

staging:
  stage: staging
  script:
  - echo "Deploying app to [staging] environment as $APPS_DOMAIN-staging"
  - HAS_DEPLOYMENTS=$(now $NOW_SECRET $NOW_TEAM ls | grep -q staging && echo 1 || echo 0)
  - NOW_DEPLOYMENT=$(now ${NOW_SECRET} ${NOW_TEAM} ${NOW_BUILD_PARAMS} -n ${APPS_DOMAIN}-staging)
  - sleep 10
  - now $NOW_SECRET $NOW_TEAM alias $NOW_DEPLOYMENT staging.$APPS_DOMAIN
  - if [[ $HAS_DEPLOYMENTS -gt 0 ]]; then now $NOW_SECRET $NOW_TEAM rm $APPS_DOMAIN-staging --safe --yes; fi
  environment:
    name: staging
    url: https://staging.$APPS_DOMAIN
  only:
  - master

production:
  stage: production
  script:
  - echo "Deploying app to [production] environment as $APPS_DOMAIN-production"
  - HAS_DEPLOYMENTS=$(now $NOW_SECRET $NOW_TEAM ls | grep -q production && echo 1 || echo 0)
  - NOW_DEPLOYMENT=$(now ${NOW_SECRET} ${NOW_TEAM} ${NOW_BUILD_PARAMS} -n ${APPS_DOMAIN}-production)
  - sleep 10
  - now $NOW_SECRET $NOW_TEAM alias
  environment:
    name: production
    url: https://$APPS_DOMAIN
  when: manual
  only:
  - master

It takes a while to build during rush hours (well, I'm using Gitlab's free runners, so I don't expect any miracles). But all in all, this is a very simple, very effective way to run some minor devops for a nextjs app.

For completeness, here's my Dockerfile:

Dockerfile
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
30
FROM node:8-alpine

ENV NPM_CONFIG_LOGLEVEL warn
ARG NODE_ENV
ENV NODE_ENV=${NODE_ENV}
ARG HOSTNAME
ENV HOSTNAME=${HOSTNAME}
ARG NOW_URL
ENV NOW_URL=${NOW_URL}
ARG NOW_REGION
ENV NOW_REGION=${NOW_REGION}
ARG NOW_DC
ENV NOW_DC=${NOW_DC}
ARG NOW
ENV NOW=${NOW}

COPY . ./next/

WORKDIR /next

RUN npm config set color false &&\
    npm config set unsafe-perm true &&\
    npm install -g npm &&\
    npm run next:install &&\
    npm run next:build &&\
    rm -rf ~/.npm && npm prune --production

CMD npm run next:start

EXPOSE 80

2