Http wrapper for Terraform CLI Part 1

OOB Hashicorp doesn’t offer an Terraform HTTP API/SDK for provisioning resources to cloud providers using terrafrom templates,(Their proprietary cloud does an awesome job of doing this).

However on the other hand Azure and AWS have an API (SDK) for provisioning resources using ARM and cloud formation templates.

For this POC i went ahead and created a wrapper for the terraform CLI to create an resource group in Azure.

The wrapper is written in Azure C# functions running locally, the second post of this series will cover the same functions running in Azure (hopefully).

The steps that i took are as follows

  1. Installed AZURE CLI and logged in.
  2. Created a Azure Functions Project
  3. Copied the terraform template, and the terraform cli into windows temp folder (C:\Users\<<username>>\AppData\Local\Temp)
  4. And the code is below and self explanatory, used the StartProcess method (and i did something similar Invoke Btstask from .net code – madhukar gilla (wordpress.com) in Dec 2010)
terraform {
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "~>2.0"
    }
  }
}
provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "myterraformgroup" {
    name     = "myResourceGroup"
    location = "eastus"

    tags = {
        environment = "Terraform Demo"
    }
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace TerraformHttpWrapper
{
    public static class Function2
    {
        [FunctionName("Function2")]
        public static async Task<List<string>> RunOrchestrator(
            [OrchestrationTrigger] IDurableOrchestrationContext context)
        {
            var outputs = new List<string>();

            // Replace "hello" with the name of your Durable Activity Function.
            outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Tokyo"));
            //outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "Seattle"));
            //outputs.Add(await context.CallActivityAsync<string>("Function2_Hello", "London"));

            // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
            return outputs;
        }

        [FunctionName("Function2_Hello")]
        public static string SayHello([ActivityTrigger] string name, ILogger log, ExecutionContext context)
        {
            log.LogInformation($"Saying hello to {name}.");

            log.LogInformation("C# HTTP trigger function processed a request.");


            var tempRootDir = Path.GetTempPath();


            Directory.SetCurrentDirectory(tempRootDir);

            log.LogInformation($"----------------------------- Current directory { Directory.GetCurrentDirectory()} --------------------");
            try
            {
                Process p = new Process();
                p.StartInfo.UseShellExecute = false;
                p.StartInfo.FileName = "terraform.exe";
                p.StartInfo.Arguments = "init";
                p.StartInfo.RedirectStandardOutput = true;

                log.LogInformation("*****************************" + Environment.CurrentDirectory);

                p.Start();
                log.LogInformation(p.StandardOutput.ReadToEnd());
                p.WaitForExit();


                log.LogInformation("----------------------------- DONE init --------------------");

                Process ptwo = new Process();
                ptwo.StartInfo.UseShellExecute = false;
                ptwo.StartInfo.FileName = "terraform.exe";
                ptwo.StartInfo.Arguments = "plan  -detailed-exitcode -lock=false";
                ptwo.StartInfo.RedirectStandardOutput = true;

                ptwo.Start();
                log.LogInformation(ptwo.StandardOutput.ReadToEnd());
                ptwo.WaitForExit();

                log.LogInformation("----------------------------- DONE plan --------------------");

                Process pthree = new Process();
                pthree.StartInfo.UseShellExecute = false;
                pthree.StartInfo.FileName = "terraform.exe";
                pthree.StartInfo.Arguments = "apply -auto-approve -lock=false";
                pthree.StartInfo.RedirectStandardOutput = true;

                pthree.Start();
                log.LogInformation(pthree.StandardOutput.ReadToEnd());
                pthree.WaitForExit();

                //log.LogInformation(output);

                log.LogInformation("----------------------------- DONE apply --------------------");

            }
            catch (Exception ed)
            {
                log.LogInformation("Error: ." + ed);
            }

            return name;
        }

        [FunctionName("Function2_HttpStart")]
        public static async Task<HttpResponseMessage> HttpStart(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestMessage req,
            [DurableClient] IDurableOrchestrationClient starter,
            ILogger log)
        {
            // Function input comes from the request content.
            string instanceId = await starter.StartNewAsync("Function2", null);

            log.LogInformation($"Started orchestration with ID = '{instanceId}'.");

            return starter.CreateCheckStatusResponse(req, instanceId);
        }
    }
}

And the logs from the Azure Function and terraform cli are as follows.

Terrafrom and Azure Functions logs

And the created function group