diff --git a/backend/remote-state/azure/arm_client.go b/backend/remote-state/azure/arm_client.go index 90e4b261d..8a289f68e 100644 --- a/backend/remote-state/azure/arm_client.go +++ b/backend/remote-state/azure/arm_client.go @@ -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 { diff --git a/backend/remote-state/azure/backend.go b/backend/remote-state/azure/backend.go index a930cbb02..9d08f4124 100644 --- a/backend/remote-state/azure/backend.go +++ b/backend/remote-state/azure/backend.go @@ -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 }, @@ -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 { @@ -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) diff --git a/backend/remote-state/azure/backend_test.go b/backend/remote-state/azure/backend_test.go index 1df684e1a..db798b4ef 100644 --- a/backend/remote-state/azure/backend_test.go +++ b/backend/remote-state/azure/backend_test.go @@ -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) diff --git a/backend/remote-state/azure/client_test.go b/backend/remote-state/azure/client_test.go index d3edfd39b..8f51a9082 100644 --- a/backend/remote-state/azure/client_test.go +++ b/backend/remote-state/azure/client_test.go @@ -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) diff --git a/backend/remote-state/azure/helpers_test.go b/backend/remote-state/azure/helpers_test.go index 9e2d8d343..343a980f5 100644 --- a/backend/remote-state/azure/helpers_test.go +++ b/backend/remote-state/azure/helpers_test.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "strings" "testing" "github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources" @@ -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, @@ -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) diff --git a/website/docs/backends/types/azurerm.html.md b/website/docs/backends/types/azurerm.html.md index 7abf40309..d2d1d27c3 100644 --- a/website/docs/backends/types/azurerm.html.md +++ b/website/docs/backends/types/azurerm.html.md @@ -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 @@ -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 @@ -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.