Tag Cloud
elasticsearch aws ecs apache spark scala AWS EMR hadoop golang telegram bot webhooks ssl nginx digital-ocean emr apache pig datapipeline found.io elastic-cloud rails try capybara docker devops capistrano heka bigquery kafka protobuf vim iterm javascript emberjs git scripting dnsmasq bem frontend meteorjs meteorite heroku

How I Deployed My Golang Telegram Bot

Continuing where I left off in the first part of my Golang Telegram Bot, in this post I go through all the steps I took to get to a one command deployment of my Telegram bot into a Digital Ocean Ubuntu 16.04 instance. A number of components were involved: dockerizing the app, setting up a self-signed SSL cert, get the Nginx to work as a reverse proxy with that, submitting the SSL to Telegram, and finally setting up git post-update webhooks for deployment.

Dockerizing the App

To prepare the bot for deployment, a natural choice was to dockerize it. I’ve found it simpler, by far, to use docker to get the environment needed for my apps to run instead of manually fiddling with the server in most cases. It also gives me the benefit of being able to run multiple containers on a single instance if the load isn’t too high on them. Let’s dive in to the code.

Dockerfile
FROM golang:1.6

ADD configs.toml /go/bin/

ADD . /go/src/github.com/aranair/remindbot
WORKDIR /go/src/github.com/aranair/remindbot

RUN go get ./...
RUN go install ./...

WORKDIR /go/bin/
ENTRYPOINT remindbot

EXPOSE 8080

I start from the base Golang 1.6 image.

From there, the next line adds a configs.toml into the bin folder. This file contains credentials and configs that my app needs to run. This should be added into the server manually, so that it doesn’t get checked into the github repository for security reasons.

I took a look at the official Golang Dockerfile and I saw that the default gopath is /go. By adding my config item into /go/bin folder, I can easily give the app direct access to the file, without having to provide it an additional arbiturary path to get that file.

The next line adds the files into the image during the build process. Previously, I would get the files in a different way. I would use this:

go get /go/src/github.com/aranair/remindbot

But it’s actually a little easier to do it in the above way:

ADD . /go/src/github.com/aranair/remindbot

This would take all the files at ., the location where docker build would run from, and copy them into /go/src/github.com/aranair/remindbot during the build process, essentially achieving the same result as go get ....

What’s different here is that I don’t -have- to remember to push to github before the deployment. I also wouldn’t need to manually git pull. The entire deployment process can be contained inside the post-update webhook. I’ll discuss that in more detail further down.

Docker Compose

Personally, I hate fiddling with manual running of the containers so I just use docker-compose, especially if there is more than one component to worry about. For this bot, there is really only the mounted volume so that my sqlite3 files won’t get flushed on a deployment but I could just as easily set-up a PostgreSQL database up with a few simple additions to this file.

version: '2'
services:
  gobot:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - /var/data:/var/data

The section under ports exposes and links the container’s port 8080 to the server’s port 8080 (HOST::CONTAINER format). More about that can be found in the compose-file documentations.

For the volumes, the code above simply tells the container to link the host’s /var/data/ to the container’s /var/data/ essentially creating a mounted volume. Again, the format is HOST::CONTAINER. I store the files there for my Sqlite database and this mounted volume preserves the data on deployments.

How to Set-up Self-Signed SSL Certificate

One of the main hassles and requirements of the Telegram API is that they require https, and that requires an SSL certificate. It encrypts communication between Telegram and my server, and this helps Telegram to verify that my server is the correct one, and not a bogus one when a potential man-in-the-middle hijacks the traffic.

I could go get an SSL cert from a provider, but in this case, what we’re really concerned is the identity of my server to Telegram and not for users so a self-signed certificate would work just as well.

I SSH’d into the server to create the certs.

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/self-signed.key -out /etc/ssl/certs/self-signed.crt
  • -days defines the validity span. I did adjust the validity-days to something much bigger but generally might be better with a year or two.
  • rsa:2048 means that will create an RSA key that is 2048 bits long.
  • -keyout refers to the private key for the cert
  • -out refers the cert itself

The process leads me through the steps above for some further information. The most important bit is the Common Name. In my case, I didn’t have a domain, so I simply put in the server_IP_address for my Digital Ocean instance.

Country Name (2 letter code) [AU]:SG
State or Province Name (full name) [Some-State]:Singapore
Locality Name (eg, city) []:Singapore
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:server_IP_address
Email Address []:boa.homan@gmail.com

As part of Nginx best practices, I also created a strong Diffie-Hellman group for added security.

sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

Configuring Nginx to Use the SSL Cert

I first made a new Nginx configuration snippet at /etc/nginx/snippets that simply points to the SSL certs I just created above.

ssl_certificate /etc/ssl/certs/self-signed.crt;
ssl_certificate_key /etc/ssl/private/self-signed.key;

Another snippet to set-up some SSL settings based on recommendations from https://cipherli.st/.

# from https://cipherli.st/
# and https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Disable preloading HSTS for now.  You can use the commented out header line that includes
# the "preload" directive if you understand the implications.
#add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

ssl_dhparam /etc/ssl/certs/dhparam.pem

Finally, we will need to edit the Nginx configuration files to use SSL.

This is what the file intially looks like:

# /etc/nginx/sites-available/default

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    # SSL configuration

    # listen 443 ssl default_server;
    # listen [::]:443 ssl default_server;

    ...

I changed it to look like this (remember to replace IP!)

server {
  listen 80 default_server;
  listen [::]:80 default_server;
  server_name YOUR_SERVER_IP_ADDRESS;
  return 301 https://$server_name$request_uri;
}

server {
# SSL configuration

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

  include snippets/self-signed.conf;
  include snippets/ssl-params.conf;

  location / {
    proxy_pass http://127.0.0.1:8080;
  }
}

What I did was to basically ask Nginx to automatically redirect HTTP requests to HTTPS. And ask it to server root from the port 8080 that the docker container is listening to.

UFW Firewall

For security reasons, I also enabled ufw firewall by doing the following:

sudo ufw enable
sudo ufw allow 'OpenSSH'
sudo ufw allow 'Nginx Full'
sudo ufw delete allow 'Nginx HTTP'

It should look something like this in sudo ufw status

To                         Action      From
--                         ------      ----
Nginx Full                 ALLOW       Anywhere
OpenSSH                    ALLOW       Anywhere
Nginx Full (v6)            ALLOW       Anywhere (v6)
OpenSSH (v6)               ALLOW       Anywhere (v6)

And its done; to start Nginx, all that’s left is to sudo nginx -t

Sending Telegram the SSL Cert

Telegram will need the other side of the cert. Consulting their documentation, it seems that they need the PEM file. To get that, I had to convert the current CRT file into the PEM format.

openssl x509 -in /etc/ssl/certs/self-signed.crt -outform pem -out /etc/ssl/certs/bot.pem

To get the cert to Telegram’s hands, I sent it via their API using this: (Replace bot keys!)

curl -F "url=https://your.domain.or.ip.com" -F "certificate=@/etc/ssl/certs/bot.pem" https://api.telegram.org/bot12345:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/setWebhook

Set-up Git Hooks

Great! Now all that’s left is the deployment process. The general outcome that I wanted is that it’ll be a one-command process that updates the code in the server, rebuilds and restarts the docker container with the updated app.

At my server, I created the necessary files for the server git repository

cd /var
mkdir repo && cd repo
mkdir bot.git && cd bot.git
git init --bare

The Hooks

Looking into the hooks folder in bot.git, there were many samples for the different hooks provided. For the purpose of this bot, I created a post-receive hook with the following content.

#!/bin/sh
git --work-tree=/var/app/remindbot --git-dir=/var/repo/bot.git checkout -f
cd /var/app/remindbot
docker-compose build
docker-compose down
docker-compose up -d

The sets the /var/app folder to be the working directory for my app. And the script goes into my working directory and rebuilds the container and restarts it. All of this will happen on deployment!

Of course, I also had to make the post-receive file executable.

chmod +x post-receive

Deploy All The Things!

From my development machine, I added a remote repo to my local git repo.

git remote add prod ssh://user@my.domain.or.ip.com/var/repo/bot.git
git push prod master

And the Telegram bot finally goes live, responding to live chats in Telegram. All of this code can be found at the project github repository. The reminder bot’s name is Hazel. She responds instantly to chats in Telegram and helps me to manage my ever-growing list of responsibilities everyday now :P

Feel free to star it or fork it or leave comments below!

comments powered by Disqus