Skip to content

Commit

Permalink
feat: add Azure FunctionApp Module (#591)
Browse files Browse the repository at this point in the history
* Azure FunctionApp Module

* Update Azure fucntions example

* add appservice module and test

* update azurefunction test

* fix appservice_tests

* fix TestGetAppServiceClientE test

* fix typo

Co-authored-by: Hadwa Abdelhalem <[email protected]>
Co-authored-by: Hadwa Gaber <[email protected]>
  • Loading branch information
3 people authored Jul 16, 2021
1 parent 46fba20 commit 3d07a97
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 0 deletions.
33 changes: 33 additions & 0 deletions examples/azure/terraform-azure-functionapp-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Terraform Azure Function App Example

This folder contains a Terraform module that deploys a Function App in [Azure](https://azure.microsoft.com/) to demonstrate how you can use Terratest to write automated tests for your Azure Terraform code.

This module deploys [Azure Storage](https://azure.microsoft.com/en-us/services/storage/), [Azure Function App](https://azure.microsoft.com/en-us/services/functions/), [Azure Function App](https://azure.microsoft.com/en-us/services/functions/).

Check out [test/azure/terraform_azure_functionapp_example_test.go](/test/azure/terraform_azure_functionapp_example_test.go) to see how you can write automated tests for this module and validate the configuration of the parameters and options.

**WARNING**: This module and the automated tests for it deploy real resources into your Azure account which can cost you
money. The resources are all part of the [Azure Free Account](https://azure.microsoft.com/free/), so if you haven't used that up,
it should be free, but you are completely responsible for all Azure charges.

## Running this module manually

1. Sign up for [Azure](https://azure.microsoft.com/)
1. Configure your Azure credentials using one of the [supported methods for Azure CLI
tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)
1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`
1. Ensure [environment variables](../README.md#review-environment-variables) are available
1. Run `terraform init`
1. Run `terraform apply`
1. When you're done, run `terraform destroy`

## Running automated tests against this module

1. Sign up for [Azure](https://azure.microsoft.com/)
1. Configure your Azure credentials using one of the [supported methods for Azure CLI
tools](https://docs.microsoft.com/cli/azure/azure-cli-configuration?view=azure-cli-latest)
1. Install [Terraform](https://www.terraform.io/) and make sure it's on your `PATH`
1. Configure your Terratest [Go test environment](../README.md)
1. `cd test/azure`
1. `go build terraform_azure_functionapp_example_test.go`
1. `go test -v -run TestTerraformAzureFunctionAppExample`
81 changes: 81 additions & 0 deletions examples/azure/terraform-azure-functionapp-example/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ---------------------------------------------------------------------------------------------------------------------
# Deploy an Azure storage account, service plan, function app, and application insights
# This is an example of how to deploy an Azure function app.
# See test/terraform_azure_functionapp_example_test.go for how to write automated tests for this code.
# ---------------------------------------------------------------------------------------------------------------------


# ---------------------------------------------------------------------------------------------------------------------
# CONFIGURE OUR AZURE CONNECTION
# ---------------------------------------------------------------------------------------------------------------------
provider "azurerm" {
version = "~>2.29.0"
features {}
}

# ---------------------------------------------------------------------------------------------------------------------
# DEPLOY THE RESOURCE GROUP
# ---------------------------------------------------------------------------------------------------------------------

resource "azurerm_resource_group" "app_rg" {
name = "terratest-functionapp-rg-${var.postfix}"
location = var.location
}

# ---------------------------------------------------------------------------------------------------------------------
# DEPLOY THE AZURE STORAGE ACCOUNT
# ---------------------------------------------------------------------------------------------------------------------

resource "azurerm_storage_account" "storage" {
name = "storageaccount${var.postfix}"
resource_group_name = azurerm_resource_group.app_rg.name
location = azurerm_resource_group.app_rg.location
account_tier = "Standard"
account_replication_type = "LRS"
}

# ---------------------------------------------------------------------------------------------------------------------
# DEPLOY THE APP SERVICE PLAN
# ---------------------------------------------------------------------------------------------------------------------

resource "azurerm_app_service_plan" "app_service_plan" {
name = "appservice-plan-${var.postfix}"
location = azurerm_resource_group.app_rg.location
resource_group_name = azurerm_resource_group.app_rg.name
kind = "FunctionApp"

sku {
tier = "Standard"
size = "S1"
}
}

# ---------------------------------------------------------------------------------------------------------------------
# DEPLOY THE APPLICATION INSIGHTS
# ---------------------------------------------------------------------------------------------------------------------

resource "azurerm_application_insights" "application_insights" {
name = "appinsights-${var.postfix}"
location = azurerm_resource_group.app_rg.location
resource_group_name = azurerm_resource_group.app_rg.name
application_type = "web"
}

# ---------------------------------------------------------------------------------------------------------------------
# DEPLOY THE AZURE FUNCTION APP
# ---------------------------------------------------------------------------------------------------------------------

resource "azurerm_function_app" "function_app" {
name = "functionapp-${var.postfix}"
location = azurerm_resource_group.app_rg.location
resource_group_name = azurerm_resource_group.app_rg.name
app_service_plan_id = azurerm_app_service_plan.app_service_plan.id
storage_account_name = azurerm_storage_account.storage.name
storage_account_access_key = azurerm_storage_account.storage.primary_access_key


app_settings = {
"APPINSIGHTS_INSTRUMENTATIONKEY" = azurerm_application_insights.application_insights.instrumentation_key
"APPLICATIONINSIGHTS_CONNECTION_STRING" = "InstrumentationKey=${azurerm_application_insights.application_insights.instrumentation_key}"
}
}
19 changes: 19 additions & 0 deletions examples/azure/terraform-azure-functionapp-example/output.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "resource_group_name" {
value = azurerm_resource_group.app_rg.name
}

output "function_app_id" {
value = azurerm_function_app.function_app.id
}

output "default_hostname" {
value = azurerm_function_app.function_app.default_hostname
}

output "function_app_kind" {
value = azurerm_function_app.function_app.kind
}

output "function_app_name" {
value = azurerm_function_app.function_app.name
}
31 changes: 31 additions & 0 deletions examples/azure/terraform-azure-functionapp-example/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ---------------------------------------------------------------------------------------------------------------------
# ENVIRONMENT VARIABLES
# Define these secrets as environment variables
# ---------------------------------------------------------------------------------------------------------------------

# ARM_CLIENT_ID
# ARM_CLIENT_SECRET
# ARM_SUBSCRIPTION_ID
# ARM_TENANT_ID

# ---------------------------------------------------------------------------------------------------------------------
# REQUIRED PARAMETERS
# You must provide a value for each of these parameters.
# ---------------------------------------------------------------------------------------------------------------------

# ---------------------------------------------------------------------------------------------------------------------
# OPTIONAL PARAMETERS
# These parameters have reasonable defaults.
# ---------------------------------------------------------------------------------------------------------------------

variable "location" {
description = "The supported azure location where the resource exists"
type = string
default = "West US2"
}

variable "postfix" {
description = "A postfix string to centrally mitigate resource name collisions."
type = string
default = "1276"
}
77 changes: 77 additions & 0 deletions modules/azure/appService.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package azure

import (
"context"
"testing"

"github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web"
"github.com/stretchr/testify/require"
)

// AppExists indicates whether the specified application exists.
// This function would fail the test if there is an error.
func AppExists(t *testing.T, appName string, resourceGroupName string, subscriptionID string) bool {
exists, err := AppExistsE(appName, resourceGroupName, subscriptionID)
require.NoError(t, err)

return exists
}

// AppExistsE indicates whether the specified application exists.
func AppExistsE(appName string, resourceGroupName string, subscriptionID string) (bool, error) {
_, err := GetAppServiceE(appName, resourceGroupName, subscriptionID)
if err != nil {
if ResourceNotFoundErrorExists(err) {
return false, nil
}
return false, err
}
return true, nil
}

// GetAppService gets the App service object
// This function would fail the test if there is an error.
func GetAppService(t *testing.T, appName string, resGroupName string, subscriptionID string) *web.Site {
site, err := GetAppServiceE(appName, resGroupName, subscriptionID)
require.NoError(t, err)

return site
}

// GetAppServiceE gets the App service object
func GetAppServiceE(appName string, resGroupName string, subscriptionID string) (*web.Site, error) {
rgName, err := getTargetAzureResourceGroupName(resGroupName)
if err != nil {
return nil, err
}

client, err := GetAppServiceClientE(subscriptionID)
if err != nil {
return nil, err
}

resource, err := client.Get(context.Background(), rgName, appName)
if err != nil {
return nil, err
}

return &resource, nil
}

func GetAppServiceClientE(subscriptionID string) (*web.AppsClient, error) {
// Create an Apps client
appsClient, err := CreateAppServiceClientE(subscriptionID)
if err != nil {
return nil, err
}

// Create an authorizer
authorizer, err := NewAuthorizer()
if err != nil {
return nil, err
}

// Attach authorizer to the client
appsClient.Authorizer = *authorizer
return appsClient, nil
}
48 changes: 48 additions & 0 deletions modules/azure/appService_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// +build azure

// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for
// CircleCI.

package azure

import (
"testing"

"github.com/stretchr/testify/require"
)

/*
The below tests are currently stubbed out, with the expectation that they will throw errors.
If/when CRUD methods are introduced for Azure MySQL server and database, these tests can be extended
*/

func TestAppExistsE(t *testing.T) {
t.Parallel()

resGroupName := ""
appName := ""
subscriptionID := ""

_, err := AppExistsE(appName, resGroupName, subscriptionID)
require.Error(t, err)
}

func TestGetAppServiceE(t *testing.T) {
t.Parallel()

resGroupName := ""
appName := ""
subscriptionID := ""

_, err := GetAppServiceE(appName, resGroupName, subscriptionID)
require.Error(t, err)
}

func TestGetAppServiceClientE(t *testing.T) {
t.Parallel()

subscriptionID := ""

_, err := GetAppServiceClientE(subscriptionID)
require.NoError(t, err)
}
22 changes: 22 additions & 0 deletions modules/azure/client_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-06-01/subscriptions"
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
"github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web"
autorestAzure "github.com/Azure/go-autorest/autorest/azure"
)

Expand Down Expand Up @@ -642,6 +643,27 @@ func CreateNewVirtualNetworkClientE(subscriptionID string) (*network.VirtualNetw
return &vnetClient, nil
}

// CreateAppServiceClientE returns an App service client instance configured with the
// correct BaseURI depending on the Azure environment that is currently setup (or "Public", if none is setup).
func CreateAppServiceClientE(subscriptionID string) (*web.AppsClient, error) {

// Validate Azure subscription ID
subscriptionID, err := getTargetAzureSubscription(subscriptionID)
if err != nil {
return nil, err
}

// Lookup environment URI
baseURI, err := getEnvironmentEndpointE(ResourceManagerEndpointName)
if err != nil {
return nil, err
}

// create client
appsClient := web.NewAppsClientWithBaseURI(baseURI, subscriptionID)
return &appsClient, nil
}

// GetKeyVaultURISuffixE returns the proper KeyVault URI suffix for the configured Azure environment.
// This function would fail the test if there is an error.
func GetKeyVaultURISuffixE() (string, error) {
Expand Down
55 changes: 55 additions & 0 deletions test/azure/terraform_azure_functionapp_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// +build azure

// NOTE: We use build tags to differentiate azure testing because we currently do not have azure access setup for
// CircleCI.

package test

import (
"strings"
"testing"

"github.com/gruntwork-io/terratest/modules/azure"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)

func TestTerraformAzureFunctionAppExample(t *testing.T) {
t.Parallel()

//_random := strings.ToLower(random.UniqueId())
uniquePostfix := strings.ToLower(random.UniqueId())

// website::tag::1:: Configure Terraform setting up a path to Terraform code.
terraformOptions := &terraform.Options{
TerraformDir: "../../examples/azure/terraform-azure-functionapp-example",
Vars: map[string]interface{}{
"postfix": uniquePostfix,
},
}
// website::tag::5:: At the end of the test, run `terraform destroy` to clean up any resources that were created
defer terraform.Destroy(t, terraformOptions)

// website::tag::2:: Run `terraform init` and `terraform apply`. Fail the test if there are any errors.
terraform.InitAndApply(t, terraformOptions)

// website::tag::3:: Run `terraform output` to get the values of output variables
resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
appName := terraform.Output(t, terraformOptions, "function_app_name")

appId := terraform.Output(t, terraformOptions, "function_app_id")
appDefaultHostName := terraform.Output(t, terraformOptions, "default_hostname")
appKind := terraform.Output(t, terraformOptions, "function_app_kind")

// website::tag::4:: Assert
assert.True(t, azure.AppExists(t, appName, resourceGroupName, ""))
site := azure.GetAppService(t, appName, resourceGroupName, "")

assert.Equal(t, appId, *site.ID)
assert.Equal(t, appDefaultHostName, *site.DefaultHostName)
assert.Equal(t, appKind, *site.Kind)

assert.NotEmpty(t, *site.OutboundIPAddresses)
assert.Equal(t, "Running", *site.State)
}

0 comments on commit 3d07a97

Please sign in to comment.