<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Frontend development
Loading

DevOps |

How to Deploy a HeyEmoji Slack App to AWS using Terraform

Learn to deploy the HeyEmoji Slack app to AWS using Terraform+Ansible, while orchestrating the tools using BitOps!

Phil Henning

Phil Henning

Twitter Reddit

In This Series: Deploying HeyEmoji using BitOps

Last Updated: December 07, 2022

HeyEmoji is a fantastic reward system teams can use to recognize each other's accomplishments, dedication, and hard work. Once you get it set up, you can mention a colleague's Slack username in any channel along with a pre-configured reward emoji - you can even include a short description of what they did that was so awesome it deserved a shoutout.

The best part? When you send an emoji to a colleague, they get emoji points, which can be tracked on a leaderboard. Competing to see who can be most helpful, considerate, or skilled at their jobs is a pretty fun way to make the day fly by. 

Want to get HeyEmoji on your own work Slack channel? This tutorial walks you through how to deploy the HeyEmoji Slack app to AWS using Terraform+Ansible so your team can enjoy Slack-generated kudos. 

You'll be orchestrating your tools using BitOps! BitOps is a declarative infrastructure orchestration tool that allows teams to write their infrastructure as code and deploy that code easily across multiple environments and cloud providers. 

You'll set up an operations repo, configure Terraform and Ansible, and finally deploy the HeyEmoji slack bot to AWS.

Table of Contents

Required Tools

NOTE: This tutorial involves provisioning an EC2 instance and deploying an application to it. Because of this, there will be AWS compute charges for completing this tutorial.

These steps will take approximately 20-30 minutes to complete. If you prefer to skip these steps, the code created for this tutorial is on Github.

Before you begin:

Every section starts with a brief explanation of what you will accomplish, followed by the file name and directory path you will be creating and the code that you need to add to new files.

In a few places, you need to replace template strings with your specific credentials. These instructions are explicitly stated and noted with UPPERCASE letters in the code.

Setting Up Your Operations Repo

In this tutorial you will be following best practices by keeping your application and operation repos separate.

On your local machine, create a directory called operations-heyemoji. Navigate to this directory and use yeoman to create an environment directory. Install yeoman and generator-bitops with the following:

npm install -g yo
npm install -g @bitovi/generator-bitops

 

Run yo @bitovi/bitops to create an operations repo. When prompted, name your environment “test”. Answer “Y” to Terraform and Ansible, and “N” to the other supported tools.

 


Configure Terraform

Managing Terraform State Files

In the terraform directory create a new file called bitops.before-deploy.d/create-tf-bucket.sh with the following content:

#!/bin/bash 
aws s3api create-bucket --bucket $TF_STATE_BUCKET --region $AWS_DEFAULT_REGION --create-bucket-configuration LocationConstraint=$AWS_DEFAULT_REGION || true

Any shell scripts in test/terraform/bitops.before-deploy.d/ will execute before any Terraform commands. This script will create an S3 bucket with the name of whatever we set the TF_STATE_BUCKET environment variable to.

You need to pass in TF_STATE_BUCKET when creating a container. S3 bucket names must be globally unique.

Terraform Providers

Providers are integrations, usually created and maintained by the company that owns the integration, that instruct Terraform on how to execute on the infrastructure's desired state. For the AWS provider, you will specify your AWS bucket name as well as what integrations your Terraform provisioning will need.

terraform/providers.tf

terraform {
    required_version = ">= 0.12"
    backend "s3" {
        bucket = "heyemoji-blog"
        key = "state"
    }
}

provider "local" {
    version = "~> 1.2"
}

provider "null" {
    version = "~> 2.1"
}

provider "template" {
    version = "~> 2.1"
}

provider "aws" {
    version = ">= 2.28.1"
    region  = "us-east-2"
}

Providing a Home with Virtual Private Cloud

Next up, create another file called vpc.tf

This is where you configure our AWS Virtual Private Cloud within which your application will be hosted.

terraform/vpc.tf
/* get region from AWS_DEFAULT_REGION */
data "aws_region" "current" {}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags = merge(local.aws_tags,{
    Name = "heyemoji-blog-vpc"
  })
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = local.aws_tags
}

resource "aws_subnet" "main" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = aws_vpc.main.cidr_block
  availability_zone = "${data.aws_region.current.name}a"
  tags = local.aws_tags
}

resource "aws_route_table" "rt" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  tags = local.aws_tags
}

resource "aws_route_table_association" "mfi_route_table_association" {
  subnet_id      = aws_subnet.main.id
  route_table_id = aws_route_table.rt.id
}

 

AWS AMI

Retrieve an Amazon Machine Image (AMI), which is like a child object to your AWS userID. The AMI has its own permissions and uses a unique userID for provisioning services. The AMI secures your provisioning and deployment and attaches a known Machine User for trace back.

terraform/ami.tf
data "aws_ami" "ubuntu" {
  most_recent = true
  owners = ["099720109477"]
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

 

Security with AWS Security Groups

You need to tell AWS what permissions our infrastructure has. In the case below, you are opening SSH as well as websocket traffic and stopping any inbound traffic, which is sufficient as you don't need to make your instance accessible from the outside world. 

terraform/security-groups.tf
/* local vars */
locals {
  aws_tags = {
    RepoName = "https://github.com/mmcdole/heyemoji.git"
    OpsRepoEnvironment = "blog-test"
    OpsRepoApp = "heyemoji-blog"
  }
}


resource "aws_security_group" "allow_traffic" {
  name        = "allow_traffic"
  description = "Allow traffic"
  vpc_id      = aws_vpc.main.id
  ingress = [{
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    ipv6_cidr_blocks = null
    prefix_list_ids = null
    security_groups = null
    self = null

  },{
    description = "WEBSOCKET"
    from_port   = 3334
    to_port     = 3334
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    ipv6_cidr_blocks = null
    prefix_list_ids = null
    security_groups = null
    self = null
  }]
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = merge(local.aws_tags,{
    Name = "heyemoji-blog-sg"
  })
}

 

AWS EC2 Instance

Create the instance.tf file. This file tells Terraform that you are provisioning a simple t3.micro ec2 instance, sets the security groups you created, and adds itself to the VPC network.
terraform/instance.tf
resource "tls_private_key" "key" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

resource "aws_key_pair" "aws_key" {
  key_name   = "heyemoji-blog-ssh-key"
  public_key = tls_private_key.key.public_key_openssh
}

resource "aws_instance" "server" {
  ami                         = data.aws_ami.ubuntu.id
  instance_type               = "t3.micro"
  key_name                    = aws_key_pair.aws_key.key_name
  associate_public_ip_address = true
  subnet_id                   = aws_subnet.main.id
  vpc_security_group_ids      = [aws_security_group.allow_traffic.id]
  monitoring                  = true
}

 

Ansible inventory

For the Terraform files, you need to create an inventory and context file that Ansible will use. The inventory.tmpl will be loaded by the Ansible config, and the locals.tf file will inject the ip and ssh_keyfile values into the tmpl file during the Terraform apply stage. 
terraform/inventory.tmpl
heyemoji_blog_servers:
 hosts:
   ${ip} 
 vars:
   ansible_ssh_user: ubuntu
   ansible_ssh_private_key_file: ${ssh_keyfile}

 

terraform/locals.tf
resource "local_file" "private_key" {
  # This creates a keyfile pair that allows ansible to connect to the ec2 container
  sensitive_content = tls_private_key.key.private_key_pem
  filename          = format("%s/%s/%s", abspath(path.root), ".ssh", "heyemoji-blog-ssh-key.pem")
  file_permission   = "0600"
}

resource "local_file" "ansible_inventory" {
  content = templatefile("inventory.tmpl", {
    ip          = aws_instance.server.public_ip,
    ssh_keyfile = local_file.private_key.filename
  })
  filename = format("%s/%s", abspath(path.root), "inventory.yaml")
}

 

Configure Ansible

You just used Terraform to provision services that will host your application.  Next, you'll use Ansible to build and deploy your application to your provisioned services. You'll create a playbook that will detail the necessary build and deploy instructions for your application.

A Note on Images

You are going to clone, build, and deploy your image using Ansible. You'll build and deploy connected but distinct steps in a CI pipeline.

Though in this example we're building and deploying within a single repo, this isn't necessary for all projects. It's an industry standard best practice to keep building and deploying steps separate.

In these steps, you'll keep your setup simple and manually pull and build the image. 

Clean Up Generated Files

Three of the files that were generated are out of scope for this blog. Delete the following generated files/folders: 
  • test/ansible/bitops.after-deploy.d
  • test/ansible/bitops.before-deploy.d 
  • test/ansible/inventory.yml. 

Ansible Playbook

You need to define your Ansible Playbook. This will be your automation blueprint. You will specify which tasks we want to run here and define your tasks in their own files in a later section.
 
Create the following files within the ansible/ folder:
ansible/playbook.yaml
- hosts: heyemoji_blog_servers
  become: true
  vars_files:
    - vars/default.yml
  tasks:
  - name: Include install
    include_tasks: tasks/install.yml
  - name: Include fetch
    include_tasks: tasks/fetch.yml
  - name: Include build
    include_tasks: tasks/build.yml
  - name: Include start
    include_tasks: tasks/start.yml

 

Ansible Configuration

Next, you'll create an Ansible configuration file. This informs Ansible where the terraform-generated inventory file is. It also sets flags so that Ansible can SSH to our AWS provisoned services during the Ansible deployment step.

ansible/inventory.cfg
[defaults]
inventory=../terraform/inventory.yaml
host_key_checking = False
transport = ssh

[ssh_connection]
ssh_args = -o ForwardAgent=yes

 

Ansible Variables

Next, you'll set up ENV vars. Make sure to update the USERNAME and REPO to represent your forked HeyEmoji path.
ansible/vars/default.yml
heyemoji_repo: "https://github.com/mmcdole/heyemoji.git"
heyemoji_path: /home/ubuntu/heyemoji

heyemoji_bot_name: heyemoji-dev
heyemoji_database_path: ./data/
heyemoji_slack_api_token: "{{ lookup('env', 'HEYEMOJI_SLACK_API_TOKEN') }}"
heyemoji_slack_emoji: star:1
heyemoji_slack_daily_cap: "5"
heyemoji_websocket_port: "3334"

create_containers: 1
default_container_image: heyemoji:latest
default_container_name: heyemoji
default_container_image: ubuntu
default_container_command: /heyemoji

 

Ansible Tasks

Now comes the fun part! You have to define your Ansible tasks, which are the specific instructions you want our playbook to execute on. For this tutorial, you need build, fetch, install, and deploy tasks. 
ansible/tasks/build.yml
- name: build container image
  docker_image:
    name: "{{ default_container_image }}"
    build:
      path: "{{ heyemoji_path }}"
    source: build
    state: present

 

ansible/tasks/fetch.yml
- name: git clone heyemoji
  git:
    repo: "{{ heyemoji_repo }}"
    dest: "{{ heyemoji_path }}"
  become: no

 

ansible/tasks/install.yml
# install docker
- name: Install required system packages
  apt: name={{ item }} state=latest update_cache=yes
  loop: [ 'apt-transport-https', 'ca-certificates', 'curl', 'software-properties-common', 'python3-pip', 'virtualenv', 'python3-setuptools']

- name: Add Docker GPG apt Key
  apt_key:
    url: https://download.docker.com/linux/ubuntu/gpg
    state: present

- name: Add Docker Repository
  apt_repository:
    repo: deb https://download.docker.com/linux/ubuntu bionic stable
    state: present

- name: Update apt and install docker-ce
  apt: update_cache=yes name=docker-ce state=latest

- name: Install Docker Module for Python
  pip:
    name: docker

 

ansible/tasks/start.yml
# Creates the number of containers defined by the variable create_containers, using values from vars file
- name: Create default containers
  docker_container:
    name: "{{ default_container_name }}{{ item }}"
    image: "{{ default_container_image }}"
    command: "{{ default_container_command }}"
    exposed_ports: "{{ heyemoji_websocket_port }}"
    env:
      HEY_BOT_NAME: "{{ heyemoji_bot_name }}"
      HEY_DATABASE_PATH: "{{ heyemoji_database_path }}"
      HEY_SLACK_TOKEN: "{{ heyemoji_slack_api_token }}"
      HEY_SLACK_EMOJI: "{{ heyemoji_slack_emoji }}"
      HEY_SLACK_DAILY_CAP: "{{ heyemoji_slack_daily_cap }}"
      HEY_WEBSOCKET_PORT: "{{ heyemoji_websocket_port }}"
    # restart a container
    # state: started
  register: command_start_result
  loop: "{{ range(0, create_containers, 1)|list }}"

 

Create Slack Bot and Add to Workspace

Follow the instructions below from the HeyEmoji README:
1. Browse to https://api.slack.com/apps?new_classic_app=1
2. Assign a name and workspace to your new Slack Bot Application
3. Basic Information > Set display name and icon
4. App Home > Add Legacy Bot User
5. OAuth & Permissions > Install App to Workspace
6. Copy your **Bot User OAuth Access Token** for your HEYEMOJI_SLACK_API_TOKEN
7. Run heyemoji specifying the above token! 🎉

Deploy HeyEmoji Using BitOps

You've finished all the necessary setup steps. Now it's time to deploy your HeyEmoji Slack app!
 
Replace the ENV values with your own credentials and tokens.
docker run \
-e BITOPS_ENVIRONMENT="test" \
-e AWS_ACCESS_KEY_ID="AWS_ACCESS_KEY_ID" \
-e AWS_SECRET_ACCESS_KEY="AWS_SECRET_ACCESS_KEY" \
-e AWS_DEFAULT_REGION="us-east-2" \
-e TERRAFORM_APPLY="true" \
-e TF_STATE_BUCKET="heyemoji-blog" \
-e HEYEMOJI_SLACK_API_TOKEN="YOUR SLACK API TOKEN" \
-v $(pwd):/opt/bitops_deployment \
bitovi/bitops:latest

 

Verify Deployment

Open Slack and create a new private channel. Next, add your new bot to the channel by @mentioning them in the channel's chat. Once the bot has been added you'll see:
 @HeyEmoji - Blog leaderboards in the chat.
This response will pop up:
Nobody has given any emoji points yet!

This tells you your bot is alive! You can now hand out awards to others in the chat by typing:
Hey @member have a :star:

Cleanup

To delete the resources you've provisioned, add -e TERRAFORM_DESTROY=true \ to the docker run command:
docker run \
-e BITOPS_ENVIRONMENT="test" \
-e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \
-e AWS_DEFAULT_REGION="us-east-2" \
-e TF_STATE_BUCKET="heyemoji-blog" \
-e HEYEMOJI_SLACK_API_TOKEN="YOUR SLACK API TOKEN" \
-e TERRAFORM_DESTROY=true \
-v $(pwd):/opt/bitops_deployment \
bitovi/bitops:latest

Final Words

Great work! You've deployed the HeyEmoji Slack app to your AWS infrastructure using Terraform and Ansible and you orchestrated the build + deploy using BitOps. You learned a few DevOps concepts, such as what an OpsRepo is and what best practices you should consider when building application images.
 
Need another challenge? Try adding data resilience to your HeyEmoji slack app!