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
- Setting Up Your Operations Repo
- What is an Operation Repo?
- Configure Terraform
- Configure Ansible
- Create a Slack Bot and Add to Slack Workspace
- Deploy HeyEmoji Using BitOps
- Verify and Cleanup
- Final Words
Required Tools
- git
- docker
- aws cli - Install V1, some features we will be using aren't supported by the v2 api
- slack
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
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
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.
Clean Up Generated Files
- test/ansible/bitops.after-deploy.d
- test/ansible/bitops.before-deploy.d
- test/ansible/inventory.yml.
Ansible Playbook
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
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
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
Deploy HeyEmoji Using BitOps
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
@HeyEmoji - Blog leaderboards
in the chat.Nobody has given any emoji points yet!
Hey @member have a :star:
Cleanup
-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