Skip to content

Commit

Permalink
backend/azurerm: support for authenticating via msi (#19433)
Browse files Browse the repository at this point in the history
* backend/azurerm: support for authenticating via msi

* adding acceptance tests for msi auth

* including the resource group name in the tests

* support for using the test client via msi
  • Loading branch information
tombuildsstuff authored Nov 22, 2018
1 parent 9f15381 commit c928962
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 3 deletions.
6 changes: 4 additions & 2 deletions backend/remote-state/azure/arm_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ func buildArmClient(config BackendConfig) (*ArmClient, error) {
SubscriptionID: config.SubscriptionID,
TenantID: config.TenantID,
Environment: config.Environment,
MsiEndpoint: config.MsiEndpoint,

// Feature Toggles
SupportsClientSecretAuth: true,
// TODO: support for Azure CLI / Client Certificate / MSI
SupportsClientSecretAuth: true,
SupportsManagedServiceIdentity: config.UseMsi,
// TODO: support for Azure CLI / Client Certificate auth
}
armConfig, err := builder.Build()
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions backend/remote-state/azure/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ func New() backend.Backend {
DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""),
},

"use_msi": {
Type: schema.TypeBool,
Optional: true,
Description: "Should Managed Service Identity be used?.",
DefaultFunc: schema.EnvDefaultFunc("ARM_USE_MSI", false),
},

"msi_endpoint": {
Type: schema.TypeString,
Optional: true,
Description: "The Managed Service Identity Endpoint.",
DefaultFunc: schema.EnvDefaultFunc("ARM_MSI_ENDPOINT", ""),
},

// TODO: rename these fields
// TODO: support for custom resource manager endpoints
},
Expand Down Expand Up @@ -106,9 +120,11 @@ type BackendConfig struct {
ClientID string
ClientSecret string
Environment string
MsiEndpoint string
ResourceGroupName string
SubscriptionID string
TenantID string
UseMsi bool
}

func (b *Backend) configure(ctx context.Context) error {
Expand All @@ -127,10 +143,12 @@ func (b *Backend) configure(ctx context.Context) error {
ClientID: data.Get("arm_client_id").(string),
ClientSecret: data.Get("arm_client_secret").(string),
Environment: data.Get("environment").(string),
MsiEndpoint: data.Get("msi_endpoint").(string),
ResourceGroupName: data.Get("resource_group_name").(string),
StorageAccountName: data.Get("storage_account_name").(string),
SubscriptionID: data.Get("arm_subscription_id").(string),
TenantID: data.Get("arm_tenant_id").(string),
UseMsi: data.Get("use_msi").(bool),
}

armClient, err := buildArmClient(config)
Expand Down
28 changes: 28 additions & 0 deletions backend/remote-state/azure/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,34 @@ func TestBackendAccessKeyBasic(t *testing.T) {
backend.TestBackendStates(t, b)
}

func TestBackendManagedServiceIdentityBasic(t *testing.T) {
testAccAzureBackendRunningInAzure(t)
rs := acctest.RandString(4)
res := testResourceNames(rs, "testState")
armClient := buildTestClient(t, res)

ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)

b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"resource_group_name": res.resourceGroup,
"use_msi": true,
"arm_subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"),
"arm_tenant_id": os.Getenv("ARM_TENANT_ID"),
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)

backend.TestBackendStates(t, b)
}

func TestBackendServicePrincipalBasic(t *testing.T) {
testAccAzureBackend(t)
rs := acctest.RandString(4)
Expand Down
33 changes: 33 additions & 0 deletions backend/remote-state/azure/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,39 @@ func TestRemoteClientAccessKeyBasic(t *testing.T) {
remote.TestClient(t, state.(*remote.State).Client)
}

func TestRemoteClientManagedServiceIdentityBasic(t *testing.T) {
testAccAzureBackendRunningInAzure(t)
rs := acctest.RandString(4)
res := testResourceNames(rs, "testState")
armClient := buildTestClient(t, res)

ctx := context.TODO()
err := armClient.buildTestResources(ctx, &res)
if err != nil {
armClient.destroyTestResources(ctx, res)
t.Fatalf("Error creating Test Resources: %q", err)
}
defer armClient.destroyTestResources(ctx, res)

b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"storage_account_name": res.storageAccountName,
"container_name": res.storageContainerName,
"key": res.storageKeyName,
"resource_group_name": res.resourceGroup,
"use_msi": true,
"arm_subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"),
"arm_tenant_id": os.Getenv("ARM_TENANT_ID"),
"environment": os.Getenv("ARM_ENVIRONMENT"),
})).(*Backend)

state, err := b.StateMgr(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}

remote.TestClient(t, state.(*remote.State).Client)
}

func TestRemoteClientServicePrincipalBasic(t *testing.T) {
testAccAzureBackend(t)
rs := acctest.RandString(4)
Expand Down
31 changes: 30 additions & 1 deletion backend/remote-state/azure/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"strings"
"testing"

"github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources"
Expand All @@ -21,20 +22,47 @@ func testAccAzureBackend(t *testing.T) {
}
}

// these kind of tests can only run when within Azure (e.g. MSI)
func testAccAzureBackendRunningInAzure(t *testing.T) {
testAccAzureBackend(t)

if os.Getenv("TF_RUNNING_IN_AZURE") == "" {
t.Skip("Skipping test since not running in Azure")
}
}

func buildTestClient(t *testing.T, res resourceNames) *ArmClient {
subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID")
tenantID := os.Getenv("ARM_TENANT_ID")
clientID := os.Getenv("ARM_CLIENT_ID")
clientSecret := os.Getenv("ARM_CLIENT_SECRET")
msiEnabled := strings.EqualFold(os.Getenv("ARM_USE_MSI"), "true")
environment := os.Getenv("ARM_ENVIRONMENT")

// location isn't used in this method, but is in the other test methods
location := os.Getenv("ARM_LOCATION")

if subscriptionID == "" || tenantID == "" || clientID == "" || clientSecret == "" || environment == "" || location == "" {
hasCredentials := (clientID != "" && clientSecret != "") || msiEnabled
if !hasCredentials {
t.Fatal("Azure credentials missing or incomplete")
}

if subscriptionID == "" {
t.Fatalf("Missing ARM_SUBSCRIPTION_ID")
}

if tenantID == "" {
t.Fatalf("Missing ARM_TENANT_ID")
}

if environment == "" {
t.Fatalf("Missing ARM_ENVIRONMENT")
}

if location == "" {
t.Fatalf("Missing ARM_LOCATION")
}

armClient, err := buildArmClient(BackendConfig{
SubscriptionID: subscriptionID,
TenantID: tenantID,
Expand All @@ -43,6 +71,7 @@ func buildTestClient(t *testing.T, res resourceNames) *ArmClient {
Environment: environment,
ResourceGroupName: res.resourceGroup,
StorageAccountName: res.storageAccountName,
UseMsi: msiEnabled,
})
if err != nil {
t.Fatalf("Failed to build ArmClient: %+v", err)
Expand Down
43 changes: 43 additions & 0 deletions website/docs/backends/types/azurerm.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ terraform {
}
```

When authenticating using Managed Service Identity (MSI):

```hcl
terraform {
backend "azurerm" {
storage_account_name = "abcd1234"
container_name = "tfstate"
key = "prod.terraform.tfstate"
use_msi = true
arm_subscription_id = "00000000-0000-0000-0000-000000000000"
arm_tenant_id = "00000000-0000-0000-0000-000000000000"
}
}
```

When authenticating using the Access Key associated with the Storage Account:

```hcl
Expand Down Expand Up @@ -60,6 +75,22 @@ data "terraform_remote_state" "foo" {
}
```

When authenticating using Managed Service Identity (MSI):

```hcl
data "terraform_remote_state" "foo" {
backend = "azurerm"
config = {
storage_account_name = "terraform123abc"
container_name = "terraform-state"
key = "prod.terraform.tfstate"
use_msi = true
arm_subscription_id = "00000000-0000-0000-0000-000000000000"
arm_tenant_id = "00000000-0000-0000-0000-000000000000"
}
}
```

When authenticating using the Access Key associated with the Storage Account:

```hcl
Expand Down Expand Up @@ -91,6 +122,18 @@ The following configuration options are supported:

---

When authenticating using the Managed Service Identity (MSI) - the following fields are also supported:

* `arm_subscription_id` - (Optional) The Subscription ID in which the Storage Account exists. This can also be sourced from the `ARM_SUBSCRIPTION_ID` environment variable.

* `arm_tenant_id` - (Optional) The Tenant ID in which the Subscription exists. This can also be sourced from the `ARM_TENANT_ID` environment variable.

* `msi_endpoint` - (Optional) The path to a custom Managed Service Identity endpoint which is automatically determined if not specified. This can also be sourced from the `ARM_MSI_ENDPOINT` environment variable.

* `use_msi` - (Optional) Should Managed Service Identity authentication be used? This can also be sourced from the `ARM_USE_MSI` environment variable.

---

When authenticating using the Storage Account's Access Key - the following fields are also supported:

* `access_key` - (Optional) The Access Key used to access the Blob Storage Account. This can also be sourced from the `ARM_ACCESS_KEY` environment variable.
Expand Down

0 comments on commit c928962

Please sign in to comment.