Azure is a fairly new cloud environment for me. Getting an API up and running on container instances posed a bit of a challenge. So here I am sharing my approach to automate and deploy container groups in Azure.
Things to do
- Build a Rails API
- Containerise API
- Push API container to Azure container repository from Github
- Deploy API container to Azure container group using ARM templates
- Secrets should be stored in Azure key vault
Build a Rails API
There are plenty of tutorials out there that clearly outlay how to build a Rails API. It is as simple as running the following in your terminal.
rails new my_api --api
Containerise API
Here is a sample Dockerfile for a Rails API.
# set OS
FROM ubuntu:bionic
# Set Ruby
FROM ruby:2.7.2
RUN gem install bundler -v 2.0
# create api directory
RUN mkdir /my_api
WORKDIR /my_api
# copy gemfile
COPY Gemfile /my_api/Gemfile
COPY Gemfile.lock /my_api/Gemfile.lock
# install gems
RUN bundle install
# copy app files
COPY . /my_api
EXPOSE 80
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb", "-p", "80"]
Push API container to Azure container repository
This step is automated via Github workflows. Azure container registry is a prerequisite for this step.
- Allow Github to access Azure container registry
Create a service principal for Azure authentication via Azure cli. A handy tutorial is available here
- Set secrets in Github repository
AZURE_CREDENTIALS – The entire JSON response when you created RBAC in the previous step.
REGISTRY_LOGIN_SERVER – Name of your registry.
REGISTRY_USERNAME – The client ID you can find from the credentials JSON.
REGISTRY_PASSWORD – The client secret from the credentials JSON.
The following workflow yaml pushes API container to a repository named my_api to Azure registry tagged latest commit sha.
name: acr_registry_build
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
# checkout the repo
- name: 'Checkout GitHub Action'
uses: actions/checkout@main
- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: 'Build and push image'
uses: azure/docker-login@v1
with:
login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- run: |
docker build -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/my_api:${{ github.sha }} -f Dockerfile .
docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/my_api:${{ github.sha }}
Deploy API container to Azure container group
Once the container is ready in container registry, it can now be deployed. Deployment automation is handled as ARM templates. Any secrets the API requires in its environment are stored in Azure key vault.
Github workflow yaml for deployment. Two additional secrets Azure Subscription ID and Resource Group need to be added to Github. Provide ARM template and parameters file along with additional inline parameters to arm-deploy action.
name: api_deployment
on: workflow_dispatch
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# checkout the repo
- name: 'Checkout GitHub Action'
uses: actions/checkout@main
- name: 'Login via Azure CLI'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: 'Deploy to Azure'
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
resourceGroupName: ${{ secrets.RESOURCE_GROUP }}
template: my-api-deployment.json
parameters: parameters.json image=${{ secrets.REGISTRY_LOGIN_SERVER }}/my_api:${{ github.sha }} registryLoginServer=${{ secrets.REGISTRY_LOGIN_SERVER }}
ARM template to deploy API container to container groups. (my-api-deployment.json referenced in Github workflow yaml )
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"vnetName": {
"type": "string",
"defaultValue": "MyApiVNet",
"metadata": {
"description": "VNet name"
}
},
"subnetName": {
"type": "string",
"defaultValue": "MyApiSubnet",
"metadata": {
"description": "Subnet name"
}
},
"location": {
"type": "string",
"defaultValue": "australiaeast",
"metadata": {
"description": "Location for all resources."
}
},
"containerGroupName": {
"type": "string",
"defaultValue": "my-api-containergroup",
"metadata": {
"description": "Container group name"
}
},
"containerName": {
"type": "string",
"defaultValue": "my-api-container",
"metadata": {
"description": "Container name"
}
},
"image": {
"type": "string",
"metadata": {
"description": "Container image to deploy. Should be of the form accountName/imagename:tag for images stored in Docker Hub or a fully qualified URI for a private registry like the Azure Container Registry."
},
"defaultValue": ""
},
"port": {
"type": "string",
"metadata": {
"description": "Port to open on the container."
},
"defaultValue": "80"
},
"cpuCores": {
"type": "string",
"metadata": {
"description": "The number of CPU cores to allocate to the container. Must be an integer."
},
"defaultValue": "1.0"
},
"memoryInGb": {
"type": "string",
"metadata": {
"description": "The amount of memory to allocate to the container in gigabytes."
},
"defaultValue": "1.5"
},
"registryLoginServer": {
"type": "string"
},
"registryUsername": {
"type": "string"
},
"registryPassword": {
"type": "string"
},
"clientApiAccessKey": {
"type": "securestring"
},
"railsMasterKey": {
"type": "securestring"
}
},
"variables": {
"networkProfileName": "my-api-networkProfile",
"interfaceConfigName": "eth0",
"interfaceIpConfig": "ipconfigprofile1"
},
"resources": [
{
"name": "[variables('networkProfileName')]",
"type": "Microsoft.Network/networkProfiles",
"apiVersion": "2020-05-01",
"location": "[parameters('location')]",
"properties": {
"containerNetworkInterfaceConfigurations": [
{
"name": "[variables('interfaceConfigName')]",
"properties": {
"ipConfigurations": [
{
"name": "[variables('interfaceIpConfig')]",
"properties": {
"subnet": {
"id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('subnetName'))]"
}
}
}
]
}
}
]
}
},
{
"name": "[parameters('containerGroupName')]",
"type": "Microsoft.ContainerInstance/containerGroups",
"apiVersion": "2019-12-01",
"location": "[parameters('location')]",
"dependsOn": [
"[resourceId('Microsoft.Network/networkProfiles', variables('networkProfileName'))]"
],
"properties": {
"containers": [
{
"name": "[parameters('containerName')]",
"properties": {
"image": "[parameters('image')]",
"ports": [
{
"port": "[parameters('port')]",
"protocol": "Tcp"
}
],
"environmentVariables": [
{
"name": "CLIENT_API_ACCESS_KEY",
"secureValue": "[parameters('clientApiAccessKey')]"
},
{
"name": "RAILS_ENV",
"value": "production"
},
{
"name": "RAILS_MASTER_KEY",
"secureValue": "[parameters('railsMasterKey')]"
}
],
"resources": {
"requests": {
"cpu": "[parameters('cpuCores')]",
"memoryInGB": "[parameters('memoryInGb')]"
}
}
}
}
],
"imageRegistryCredentials": [
{
"server": "[parameters('registryLoginServer')]" ,
"username": "[parameters('registryUsername')]",
"password": "[parameters('registryPassword')]"
}
],
"osType": "Linux",
"networkProfile": {
"id": "[resourceId('Microsoft.Network/networkProfiles', variables('networkProfileName'))]"
},
"restartPolicy": "Always"
}
}
],
"outputs": {
"containerIPv4Address": {
"type": "string",
"value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups/', parameters('containerGroupName'))).ipAddress.ip]"
}
}
}
Parameters file defining key vault access. ( parameters.json referenced in Github workflow yaml)
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"clientApiAccessKey": {
"reference": {
"keyVault": {
"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyApiAUEastRG/providers/Microsoft.KeyVault/vaults/MyApiKeyVault"
},
"secretName": "clientApiAccessKey"
}
},
"registryUsername": {
"reference": {
"keyVault": {
"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyApiAUEastRG/providers/Microsoft.KeyVault/vaults/MyApiKeyVault"
},
"secretName": "registryUsername"
}
},
"registryPassword": {
"reference": {
"keyVault": {
"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyApiAUEastRG/providers/Microsoft.KeyVault/vaults/MyApiKeyVault"
},
"secretName": "registryPassword"
}
},
"railsMasterKey": {
"reference": {
"keyVault": {
"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/MyApiAUEastRG/providers/Microsoft.KeyVault/vaults/MyApiKeyVault"
},
"secretName": "railsMasterKey"
}
}
}
}
Azure container group can be wired to Azure app gateway as a backend pool. SSL and other security enforcements can be done at app gateway end.
References
https://docs.microsoft.com/en-us/azure/container-instances/container-instances-github-action