Discord Bot Part 5 - Infrastructure as Code
Now that the template of the bot is pretty stable and is modular in nature the next step is to develop a Continuous Integration / Continuous Deployment pipeline. To achieve this we are going to use GitHub Actions and Azure Resource Manager Templates to package our infrastructure, configuration and code and deploy it to Azure. If you don’t want to read through the process I used, you can go straight to the new Bot Template on GitHub
Adding the components
The four components we are going to deploy are a storage account, an Event Grid domain, a Linux based Function Application and a Linux based Web Application. I started off with the Azure Quickstart Templates for a basic Linux Web applications. Most of these are pretty stock standard and I didn’t really change all that much apart from removing the storage account so that I could share it between the Functions and the WebApp. If you only want the individual ARM templates that I used, I put them up in there own GitHub Repository. Otherwise the templates are just in the base folder. The other item I need to create was the work flow yaml file for GitHub Actions. I used a example Azure workflow initially to get me started.
Planning the Workflow
The bot needs a number of resources and configuration applied before it can actually work. The easiest way I found to plan this out was to try and manually deploy the Bot into Azure and get it up and running. This processes basically consisted of:
1) Deploying the Infrastructure 2) Deploy the Code 3) Configuring the components and initializing the event grid listeners
So for the work flow I ended up approaching it the same way.
Configuring GitHub
Firstly I needed to allow GitHub access to my Azure tenant so that it can deploy infrastructure. To do this I created a resource group in my Azure tenant and ran the following command in the Azure CLI:
az ad sp create-for-rbac --name "github-deployer" --sdk-auth --role contributor --scopes /subscriptions/<YOUR_SUBSCRIPTION_ID>/resourcegroups/<YOUR_RESOURCE_GROUP>
This created a service principle account that had access to create resources in my resource group. When you run this command a JSON file is returned with the account details and secret. To get the Azure GitHub Actions to work you need to create a secret in your GitHub project called AZURE_CREDENTIALS
and paste this JSON object in it. This allows you to log into Azure using this service principle that only has access to a singular resource group.
After going through the various ARM templates and actions I would need to use, I also created a secret to hold my SUBSCRIPTION_ID
and a secret to hold my DISCORD_BOT_TOKEN
. These three secrets also mean that when someone else deploys the bot they just need to add these secrets and they should be good to go.
Deploying ARM Templates
The first thing to do was to deploy all the ARM templates to get the resources created within Azure. Because ARM templates are both declarative and non-destructive, they can be deployed over the top of existing resources without affecting them. In the workflow.yml
file, I created a new job called deploy-azure-infrastructure
. In this job the first two steps, as shown below, check out the source and login to Azure using the service principle secret:
- name: 'Check Out Main'
uses: actions/checkout@main
- name: Login via Azure Module
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
enable-AzPSSession: true
The rest of this GitHub work flow is simply repeating the ARM deployment step for all my ARM templates. While this sounds easy, I found troubleshooting both ARM templates and GitHub actions very annoying. I ended up deploying the ARM templates on after the other in the Azure portal to try and get it to work properly. To deploy and ARM template the boiler plate configuration looks something like this:
- name: 'Deploy ARM Template'
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ secrets.SUBSCRIPTION_ID }}
resourceGroupName: ${{ env.AZURE_RG }}
template: ./<template>.json
parameters:
<param>: <value>
To save myself some issues, I created an environment variable back at the top of the work flow for the Azure Resource Group name to use. I also did this for the bot naming convention, location and web SKU. Finally, at the end of this job I also set a number of environment variables up on the WebApp and Function App so that when I deployed my code I wouldn’t need to add them are restart later. To do this I used a Azure CLI task as follows:
- name: 'Add Environment Variables'
uses: azure/CLI@v1
env:
EVENTGRID_NAME: ${{ env.DNETBOT_NAME }}-eventgrid-domain
FUNCTION_NAME: ${{ env.DNETBOT_NAME }}-function
WEBAPP_NAME: ${{ env.DNETBOT_NAME }}-proxy
with:
azcliversion: 2.14.2
inlineScript: |
echo ::add-mask::$EVENTGRID_ENDPOINT
echo ::add-mask::$EVENTGRID_KEY
EVENTGRID_KEY=$(az eventgrid domain key list --name ${{ env.EVENTGRID_NAME }} --resource-group ${{ env.AZURE_RG }} --query key1 -o tsv)
EVENTGRID_ENDPOINT=$(az eventgrid domain show --name ${{ env.EVENTGRID_NAME }} --resource-group ${{ env.AZURE_RG }} --query endpoint -o tsv)
az webapp config appsettings set -g ${{ env.AZURE_RG }} -n ${{ env.WEBAPP_NAME }} --settings BOT_EventGridDomainEndPoint=$EVENTGRID_ENDPOINT --output none
az webapp config appsettings set -g ${{ env.AZURE_RG }} -n ${{ env.WEBAPP_NAME }} --settings BOT_EventGridDomainAccessKey=$EVENTGRID_KEY --output none
az webapp config appsettings set -g ${{ env.AZURE_RG }} -n ${{ env.WEBAPP_NAME }} --settings BOT_DISCORD_BOT_TOKEN=${{ secrets.DISCORD_BOT_TOKEN }} --output none
az functionapp config appsettings set --resource-group ${{ env.AZURE_RG }} --name ${{ env.FUNCTION_NAME }} --settings EventGridDomain=$EVENTGRID_ENDPOINT --output none
az functionapp config appsettings set --resource-group ${{ env.AZURE_RG }} --name ${{ env.FUNCTION_NAME }} --settings EventGridKey=$EVENTGRID_KEY --output none
Essentially this step just runs the various Azure CLI commands to get the Event Grid details we created and set them on the WebApp and Function App, as well as set the Discord Bot Token on the WebApp as well.
Deploying Code and Connecting Endpoints
The code deploy actions are pretty straight forward and I used the standard WebApp deploy example. Essentially this job checks out the source, configures dotnet core and builds both solutions. The WebApp deployment uses the standard azure/webapps-deploy@v2
module to deploy the WebApp component, but the Functions deployment I ended up having to use the Azure CLI to deploy it with the command az functionapp deployment source config-zip -g ${{ env.AZURE_RG }} -n ${{ env.DNETBOT_NAME }}-function --src dnet-func-release.zip
.
Finally, the last step in this process is to create the topics and connect the endpoints, using the Azure CLI. We need to do this task at the end because the EventGrid won’t connect a web hook unless it is active and responding.
Added Documentation
In addition to these changes, because of the modular nature of the framework, most of these elements can be copied and modified to add additionally functionality. For example, the documentation for adding an event handler can be reused to respond to events in a variety of different ways.
Next Steps
Next up for the bot framework is to expand out the different event types and looking to add some basic functionality found in most Discord Bots.