diff --git a/.vscode/cspell-github-user-aliases.txt b/.vscode/cspell-github-user-aliases.txt index 02a20883e66..cdab5b59d0b 100644 --- a/.vscode/cspell-github-user-aliases.txt +++ b/.vscode/cspell-github-user-aliases.txt @@ -26,6 +26,7 @@ nicksnyder otiai10 pamelafox pbnj +rujche sebastianmattar sergi sethvargo @@ -33,6 +34,6 @@ stretchr theckman TheEskhaton tonybaloney +vivazqu weilim -Yionse -rujche +Yionse \ No newline at end of file diff --git a/cli/azd/cmd/env.go b/cli/azd/cmd/env.go index 9d80f16ee92..10a8a7f101f 100644 --- a/cli/azd/cmd/env.go +++ b/cli/azd/cmd/env.go @@ -426,7 +426,7 @@ func (e *envSetSecretAction) Run(ctx context.Context) (*actions.ActionResult, er } // akvs -> Azure Key Vault Secret (akvs:////) - envValue := keyvault.NewAkvs(subId, kvAccount.Name, kvSecretName) + envValue := keyvault.NewAzureKeyVaultSecret(subId, kvAccount.Name, kvSecretName) e.env.DotenvSet(secretName, envValue) if err := e.envManager.Save(ctx, e.env); err != nil { return nil, fmt.Errorf("saving environment: %w", err) diff --git a/cli/azd/docs/using-environment-secrets.md b/cli/azd/docs/using-environment-secrets.md index d59f5642228..ec6cc694db8 100644 --- a/cli/azd/docs/using-environment-secrets.md +++ b/cli/azd/docs/using-environment-secrets.md @@ -79,3 +79,79 @@ hooks: ``` The next time you execute `azd provision`, the `preprovision` hook will run and resolve `MY_SECRET` into `SECRET_RESOLVE`. + + +## Pipeline config + +The Azure Developer CLI simplifies the process of setting up continuous integration (CI) for your application. Whether you are using GitHub or Azure DevOps, you can run the command `azd pipeline config` and follow the guided steps provided by AZD to configure CI/CD. + +As part of the automatic configuration, AZD creates secrets and variables for your CI/CD deployment workflow. For example, the Azure Subscription ID and location are set as variables. Additionally, you can define a list of variables and secrets by using the `pipeline` configuration in the `azure.yaml` file within your project. The list of variables or secrets you define corresponds to the names of the keys in your AZD environment (.env). If the name of the key holds a secret reference (akvs), AZD will apply the following rules to set the value in your CI/CD settings. + +### Variables + +If the secret is added to the `variables` section of the pipeline configuration, the Azure Developer CLI (AZD) will use the value from the environment without retrieving the actual secret value. This approach is beneficial when you prefer to maintain Azure Key Vault references within your CI/CD settings. By doing so, you can rotate your secrets in the Key Vault, ensuring that your CI/CD pipeline uses the latest secret values without the need to update your workflow variables or secrets. + +#### Example + +- From an initialized AZD project, you have already set a secret. For example, you ran: + +```sh +azd env set-secret SECURE_KEY +``` + +If you run `azd env get-values`, you would see an output like: + +``` +AZURE_ENV_NAME="your-env-name" +AZURE_LOCATION="location" +AZURE_SUBSCRIPTION_ID="faa080af-c1d8-40ad-9cce-000000000000" +SECURE_KEY="akvs://faa080af-c1d8-40ad-9cce-000000000000/vivazqu-kv/SECURE-KEY-kv-secret" +``` + +- Then, you can set `SECURE_KEY` as a `variable` for your CI/CD pipeline by adding the `pipeline` field to the `azure.yaml` like: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json +name: your-project-name +pipeline: + variables: + - SECURE_KEY +``` +When you run `azd pipeline config`, the `SECURE_KEY` will be set as a variable in your CI/CD workflow, and its value will be the Azure Key Vault reference. For the example above, it would be: `akvs://faa080af-c1d8-40ad-9cce-000000000000/vivazqu-kv/SECURE-KEY-kv-secret`. + +> Note: AZD will attempt to assign a read-access role to the service principal used for logging into AZD from the CI/CD workflow. If you do not have sufficient permissions to assign the read role for the Key Vault, the operation will fail. + +You can reference the `SECURE_KEY` variable as a system environment variable. If `SECURE_KEY` is mapped to a Bicep input parameter or if it is mapped from a hook definition (refer to the earlier sections of this document for more details), AZD will automatically retrieve the value for the secret. + +### Secrets + +If the secret is added to the `secrets` section of the pipeline configuration, the Azure Developer CLI (AZD) will retrieve the actual value of the secret from the Azure Key Vault. This method is useful when it is not possible to assign a read-access role to the service principal used in your CI/CD workflow. However, it is important to note that you will need to update the secret value manually each time the secret is rotated. To re-apply the updated value, you can run the `azd pipeline config` command again. + +#### Example + +- From an initialized AZD project, you have already set a secret. For example, you ran: + +```sh +azd env set-secret SECURE_KEY +``` + +If you run `azd env get-values`, you would see an output like: + +``` +AZURE_ENV_NAME="your-env-name" +AZURE_LOCATION="location" +AZURE_SUBSCRIPTION_ID="faa080af-c1d8-40ad-9cce-000000000000" +SECURE_KEY="akvs://faa080af-c1d8-40ad-9cce-000000000000/vivazqu-kv/SECURE-KEY-kv-secret" +``` + +- Then, you can set `SECURE_KEY` as a `secret` for your CI/CD pipeline by adding the `pipeline` field to the `azure.yaml` like: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json +name: your-project-name +pipeline: + secrets: + - SECURE_KEY +``` + +- When you run `azd pipeline config`, the `SECURE_KEY` will be set as a secret in your CI/CD workflow and its value will be the Azure Key Vault value. diff --git a/cli/azd/pkg/ext/hooks_runner.go b/cli/azd/pkg/ext/hooks_runner.go index 7a78c5b3df3..66448a581c6 100644 --- a/cli/azd/pkg/ext/hooks_runner.go +++ b/cli/azd/pkg/ext/hooks_runner.go @@ -148,7 +148,7 @@ func (h *HooksRunner) execHook(ctx context.Context, hookConfig *HookConfig, opti for key, value := range hookConfig.Secrets { setValue := value if valueFromEnv, exists := h.env.LookupEnv(value); exists { - if keyvault.IsAkvs(valueFromEnv) { + if keyvault.IsAzureKeyVaultSecret(valueFromEnv) { secretValue, err := keyvaultService.SecretFromAkvs(ctx, valueFromEnv) if err != nil { return err diff --git a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go index 989c6f01f49..9426d4544ae 100644 --- a/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go +++ b/cli/azd/pkg/infra/provisioning/bicep/bicep_provider.go @@ -1882,7 +1882,7 @@ func (p *BicepProvider) ensureParameters( if stringValue, isString := paramValue.(string); isString && param.Secure() { // For secure parameters using a string value, azd checks if the string is an Azure Key Vault Secret // and if yes, it fetches the secret value from the Key Vault. - if keyvault.IsAkvs(stringValue) { + if keyvault.IsAzureKeyVaultSecret(stringValue) { paramValue, err = p.keyvaultService.SecretFromAkvs(ctx, stringValue) if err != nil { return nil, err diff --git a/cli/azd/pkg/keyvault/keyvault.go b/cli/azd/pkg/keyvault/keyvault.go index 7b13748b14f..cb98c253078 100644 --- a/cli/azd/pkg/keyvault/keyvault.go +++ b/cli/azd/pkg/keyvault/keyvault.go @@ -341,7 +341,7 @@ const ( RoleIdKeyVaultSecretsUser string = resourceIdPathPrefix + "4633458b-17de-408a-b874-0445c86b69e6" ) -func IsAkvs(id string) bool { +func IsAzureKeyVaultSecret(id string) bool { return strings.HasPrefix(id, vaultSchemaAkvs) } @@ -351,32 +351,70 @@ func IsValidSecretName(kvSecretName string) bool { }) == -1 } -func NewAkvs(subId, vaultId, secretName string) string { +func NewAzureKeyVaultSecret(subId, vaultId, secretName string) string { return vaultSchemaAkvs + subId + "/" + vaultId + "/" + secretName } func (kvs *keyVaultService) SecretFromAkvs(ctx context.Context, akvs string) (string, error) { - if !IsAkvs(akvs) { - return "", fmt.Errorf("invalid Azure Key Vault Secret reference: %s", akvs) + parseAkvs, err := ParseAzureKeyVaultSecret(akvs) + if err != nil { + return "", err } - noSchema := strings.TrimPrefix(akvs, vaultSchemaAkvs) - vaultParts := strings.Split(noSchema, "/") - if len(vaultParts) != 3 { - return "", fmt.Errorf( - "invalid Azure Key Vault Secret reference: %s. Expected format: %s", - akvs, - vaultSchemaAkvs+"//", - ) - } - subscriptionId, vaultName, secretName := vaultParts[0], vaultParts[1], vaultParts[2] // subscriptionId is required by the Key Vault service to figure the TenantId for the // tokenCredential. The assumption here is that the user has access to the Tenant // used to deploy the app and to whatever Tenant the Key Vault is in. And the tokenCredential // can use any of the Tenant ids. - secretValue, err := kvs.GetKeyVaultSecret(ctx, subscriptionId, vaultName, secretName) + secretValue, err := kvs.GetKeyVaultSecret( + ctx, parseAkvs.SubscriptionId, parseAkvs.VaultName, parseAkvs.SecretName) if err != nil { return "", fmt.Errorf("fetching secret value from key vault: %w", err) } return secretValue.Value, nil } + +// AzureKeyVaultSecret represents a secret stored in an Azure Key Vault. +// It contains the necessary information to identify and access the secret. +// +// Fields: +// - SubscriptionId: The ID of the Azure subscription that contains the Key Vault. +// - VaultName: The name of the Key Vault where the secret is stored. +// - SecretName: The name of the secret within the Key Vault. +type AzureKeyVaultSecret struct { + SubscriptionId string + VaultName string + SecretName string +} + +// ParseAzureKeyVaultSecret parses a string representing an Azure Key Vault Secret reference +// and returns an AzureKeyVaultSecret struct if the reference is valid. +// +// The expected format for the Azure Key Vault Secret reference is: +// "akvs:////" +// +// Parameters: +// - akvs: A string representing the Azure Key Vault Secret reference. +// +// Returns: +// - AzureKeyVaultSecret: A struct containing the subscription ID, vault name, and secret name. +// - error: An error if the Azure Key Vault Secret reference is invalid. +func ParseAzureKeyVaultSecret(akvs string) (AzureKeyVaultSecret, error) { + if !IsAzureKeyVaultSecret(akvs) { + return AzureKeyVaultSecret{}, fmt.Errorf("invalid Azure Key Vault Secret reference: %s", akvs) + } + + noSchema := strings.TrimPrefix(akvs, vaultSchemaAkvs) + vaultParts := strings.Split(noSchema, "/") + if len(vaultParts) != 3 { + return AzureKeyVaultSecret{}, fmt.Errorf( + "invalid Azure Key Vault Secret reference: %s. Expected format: %s", + akvs, + vaultSchemaAkvs+"//", + ) + } + return AzureKeyVaultSecret{ + SubscriptionId: vaultParts[0], + VaultName: vaultParts[1], + SecretName: vaultParts[2], + }, nil +} diff --git a/cli/azd/pkg/pipeline/pipeline.go b/cli/azd/pkg/pipeline/pipeline.go index 84d75642d55..a3bbce3104d 100644 --- a/cli/azd/pkg/pipeline/pipeline.go +++ b/cli/azd/pkg/pipeline/pipeline.go @@ -251,4 +251,6 @@ type projectProperties struct { HasAppHost bool BranchName string AuthType PipelineAuthType + Variables []string + Secrets []string } diff --git a/cli/azd/pkg/pipeline/pipeline_manager.go b/cli/azd/pkg/pipeline/pipeline_manager.go index 24fc433f1d7..7a86a400421 100644 --- a/cli/azd/pkg/pipeline/pipeline_manager.go +++ b/cli/azd/pkg/pipeline/pipeline_manager.go @@ -24,6 +24,7 @@ import ( "github.com/azure/azure-dev/cli/azd/pkg/infra/provisioning" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/ioc" + "github.com/azure/azure-dev/cli/azd/pkg/keyvault" "github.com/azure/azure-dev/cli/azd/pkg/osutil" "github.com/azure/azure-dev/cli/azd/pkg/output" "github.com/azure/azure-dev/cli/azd/pkg/output/ux" @@ -94,6 +95,7 @@ type PipelineManager struct { configOptions *configurePipelineOptions infra *project.Infra userConfigManager config.UserConfigManager + keyVaultService keyvault.KeyVaultService } func NewPipelineManager( @@ -108,6 +110,7 @@ func NewPipelineManager( serviceLocator ioc.ServiceLocator, importManager *project.ImportManager, userConfigManager config.UserConfigManager, + keyVaultService keyvault.KeyVaultService, ) (*PipelineManager, error) { pipelineProvider := &PipelineManager{ azdCtx: azdCtx, @@ -120,6 +123,7 @@ func NewPipelineManager( serviceLocator: serviceLocator, importManager: importManager, userConfigManager: userConfigManager, + keyVaultService: keyVaultService, } // check that scm and ci providers are set @@ -408,6 +412,72 @@ func (pm *PipelineManager) Configure(ctx context.Context, projectName string) (r pm.configOptions.projectVariables, pm.configOptions.projectSecrets, defaultAzdVariables, defaultAzdSecrets, pm.env.Dotenv()) + // resolve akvs secrets + // For each akvs in the secrets array: + // azd gets the value from Azure Key Vault and use it as a secret in the pipeline + for key, value := range pm.configOptions.secrets { + if !strings.HasPrefix(value, "akvs://") { + continue + } + kvSecret, err := pm.keyVaultService.SecretFromAkvs(ctx, value) + if err != nil { + return result, fmt.Errorf("failed to resolve akvs '%s': %w", key, err) + } + pm.configOptions.secrets[key] = kvSecret + } + // For each akvs in the variables array: + // azd must grant read access role to the pipelines's identity to read the akvs + displayMsg = "Assigning read access role for Key Vault to service principal" + pm.console.ShowSpinner(ctx, displayMsg, input.Step) + kvAccounts := make(map[string]struct{}) + for key, value := range pm.configOptions.variables { + if !strings.HasPrefix(value, "akvs://") { + continue + } + + akvs, err := keyvault.ParseAzureKeyVaultSecret(value) + if err != nil { + return result, fmt.Errorf("failed to parse akvs '%s': %w", key, err) + } + kvId := akvs.SubscriptionId + akvs.VaultName + if _, ok := kvAccounts[kvId]; ok { + // skip if already assigned role for this key vault + continue + } + + // can't use keyvaultService.Get() because it requires the resource group name and we don't save it for akvs + allKvFromSub, err := pm.keyVaultService.ListSubscriptionVaults(ctx, akvs.SubscriptionId) + if err != nil { + return result, fmt.Errorf( + "assigning read access role for Key Vault to service principal: %w", err) + } + var vaultResourceId string + foundKeyVault := slices.ContainsFunc(allKvFromSub, func(kv keyvault.Vault) bool { + if kv.Name == akvs.VaultName { + vaultResourceId = kv.Id + return true + } + return false + }) + if !foundKeyVault { + return result, fmt.Errorf( + "assigning read access role for Key Vault to service principal: "+ + "key vault '%s' not found in subscription '%s'", akvs.VaultName, akvs.SubscriptionId) + } + + // CreateRbac uses the azure-sdk RoleAssignmentsClient.Create() which creates or updates the role assignment + // We don't need to check if the role assignment already exists, the method will handle it. + err = pm.entraIdService.CreateRbac( + ctx, akvs.SubscriptionId, vaultResourceId, keyvault.RoleIdKeyVaultSecretsUser, *servicePrincipal.Id) + if err != nil { + return result, fmt.Errorf( + "assigning read access role for Key Vault to service principal: %w", err) + } + // save the kvId to avoid assigning the role multiple times for the same key vault + kvAccounts[kvId] = struct{}{} + } + pm.console.StopSpinner(ctx, displayMsg, input.GetStepResultFormat(err)) + // config pipeline handles setting or creating the provider pipeline to be used ciPipeline, err := pm.ciProvider.configurePipeline(ctx, gitRepoInfo, pm.configOptions) if err != nil { @@ -766,6 +836,8 @@ func (pm *PipelineManager) initialize(ctx context.Context, override string) erro HasAppHost: hasAppHost, BranchName: branchName, AuthType: authType, + Variables: prjConfig.Pipeline.Variables, + Secrets: prjConfig.Pipeline.Secrets, }); err != nil { return err } @@ -975,10 +1047,14 @@ func generatePipelineDefinition(path string, props projectProperties) error { BranchName string FedCredLogIn bool InstallDotNetForAspire bool + Variables []string + Secrets []string }{ BranchName: props.BranchName, FedCredLogIn: props.AuthType == AuthTypeFederated, InstallDotNetForAspire: props.HasAppHost, + Variables: props.Variables, + Secrets: props.Secrets, }) if err != nil { return fmt.Errorf("executing template: %w", err) diff --git a/cli/azd/pkg/pipeline/pipeline_manager_test.go b/cli/azd/pkg/pipeline/pipeline_manager_test.go index 6396a37f925..15aa6f60755 100644 --- a/cli/azd/pkg/pipeline/pipeline_manager_test.go +++ b/cli/azd/pkg/pipeline/pipeline_manager_test.go @@ -533,6 +533,30 @@ func Test_promptForCiFiles(t *testing.T) { assert.NoError(t, err) snapshot.SnapshotT(t, normalizeEOL(content)) }) + t.Run("no files - github selected - Variables and Secrets", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, pipelineProviderFiles[ciProviderGitHubActions].PipelineDirectories[0]) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, pipelineProviderFiles[ciProviderGitHubActions].Files[0]) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderGitHubActions, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: true, + BranchName: "main", + AuthType: AuthTypeFederated, + Variables: []string{"VAR_1", "VAR_2"}, + Secrets: []string{"SECRET_1", "SECRET_2"}, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) t.Run("no files - azdo selected - App host - fed Cred", func(t *testing.T) { tempDir := t.TempDir() path := filepath.Join(tempDir, pipelineProviderFiles[ciProviderAzureDevOps].PipelineDirectories[0]) @@ -621,6 +645,30 @@ func Test_promptForCiFiles(t *testing.T) { assert.NoError(t, err) snapshot.SnapshotT(t, normalizeEOL(content)) }) + t.Run("no files - azdo selected - no app host - variables and secrets", func(t *testing.T) { + tempDir := t.TempDir() + path := filepath.Join(tempDir, pipelineProviderFiles[ciProviderAzureDevOps].PipelineDirectories[0]) + err := os.MkdirAll(path, osutil.PermissionDirectory) + assert.NoError(t, err) + expectedPath := filepath.Join(tempDir, pipelineProviderFiles[ciProviderAzureDevOps].Files[0]) + err = generatePipelineDefinition(expectedPath, projectProperties{ + CiProvider: ciProviderAzureDevOps, + InfraProvider: infraProviderBicep, + RepoRoot: tempDir, + HasAppHost: false, + BranchName: "main", + AuthType: AuthTypeFederated, + Variables: []string{"VAR_1", "VAR_2"}, + Secrets: []string{"SECRET_1", "SECRET_2"}, + }) + assert.NoError(t, err) + // should've created the pipeline + assert.FileExists(t, expectedPath) + // open the file and check the content + content, err := os.ReadFile(expectedPath) + assert.NoError(t, err) + snapshot.SnapshotT(t, normalizeEOL(content)) + }) t.Run("no files - azdo selected - no app host - client cred", func(t *testing.T) { tempDir := t.TempDir() path := filepath.Join(tempDir, pipelineProviderFiles[ciProviderAzureDevOps].PipelineDirectories[0]) @@ -775,6 +823,7 @@ func createPipelineManager( mockContext.Container, project.NewImportManager(project.NewDotNetImporter(nil, nil, nil, nil, mockContext.AlphaFeaturesManager)), &mockUserConfigManager{}, + nil, ) } diff --git a/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_variables_and_secrets.snap b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_variables_and_secrets.snap new file mode 100644 index 00000000000..1238e712534 --- /dev/null +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_azdo_selected_-_no_app_host_-_variables_and_secrets.snap @@ -0,0 +1,64 @@ +# Run when commits are pushed to main +trigger: + - main + +pool: + vmImage: ubuntu-latest + +steps: + # setup-azd@0 needs to be manually installed in your organization + # if you can't install it, you can use the below bash script to install azd + # and remove this step + - task: setup-azd@0 + displayName: Install azd + + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + + # azd delegate auth to az to use service connection with AzureCLI@2 + - pwsh: | + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. + - task: AzureCLI@2 + displayName: Provision Infrastructure + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd provision --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) + VAR_1: $(VAR_1) + VAR_2: $(VAR_2) + SECRET_1: $(SECRET_1) + SECRET_2: $(SECRET_2) + + - task: AzureCLI@2 + displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + keepAzSessionActive: true + inlineScript: | + azd deploy --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + VAR_1: $(VAR_1) + VAR_2: $(VAR_2) + SECRET_1: $(SECRET_1) + SECRET_2: $(SECRET_2) + + diff --git a/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_Variables_and_Secrets.snap b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_Variables_and_Secrets.snap new file mode 100644 index 00000000000..84fd89b3812 --- /dev/null +++ b/cli/azd/pkg/pipeline/testdata/Test_promptForCiFiles-no_files_-_github_selected_-_Variables_and_Secrets.snap @@ -0,0 +1,58 @@ +# Run when commits are pushed to main +on: + workflow_dispatch: + push: + # Run when commits are pushed to mainline branch (main or master) + # Set this to the mainline branch you are using + branches: + - main + +# Set up permissions for deploying with secretless Azure federated credentials +# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication +permissions: + id-token: write + contents: read + + +jobs: + build: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + VAR_1: ${{ vars.VAR_1 }} + VAR_2: ${{ vars.VAR_2 }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install azd + uses: Azure/setup-azd@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x.x + 9.x.x + + - name: Log in with Azure (Federated Credentials) + run: | + azd auth login ` + --client-id "$Env:AZURE_CLIENT_ID" ` + --federated-credential-provider "github" ` + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + + - name: Provision Infrastructure + run: azd provision --no-prompt + env: + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + SECRET_1: ${{ secrets.SECRET_1 }} + SECRET_2: ${{ secrets.SECRET_2 }} + + - name: Deploy Application + run: azd deploy --no-prompt + diff --git a/cli/azd/resources/pipeline/.azdo/azure-dev.ymlt b/cli/azd/resources/pipeline/.azdo/azure-dev.ymlt index 890a855a3b8..1bb7670beab 100644 --- a/cli/azd/resources/pipeline/.azdo/azure-dev.ymlt +++ b/cli/azd/resources/pipeline/.azdo/azure-dev.ymlt @@ -49,6 +49,12 @@ steps: AZURE_ENV_NAME: $(AZURE_ENV_NAME) AZURE_LOCATION: $(AZURE_LOCATION) AZD_INITIAL_ENVIRONMENT_CONFIG: $(AZD_INITIAL_ENVIRONMENT_CONFIG) +{{- range $variable := .Variables }} + {{ $variable }}: $({{ $variable }}) +{{- end}} +{{- range $secret := .Secrets }} + {{ $secret }}: $({{ $secret }}) +{{- end}} - task: AzureCLI@2 displayName: Deploy Application @@ -63,5 +69,11 @@ steps: AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) AZURE_ENV_NAME: $(AZURE_ENV_NAME) AZURE_LOCATION: $(AZURE_LOCATION) +{{- range $variable := .Variables }} + {{ $variable }}: $({{ $variable }}) +{{- end}} +{{- range $secret := .Secrets }} + {{ $secret }}: $({{ $secret }}) +{{- end}} {{ end}} \ No newline at end of file diff --git a/cli/azd/resources/pipeline/.github/azure-dev.ymlt b/cli/azd/resources/pipeline/.github/azure-dev.ymlt index bf8d90323b5..1cc17ea6d3c 100644 --- a/cli/azd/resources/pipeline/.github/azure-dev.ymlt +++ b/cli/azd/resources/pipeline/.github/azure-dev.ymlt @@ -25,6 +25,9 @@ jobs: AZURE_SUBSCRIPTION_ID: ${{ "{{" }} vars.AZURE_SUBSCRIPTION_ID {{ "}}" }} AZURE_ENV_NAME: ${{ "{{" }} vars.AZURE_ENV_NAME {{ "}}" }} AZURE_LOCATION: ${{ "{{" }} vars.AZURE_LOCATION {{ "}}" }} +{{- range $variable := .Variables }} + {{ $variable }}: ${{ "{{" }} vars.{{ $variable }} {{ "}}" }} +{{- end}} steps: - name: Checkout uses: actions/checkout@v4 @@ -67,6 +70,9 @@ jobs: run: azd provision --no-prompt env: AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ "{{" }} secrets.AZD_INITIAL_ENVIRONMENT_CONFIG {{ "}}" }} +{{- range $secret := .Secrets }} + {{ $secret }}: ${{ "{{" }} secrets.{{ $secret }} {{ "}}" }} +{{- end}} - name: Deploy Application run: azd deploy --no-prompt diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index 1f5e783a293..c096b1fbca1 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -638,19 +638,14 @@ "$ref": "#/definitions/hook" }, "secrets": { - "type": "array", + "type": "object", "title": "Optional. Map of azd environment variables to hook secrets.", "description": "If variable was set as a secret in the environment, the secret value will be passed to the hook.", - "items": { - "type": "object", - "additionalProperties": { + "additionalProperties": { "type": "string" }, - "minProperties": 1, - "maxProperties": 1, - "example": { - "WITH_SECRET_VALUE": "ENV_VAR_WITH_SECRET" - } + "example": { + "WITH_SECRET_VALUE": "ENV_VAR_WITH_SECRET" } } },