An easy Grafana setup using Azure App Service for Linux

Published on Thursday, July 4, 2019

An easy Grafana setup using Azure App Service for Linux

Grafana is an open source platform for creating dashboards and analyzing time-series data. Grafana is written in Go and provides a feature-rich platform for visualizing any time-series data from sources like Azure Monitor, Azure Application Insights, OpenTSDB, Prometheus, InfluxDB, and many more. It has a wealth of plugins to provide visualization and source enhancements. Grafana can be purchased through Grafana Cloud, or it can be self-hosted.

In this post, we are going to deploy Grafana using the container to Azure App Service for Linux. We are going to use Azure Storage for hosting our var/lib/grafana folder which is the home for our plugins and SQLite database. Finally, we will provide authentication to our Grafana instance using Azure Active Directory. Azure App Service for Linux allows running containers, which explains the first step. Azure App Service also allows mounting of Azure Storage, either blob or file, to an Azure App Service, which you can map to a location on your container. The last thing to do is to wire Grafana up to Azure Active Directory. Fortunately, Grafana provides OAuth support and provides the documenation for the configuration.

I posted a poll on Twitter to see what type of examples to provide for infrastructure as code. An example in PowerShell, Bash, and Terraform was requested, so I have included all three. I have a repository in the BlueGhost Labs organization GitHub called grafana-container-on-azure that has all the code organized by language. Scripts for both creation and cleanup exist. Below is a walkthrough of what all of these scripts are doing if you want to take a deeper dive.

Getting Started

Based on the language you want to use, you will need one of the following installed.

Bash

These scripts require the Azure CLI version >= 2.0.68 and were written using Bash 5, but it should be compatible with older versions.

PowerShell

These scripts require PowerShell Core version >= 6.2.1 and was written using the new Azure Az Module version >= 2.2.0.

Terraform

These scripts require Terraform version 0.12. It uses AzureRM, AzureAD, and Random providers.

Once you have picked the environment and have gotten everything configured, we can get started creating our scripts. If you need any assistance with installation or run into any issues, reach out on GitHub, Twitter, or LinkedIn using the links to the right of this post.

Initial Configuration

We need to set some initial variables. I would urge you to change these to avoid naming conflicts in resources like the App Service.

Bash

#! /bin/bash

# Initial Setup
resource_group_name="grafana-eus-rg"
location="eastus"
storage_account_name="grafanaeusst"
app_plan_name="grafana-eus-ap"
app_service_name="grafana-eus-as"

# Generating a Grafana password
gf_password=$(date +%s | sha256sum | base64 | head -c 12 ;)

# Getting tenant id for use later
tenant_id=$(az account show --query 'tenantId' --output tsv)

PowerShell

#! /bin/env pwsh

# Initial Setup
$resourceGroupName="grafana-eus-rg"
$location="East US"
$storageAccountName="grafanaeusst"
$appPlanName="grafana-eus-ap"
$appServiceName="grafana-eus-as"

# Generating a Grafana and AD App password
$grafanaPassword = -join ((65..90) + (97..122) | Get-Random -Count 12 | % {[char]$_})
$clientSecret = New-Guid

# Getting tenant id for use later
$tenantId = (Get-AzTenant).Id

Terraform

Variables for your variables.tf file.

# variables.tf
variable "app_name" {
  default = "grafana"
}

variable "location" {
  default = "East US"
}

In your main.tf, start with the following:

provider "azurerm" {
  version = "=1.28.0"
}

provider "azuread" {
  version = "~>0.4"
}

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

locals {
  region_codes = {
    "East US" = "eus"
  }

  app_service_name = "${var.app_name}-${lookup(local.region_codes, var.location)}-as"
}

# Create Client Secret
resource "random_uuid" "client_secret" {}

# Create grafana password
resource "random_string" "grafana_password" {
  length = 16
}

Creating our Resource Group

Now let's create our resource group.

Bash

# Create Resource Group
az group create \
    --name $resource_group_name \
    --location $location \
--output none

PowerShell

# Create Resource Group
New-AzResourceGroup -Name $resourceGroupName -Location $location

Terraform

resource "azurerm_resource_group" "main" {
  name     = "${var.app_name}-${lookup(local.region_codes, var.location)}-rg"
  location = "${var.location}"
}

Create the Storage Account and Container

Now let's create the storage account we will use to store the plugins and SQLite database.

Bash

# Create Storage Account
az storage account create \
    --name $storage_account_name \
    --resource-group $resource_group_name \
    --location $location \
    --sku Standard_LRS \
    --output none

# Get Storage Account Key
storage_account_key=$(az storage account keys list \
    --account-name $storage_account_name \
    --resource-group $resource_group_name \
    --query '[0].value' \
    --output tsv)

# Create Grafana Container
az storage container create \
    --name grafana \
--account-name $storage_account_name

PowerShell

# Create Storage Account
$StorageAccountParams = @{
    ResourceGroupName = $resourceGroupName
    Name = $storageAccountName
    Location = $location
    SkuName = "Standard_LRS"
}

New-AzStorageAccount @StorageAccountParams

# Get Storage Account Key
$AccountKeyParams = @{
    ResourceGroupName = $resourceGroupName
    Name = $storageAccountName
}

$storageAccountKey = (Get-AzStorageAccountKey @AccountKeyParams).Value[0]

# Create Grafana Container
$storageContext = New-AzStorageContext -StorageAccountName $storageAccountName -StorageAccountKey $storageAccountKey
New-AzStorageContainer -Name grafana -Context $storageContext

Terraform

resource "azurerm_storage_account" "main" {
  name                     = "${var.app_name}${lookup(local.region_codes, var.location)}st"
  resource_group_name      = "${azurerm_resource_group.main.name}"
  location                 = "${azurerm_resource_group.main.location}"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_container" "main" {
  name                  = "${var.app_name}"
  resource_group_name   = "${azurerm_resource_group.main.name}"
  storage_account_name  = "${azurerm_storage_account.main.name}"
  container_access_type = "private"
}

Create the App Service Plan

Now we can create our App Service Plan, not we have to define it as a Linux plan.

Bash

# Create App Service Plan
az appservice plan create \
    --name $app_plan_name \
    --resource-group $resource_group_name \
    --sku B1 \
    --is-linux \
--output none

PowerShell

# Create App Service Plan
$AppPlanParams = @{
    ResourceGroupName = $resourceGroupName
    ResourceName = $appPlanName
    Location = $location
    ResourceType = "microsoft.web/serverfarms"
    kind = "linux"
    Properties = @{reserved="true"}
    Sku = @{name="B1";tier="Basic"; size="B1"; family="B"; capacity="1"}
}

New-AzResource @AppPlanParams -Force

Terraform

resource "azurerm_app_service_plan" "main" {
  name                = "${var.app_name}-${lookup(local.region_codes, var.location)}-ap"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"
  kind                = "Linux"
  reserved            = true

  sku {
    tier = "Basic"
    size = "B1"
  }
}

Bash and PowerShell: Create the App Service

This step is where the instructions start deviating a little as Terraform can figure out dependencies and it also allows you to configure the App Service with the creation whereas the scripting tools do not.

Bash

# Create App Service
az webapp create \
    --resource-group $resource_group_name \
    --plan $app_plan_name \
    --name $app_service_name \
    --deployment-container-image-name grafana/grafana \
--output none

PowerShell

# Create App Service
$AppServiceParams = @{
    ResourceGroupName = $resourceGroupName
    Name = $appServiceName
    AppServicePlan = $appPlanName
}

$webApp = New-AzWebApp @AppServiceParams

Bash and PowerShell: Mount the Storage Account to the App Service

Again, this differs from the Terraform. This command will mount the Blob storage to the App Service.

Bash

# Set the storage account and mount point
az webapp config storage-account add \
    --resource-group $resource_group_name \
    --name $app_service_name \
    --custom-id GrafanaData \
    --storage-type AzureBlob \
    --share-name grafana \
    --account-name $storage_account_name \
    --access-key $storage_account_key \
    --mount-path /var/lib/grafana/ \
--output none

PowerShell

# Set the storage account and mount point
$StoragePathParams = @{
    Name = "GrafanaData"
    AccountName = $storageAccountName
    Type = "AzureBlob"
    ShareName = "grafana"
    AccessKey = $storageAccountKey
    MountPath = "/var/lib/grafana/"
}

$storagePath = New-AzWebAppAzureStoragePath @StoragePathParams

Register Application with Azure Active Directory

Bash

# Get the hostname
hostname=$(az webapp show \
    --name $app_service_name \
    --resource-group $resource_group_name \
    --query 'defaulthostname' \
    --output tsv)

client_secret=$(uuidgen)

# App Registration
# https://grafana.com/docs/auth/generic-oauth/#set-up-oauth2-with-azure-active-directory
application_id=$(az ad app create \
    --display-name Grafana \
    --reply-urls https://$hostname/login/generic_oauth \
    --password $client_secret \
    --query 'appId' \
    --output tsv)

PowerShell

# App Registration
# https://grafana.com/docs/auth/generic-oauth/#set-up-oauth2-with-azure-active-directory
$SecureClientSecret = ConvertTo-SecureString -String $clientSecret -AsPlainText -Force
$AdAppParams = @{
    DisplayName = "Grafana"
    Password = $SecureClientSecret
    IdentifierUris = "http://Grafana"
    ReplyUrls = "https://$($webApp.DefaultHostName)/login/generic_oauth"
}
$adApp = New-AzADApplication @AdAppParams

Terraform

resource "azuread_application" "main" {
  name            = "Grafana"
  homepage        = "https://${local.app_service_name}.azurewebsites.net"
  identifier_uris = ["https://Grafana"]
  reply_urls      = ["https://${local.app_service_name}.azurewebsites.net/login/generic_oauth"]
}

resource "azuread_application_password" "main" {
  application_id = "${azuread_application.main.id}"
  value          = "${random_uuid.client_secret.result}"
  end_date       = "2020-01-01T01:02:03Z"
}

Terraform: Create the App Service with configuration

Now we have all the pieces. We can build out our App Service in Terraform. This resource doesn't support for mounting the blob storage to the App Service, so we will be using a local-exec provisioner to run the Azure CLI command.

Terraform

resource "azurerm_app_service" "main" {
  name                = "${local.app_service_name}"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"
  app_service_plan_id = "${azurerm_app_service_plan.main.id}"

  site_config {
    linux_fx_version = "DOCKER|grafana/grafana"
  }

  app_settings = {
    "GF_SERVER_ROOT_URL"                          = "https://${local.app_service_name}.azurewebsites.net"
    "GF_SECURITY_ADMIN_PASSWORD"                  = "${random_string.grafana_password.result}"
    "GF_INSTALL_PLUGINS"                          = "grafana-clock-panel,grafana-simple-json-datasource,grafana-azure-monitor-datasource"
    "GF_AUTH_GENERIC_OAUTH_NAME"                  = "Azure AD"
    "GF_AUTH_GENERIC_OAUTH_ENABLED"               = "true"
    "GF_AUTH_GENERIC_OAUTH_CLIENT_ID"             = "${azuread_application.main.id}"
    "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET"         = "${random_uuid.client_secret.result}"
    "GF_AUTH_GENERIC_OAUTH_SCOPES"                = "openid email name"
    "GF_AUTH_GENERIC_OAUTH_AUTH_URL"              = "https://login.microsoftonline.com/$tenantId/oauth2/authorize"
    "GF_AUTH_GENERIC_OAUTH_TOKEN_URL"             = "https://login.microsoftonline.com/$tenantId/oauth2/token"
    "GF_AUTH_GENERIC_OAUTH_API_URL"               = ""
    "GF_AUTH_GENERIC_OAUTH_TEAM_IDS"              = ""
    "GF_AUTH_GENERIC_OAUTH_ALLOWED_ORGANIZATIONS" = ""
  }

  provisioner "local-exec" {
    command = "az webapp config storage-account add --resource-group ${azurerm_resource_group.main.name} --name ${azurerm_app_service.main.name} --custom-id GrafanaData --storage-type AzureBlob --share-name ${var.app_name} --account-name ${azurerm_storage_account.main.name} --access-key ${azurerm_storage_account.main.primary_access_key} --mount-path /var/lib/grafana/"
  }

  depends_on = [
    "azurerm_storage_container.main"
  ]
}

Bash and PowerShell: Configure the App Service

Now we can configure out App Service with Bash and PowerShell.

Bash

# Configuring the settings that will become environment variables
az webapp config appsettings set \
    --resource-group $resource_group_name \
    --name $app_service_name \
    --settings \
    GF_SERVER_ROOT_URL=https://$hostname \
    GF_SECURITY_ADMIN_PASSWORD=$gf_password \
    GF_INSTALL_PLUGINS=grafana-clock-panel,grafana-simple-json-datasource,grafana-azure-monitor-datasource \
    GF_AUTH_GENERIC_OAUTH_NAME="Azure AD" \
    GF_AUTH_GENERIC_OAUTH_ENABLED=true \
    GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true \
    GF_AUTH_GENERIC_OAUTH_CLIENT_ID=$application_id \
    GF_AUTH_GENERIC_OAUTH_client_secret=$client_secret \
    GF_AUTH_GENERIC_OAUTH_SCOPES="openid email name" \
    GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://login.microsoftonline.com/$tenant_id/oauth2/authorize \
    GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://login.microsoftonline.com/$tenant_id/oauth2/token \
    GF_AUTH_GENERIC_OAUTH_API_URL="" \
    GF_AUTH_GENERIC_OAUTH_TEAM_IDS="" \
    GF_AUTH_GENERIC_OAUTH_ALLOWED_ORGANIZATIONS="" \
    --output none

PowerShell

# Configuring the settings that will become environment variables
$settings = @{
    GF_SERVER_ROOT_URL = "https://$($webApp.DefaultHostName)"
    GF_SECURITY_ADMIN_PASSWORD = "$($grafanaPassword)"
    GF_INSTALL_PLUGINS = "grafana-clock-panel,grafana-simple-json-datasource,grafana-azure-monitor-datasource"
    GF_AUTH_GENERIC_OAUTH_NAME = "Azure AD"
    GF_AUTH_GENERIC_OAUTH_ENABLED = "true"
    GF_AUTH_GENERIC_OAUTH_CLIENT_ID = "$($adApp.Id)"
    GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET = "$($clientSecret)"
    GF_AUTH_GENERIC_OAUTH_SCOPES = "openid email name"
    GF_AUTH_GENERIC_OAUTH_AUTH_URL = "https://login.microsoftonline.com/$tenantId/oauth2/authorize"
    GF_AUTH_GENERIC_OAUTH_TOKEN_URL= "https://login.microsoftonline.com/$tenantId/oauth2/token"
    GF_AUTH_GENERIC_OAUTH_API_URL = ""
    GF_AUTH_GENERIC_OAUTH_TEAM_IDS = ""
    GF_AUTH_GENERIC_OAUTH_ALLOWED_ORGANIZATIONS = ""
}

$AppConfig = @{
    ResourceGroup = $resourceGroupName
    Name = $appServiceName
    AppSettings = $settings
    AzureStoragePath = $storagePath
    ContainerImageName = "grafana/grafana"
}

Set-AzWebApp @AppConfig

Setting the outputs for the generated passwords and the address to your Grafana instance

Bash

# Printing out information you will need to know
echo Grafana password is: $gf_password
echo Grafana address is: https://$hostname
echo Client Secret is: $client_secret

PowerShell

# Printing out information you will need to know
Write-Host Grafana password is: $grafanaPassword
Write-Host Grafana address is: https://$($webApp.DefaultHostName)
Write-Host Client Secret is: $clientSecret

Terraform

Place these in a file called output.tf.

output "grafana_password" {
  value = "${random_string.grafana_password.result}"
}

output "grafana_address" {
  value = "https://${azurerm_app_service.main.default_site_hostname}"
}

output "client_secret" {
  value = "${random_uuid.client_secret.result}"
}

Execute the script

Now all that is left is to login to your Azure Account on the command line then execute the script that you created.

Bash

$ chmod +x <script-name>.sh
$ ./<script-name>.sh

PowerShell

$ ./<script-name>.sh

Terraform

$ terraform plan
$ terraform apply

Conclusion

After the method you chose has finished, you should be able to use the URL output from the execution to navigate to a running Grafan instance. You will be able to login as the admin or you can login using an Azure Active Directory account. The Azure AD login will have read-only permissions by default so you will need to log into your Grafan instance as admin to change that. If you navigate to your created Azure Storage container, you will see an SQLite database, plugins folders, and a few other files. A cool thing about this approach is the versioning built into Azure Storage, so if you make a mistake, you can revert your SQLite database. As far as performance is concerned, I think the site is fast and the database is responding quickly, which is impressive, in my opinion, to be running from blob storage. The last point that I find interesting is that all three of these solutions have an almost identical line count.

Let me know if you have any questions, and I will try to assist you as I can.

Thanks for reading,

Jamie

If you enjoy the content then consider buying me a coffee.