Deploying a DBaaS to Azure with Graph Story and Ansible
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:
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:
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.
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: "184.108.40.206" 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.