Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Azure FunctionApp Module #591

Merged
merged 10 commits into from
Jul 16, 2021
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)
}