Smart Bot 2024

This year, I plan to go for my AWS SysOps and AWS DevOps Certificates. (This would complete the software development engineer and system administrator path in AWS.) Both exams have hands-on questions in AWS on setting up small projects. So, I want to start getting some hands-on experience since I am also our Slack and Github admin at work. I thought of creating a GitHub Bot in Slack that runs on AWS.

What I would like to start with is a notification in Slack when there is growth in our Github billing. 

In Slack 

First we need to create a new slack App

Permissions

In slack we need to give the app a few permissions

  • Commands — this lets the app be a / command in Slack
  •  Chat:write — this lets the app send messages to channels
  •  Channels:history — this enables the bot to view messages and channel info for the channel the bot has been invited to
  •  channels:read — this enables the bot to read messages in the channel it is part of
  •  team:read — this lets the bot view the name, email, and icon for the workspace it is part of

In AWS

We’ll want to create a new Lambda function, and under the trigger, we want to add an API gateway. The default options for are fine for this gateway. Once it is created, you’ll have an API endpoint URL

Back in Slack

Under the command area, we want to enable commands.
The command is what you want your Slack command to be called in Slack. Since there is already a /github i’m going to call mine /ngithub
The request URL is the API gateway URL

Now install the app; when anyone in the workspace calls/ngithub, this will fire off an event to the AWS gateway, which will trigger the lambda function.

Back in AWS

Slack is expecting a response within 3 seconds, or it will re-issue the command and do this up to 3 times. So we need to respond to slack quickly. This lambda will do two things. 

  1. Call the billing lambda that will do all the work
  2. Respond to slack

The code for this is straightforward. In Slack the user will issue /ngithub billing. This will cause Slack to pass an event to the API Gateway, which will pass that event to our Lambda. We will decode that event if the text of the event is billing (anything after the command) we will get the channel id (so we respond back to that channel) and fire off another Lambda. We then will return back to Slack that we got the message with an OK

import boto3
import json
import base64

def lambda_handler(event, context):
    try:
        slack_body = parse_slack_payload(event["body"])
    except:
        return {
            'statusCode': 200,
            'body': "UF8 Error decoding event"
        }
    print(f"Received event:\n{slack_body}\nWith context:\n{context}")
    if slack_body.get("text") == "billing":
        lambda_client = boto3.client('lambda')
        lambda_payload = {'channel': slack_body.get("channel_id")}
        lambda_client.invoke(FunctionName='Github_Billing_slow',
                             InvocationType='Event',
                             Payload=json.dumps(lambda_payload))
    return {
        'statusCode': 200,
        'body': "OK"
    }


def parse_slack_payload(data):
    # decode the data using the b64decode function
    decoded_data_raw = base64.b64decode(data).decode('utf-8').split('&')

    decoded_data_formatted = {}
    for item in decoded_data_raw:
        data_object = item.split('=')
        decoded_data_formatted[data_object[0]] = data_object[1]

    return decoded_data_formatted

Second Lambda

We now need a second Lambda called Github Billing Slow (the name we gave the Lambda above).

What I want this Lambda to do is return our current Github cost. To do this we need to call the GitHub rest API. And because there isn’t a single API that get the data we need we need to call the 

  1. Action billing API
  2. storage billing API
  3. Copilot billing API 

Also since there is no API to get the cost for the 3 items, we’ll have to hardcode these values located in their billing docs. Because we will need tokens to access Slack and Github, we’ll need to create environment variables for both of these. This can be done in Configuration -> environment variables. We also in the general setting, need to increase the timeout from a few seconds to a few minutes as this operation takes some time.

The way this code work, it will iterate over each of our ORG, gathering the cost for actions and storage and adding them together. After that is done it will find the number of copilot users and then return that information to the channel the command was issued.

import json
import requests
import os
import ntap_github

COST = {
    'UBUNTU': 0.008,
    'MACOS': 0.08,
    'WINDOWS': 0.016,
    'ubuntu_4_core': 0.016,
    'ubuntu_8_core': 0.032,
    'ubuntu_16_core': 0.064,
    'ubuntu_32_core': 0.128,
    'ubuntu_64_core': 0.256,
    'windows_4_core': 0.032,
    'windows_8_core': 0.064,
    'windows_16_core': 0.128,
    'windows_32_core': 0.256,
    'windows_64_core': 0.512
}

def post_message_to_slack(text, channel_id, blocks = None):
    return requests.post('https://slack.com/api/chat.postMessage', {
        'token': os.environ["BOT_TOKEN"],
        'channel': channel_id,
        'text': text,
        'blocks': json.dumps(blocks) if blocks else None
    }).json()

def get_billing(channel_id):
    github = ntap_github.NetAppGitHub()
    post_message_to_slack("Actions cost is the current cost since the start of the month", channel_id)
    post_message_to_slack("Storage cost is github estimate for the entire month", channel_id)
    post_message_to_slack("Github Copilot cost is cost for the entire month", channel_id)
    post_message_to_slack("Calculating cost will take a minute......", channel_id)
    storage_cost = 0.0
    actions_cost = 0.0
    total_cost = 0.0
    for org in github.orgs:
        actions, storage = get_billing_data(github, org)
        storage_cost += storage
        actions_cost += actions
        total_cost += actions + storage
    results = github.query("orgs/<org Name>/copilot/billing", os.environ["GITHUB_TOKEN"])
    print(results)
    copilot_cost = results['seat_breakdown']['total'] * 19.0
    total_cost += copilot_cost
    post_message_to_slack("Total Cost: $%s" % total_cost, channel_id)
    post_message_to_slack("Total Actions Cost: $%s" % actions_cost, channel_id)
    post_message_to_slack("Total Storage Cost: $%s" % storage_cost, channel_id)
    post_message_to_slack("Total Copilot Cost: $%s" % copilot_cost, channel_id)


def get_billing_data(github, org):
    actions = github.query(('orgs/%s/settings/billing/actions' % org), os.environ["GITHUB_TOKEN"])
    org_action_cost = action_cost(actions)
    storage = github.query(('orgs/%s/settings/billing/shared-storage' % org), os.environ["GITHUB_TOKEN"])
    org_action_storage = storage_cost(storage)
    return org_action_cost, org_action_storage

def action_cost(actions):
    total_cost = 0
    for each in actions['minutes_used_breakdown']:
        if each in COST:
            category_cost = float(actions['minutes_used_breakdown'][each]) * COST[each]
            total_cost += category_cost
    return total_cost

def storage_cost(storage):
    # It cost $0.008 pre gig per day. Assume there 30 day a month
    total_cost = float(storage['estimated_paid_storage_for_month'] * .008 * 30)
    return total_cost


def lambda_handler(event, context):
    print(f"Received event:\n{event}\nWith context:\n{context}")

    channel_id = event.get("channel")
    get_billing(channel_id)


Back in Slack

We have a working slack bot in AWS. When we call /ngithub billing we get the total cost for each of our org.

Leave a comment

Blog at WordPress.com.

Up ↑