Deploying a DBaaS to Azure with Graph Story and Ansible

Josh Holmes

Who is Graph Story?

Graph Story is a startup based out of Memphis, Tennessee that offers fully-managed Neo4j databases as a service. Neo4j is one of the most popular graph database implementations, providing rich features to support machine learning and intelligence through relationships. Rather than users hosting and maintaining a Neo4j database themselves, they can leverage the Graph Story service and avoid the hassle of database management.

Enabling Azure Support for Graph Story

Knomos is a startup that is part of the Microsoft Accelerator winter 2016 class. They wanted to use Graph Story instead of hosting their own Neo4j server. However, they wanted their Graph Story managed database to run within Azure.

When we first worked with Graph Story, their database service had a variety of cloud vendors, but their Azure support was lacking. Graph Story, Knomos and Microsoft got together for one week in Seattle, Washington, to enable Graph Story’s database-as-a-service (DBaaS) on Azure.

Our goal was to make Graph Story’s automated database deployments to Azure consistent with the way they deploy to Digital Ocean, AWS and Google Compute Engine. Graph Story leverages Ansible, an application provisioning and deployment platform. With Ansible, DevOps engineers can provision and deploy apps similarly across different cloud platforms. In addition to working with Graph Story and Knomos, we also contributed to the Ansible project to enable the scenarios required by Graph Story.

Graph Story Deployment Strategy

Custom Image Creation & Deployment

Graph Story has a custom virtual machine image they use for their database nodes. In order for the image to work globally, they need to deploy each image to every Azure region they intend on supporting. The graphic below depicts how this works:

Image graphstoryarch

First a Gearman worker picks up the request for a new image to be created. The Ansible server is triggered via SSH to execute an Ansible playbook which uses 3 modules:

  • Azure-Deploy: Deploys Azure templates defined by YAML within the playbook. In our case, it’s to provision the Target VM.
  • Azure-Image-Capture: A module which captures an Azure VM image and stores the image as a blob.
  • Azure-Blob-Copy: Copies a blob from one storage account to another. For Graph Story’s scenario, it’s called iteratively to copy the captured image to a list of storage accounts in different Azure regions.

Automated Graph Story Database Instances

When a customer purchases a database on GraphStory.com, the Graph Story web server sends a message to a Gearman worker. This triggers another Ansible playbook to provision a Graph Story instance onto Azure:

Image graphstoryarch2

This is a similar architecture to the image deployment process; however, since the image already exists in the region targeted by the user, the deployment is much faster, providing a better customer experience.

Ansible Modules

We also collaborated with Ansible maintainers to publish the modules used by Graph Story for any Ansible user to capture and deploy Azure virtual machines using playbooks.

How to Deploy an Azure Virtual Machine using Ansible

Using the azure-deploy Ansible module, you can launch any sort of Azure Resource Template directly from your playbook YAML. Resource templates allow you to define a desired-state of your cloud infrastructure with a single deployment command. Typically, these templates are in JSON but with this module, they can be written in YAML. Here’s an example of deploying a virtual machine with the necessary storage and virtual network creation:

- name: Capture a VM image and copy it to other storage accounts
  hosts: 127.0.0.1
  connection: local
  remote_user: ""
  tasks:
    - name: Create Azure VM
      local_action:
        module: azure_deploy
        state: present
        subscription_id: ""
        client_id: ""
        tenant_or_domain: ""
        client_secret: "" # don't check in!
        resource_group_name: ''
        template:
          $schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#"
          contentVersion: "1.0.0.0"
          parameters:
            adminUsername:
              type: "string"
              metadata:
                description: "User name for the Virtual Machine."
            dnsNameForPublicIP:
              type: "string"
              metadata:
                description: "Unique DNS Name for the Public IP used to access the Virtual Machine."
            ubuntuOSVersion:
              type: "string"
              defaultValue: "14.04.2-LTS"
              allowedValues:
                - "12.04.5-LTS"
                - "14.04.2-LTS"
                - "15.04"
              metadata:
                description: "The Ubuntu version for the VM. This will pick a fully patched image of this given Ubuntu version. Allowed values: 12.04.5-LTS, 14.04.2-LTS, 15.04."
          variables:
            newStorageAccountName: "[uniqueString(resourceGroup().id)]"
            location: "West US"
            imagePublisher: "Canonical"
            imageOffer: "UbuntuServer"
            OSDiskName: "osdiskforlinuxsimple"
            nicName: "myVMNic"
            addressPrefix: "10.0.0.0/16"
            subnetName: "Subnet"
            subnetPrefix: "10.0.0.0/24"
            storageAccountType: "Standard_LRS"
            publicIPAddressName: "myPublicIP"
            publicIPAddressType: "Dynamic"
            vmStorageAccountContainerName: "vhds"
            vmName: ""
            vmSize: "Standard_D1"
            virtualNetworkName: "MyVNET"
            vnetID: "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]"
            subnetRef: "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]"
            sshKeyPath: "[concat('/home/',parameters('adminUsername'),'/.ssh/authorized_keys')]"
          resources:
            -
              type: "Microsoft.Storage/storageAccounts"
              name: "[uniqueString(resourceGroup().id)]"
              apiVersion: "2015-05-01-preview"
              location: "[variables('location')]"
              properties:
                accountType: "[variables('storageAccountType')]"
            -
              apiVersion: "2015-05-01-preview"
              type: "Microsoft.Network/publicIPAddresses"
              name: "[variables('publicIPAddressName')]"
              location: "[variables('location')]"
              properties:
                publicIPAllocationMethod: "[variables('publicIPAddressType')]"
                dnsSettings:
                  domainNameLabel: "[parameters('dnsNameForPublicIP')]"
            -
              type: "Microsoft.Network/virtualNetworks"
              apiVersion: "2015-05-01-preview"
              name: "[variables('virtualNetworkName')]"
              location: "[variables('location')]"
              properties:
                addressSpace:
                  addressPrefixes:
                    - "[variables('addressPrefix')]"
                subnets:
                  -
                    name: "[variables('subnetName')]"
                    properties:
                      addressPrefix: "[variables('subnetPrefix')]"
            -
              type: "Microsoft.Network/networkInterfaces"
              apiVersion: "2015-05-01-preview"
              name: "[variables('nicName')]"
              location: "[variables('location')]"
              dependsOn:
                - "[concat('Microsoft.Network/publicIPAddresses/', variables('publicIPAddressName'))]"
                - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]"
              properties:
                ipConfigurations:
                  -
                    name: "ipconfig1"
                    properties:
                      privateIPAllocationMethod: "Dynamic"
                      publicIPAddress:
                        id: "[resourceId('Microsoft.Network/publicIPAddresses',variables('publicIPAddressName'))]"
                      subnet:
                        id: "[variables('subnetRef')]"
            -
              type: "Microsoft.Compute/virtualMachines"
              apiVersion: "2015-06-15"
              name: "[variables('vmName')]"
              location: "[variables('location')]"
              dependsOn:
                - "[concat('Microsoft.Storage/storageAccounts/', variables('newStorageAccountName'))]"
                - "[concat('Microsoft.Network/networkInterfaces/', variables('nicName'))]"
              properties:
                hardwareProfile:
                  vmSize: "[variables('vmSize')]"
                osProfile:
                  computername: "[variables('vmName')]"
                  adminUsername: "[parameters('adminUsername')]"
                  adminPassword: "notused"
                  linuxConfiguration:
                    disablePasswordAuthentication: "true"
                    ssh: 
                        publicKeys: 
                          - 
                            path: "[variables('sshKeyPath')]"
                            keyData: ""
                storageProfile:
                  imageReference:
                    publisher: "[variables('imagePublisher')]"
                    offer: "[variables('imageOffer')]"
                    sku: "[parameters('ubuntuOSVersion')]"
                    version: "latest"
                  osDisk:
                    name: "osdisk"
                    vhd:
                      uri: "[concat('http://',variables('newStorageAccountName'),'.blob.core.windows.net/',variables('vmStorageAccountContainerName'),'/',variables('OSDiskName'),'.vhd')]"
                    caching: "ReadWrite"
                    createOption: "FromImage"
                networkProfile:
                  networkInterfaces:
                    -
                      id: "[resourceId('Microsoft.Network/networkInterfaces',variables('nicName'))]"
                diagnosticsProfile:
                  bootDiagnostics:
                    enabled: "true"
                    storageUri: "[concat('http://',variables('newStorageAccountName'),'.blob.core.windows.net')]"
          outputs:
            storage_key:
              type: "object"
              value: "[listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('newStorageAccountName')), '2015-05-01-preview')]"
            storage_name:
              type: "string"
              value: "[uniqueString(resourceGroup().id)]"
        parameters:
          adminUsername:
            value: ''
          dnsNameForPublicIP:
            value: ''
      register: azure

At the end of this module register azure will register the output of the deployment as a variable for use later in the playbook. In the example above, the outputs section of the template is returned, allowing us to extract useful information such as the storage account name and key.

Next, we’ll use the information from the Azure deployment module to add the newly created Azure virtual machine to our Ansible dynamic inventory.

- name: Add new instance to host group
      add_host: hostname= groupname=launched ansible_user= vm_name=
      with_items: azure.instances

azure.instances is a collection of Virtual Machine instances and this Ansible play will be called for each instance (using the with-items property), adding a new host for each iteration. Now you can provision or do anything else to this host as you normally would in Ansible, assuming that your SSH private key for the machine is accessible. We’ll add the new virtual machine to the launched inventory group for later use.

Graph Story runs their existing playbook steps used on all cloud providers after this step.

How to Capture an Azure Virtual Machine Image

Using the azure-image-capture module, capturing a virtual machine image becomes simple. First, you need to be sure to de-provision the user information generated by Azure within the target VM in order for it to be ready to become a ‘generic’ virtual machine. This ensures the image can be reused for creating any VM regardless of the local user settings. You can do this by running sudo waagent -deprovision+user which is a command to the Azure Linux agent:

- name: Run Deprovision user Command
  hosts: launched
  remote_user: ""
  tasks:
    - name: Deprovision User
      command: sudo waagent -deprovision+user -force

Now the target virtual machine is ready to be captured:

- name: Capture Image
  hosts: 127.0.0.1
  connection: local
  tasks:
    - name: Capture
      local_action:
        module: azure_image_capture 
        subscription_id: ""
        resource_group_name: ''
        destination_container: copiedvhds
        client_id: ""
        tenant_id: ""
        client_secret: ""
        vm_name: ""
        wait: ""
      register: capture_info

After this step, the VM image has been captured, and a link to the image defined by capture_info.vhd_uri is returned. You can use this image directly to create a new VM, if it resides in the data center region you require. If you’re like Graph Story, you’ll need to copy the image to various region in order to use the image within regions other than where the generic VM was created.

We wrote the azure-copy-blob module specifically for this purpose:

- name: Copy Blob
    local_action:
      module: azure_copy_blob
      source_uri: ""
      source_key: ""
      destination_account: ""
      destination_container: ""
      destination_key: ""
      destination_blob: ""
    with_items: 

In the snippet above, we can use the output of the azure-image-capture module to copy the image from the source storage account to the destination. We can iterate using the with_items property by providing a collection of storage_accounts like this:

- vars:
    storage_accounts:
      -
        account_name: graphstory4
        key: STORAGE_ACCOUNT_KEY
        container: storedimages
        blob: graphstoryneo4j2.vhd
      -
        account_name: graphstory5
        key: STORAGE_ACCOUNT_KEY
        container: storedimages
        blob: graphstoryneo4j2.vhd
      -
        account_name: graphstory6
        key: STORAGE_ACCOUNT_KEY
        container: storedimages
        blob: graphstoryneo4j2.vhd

Graph Story uses this approach to deploy and update their Graph Story images across Azure. Checkout the Ansible Modules repository, where you can find these modules plus a great variety of others to use in your next automation project.

To get started with Graph Story, checkout their pricing page where you can select a Neo4j database which fits your needs.

0 comments

Discussion is closed.

Feedback usabilla icon