Hands-on with Backstage on Azure: From App Service to Azure DevOps

Chapter 2 - Containerize Backstage

Overview

In this chapter, we will containerize our Backstage instance. We will use the Pulumi Docker provider to build and push the container to the Azure Container Registry.

The container will be used in the next chapter to deploy the Backstage instance to Azure as we going to build on top of what we created in this chapter.

In a production environment, you would use a CI/CD pipeline to build and push the container to the registry. Check for example this article.

Instructions

Step 0 - Prerequisites

Overwrite the content of backstage/app-config.production.yaml with the following content:

app:
  baseUrl: ${BACKSTAGE_BASE_URL}

backend:
  baseUrl: ${BACKSTAGE_BASE_URL}
  listen:
    port: ${WEBSITES_PORT}
    host: 0.0.0.0
  cors:
    origin: ${BACKSTAGE_BASE_URL}
  database:
    client: pg
    connection:
      host: ${POSTGRES_HOST}
      port: ${POSTGRES_PORT}
      user: ${POSTGRES_USER}
      password: ${POSTGRES_PASSWORD}

This will be used to configure the Backstage instance to work with Azure, as we will pass the values as environment variables to the container.

Create a new directory for all the workshop files and navigate into it.

mkdir backstage-azure-infrastructure
cd backstage-azure-infrastructure

This will be the root directory for the workshop. Keep this in mind for all the following steps and chapters.

Step 1 - Create a new Pulumi project

Similar to the previous chapter, we will create a new Pulumi project. This time, I will use the TypeScript language, but you can use any other language you want.

pulumi new azure-typescript --force

I use the --force flag as I want to create the project in the existing directory we created in the previous step.

As we are going to build a container, we need the Pulumi Docker provider. You can add it to your project by running the following command:

npm install @pulumi/docker --save

And as we work with the Microsoft Entra ID, we also need the Azure Active Directory provider:

npm install @pulumi/azuread --save

If you are using a different language, you can find the installation instructions for the Docker provider and the Azure Active Directory provider in the Pulumi registry.

You can delete the content of the index.ts file and replace it with the following content:

import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";
import * as azuread from "@pulumi/azuread";
import * as docker from "@pulumi/docker";


const resourceGroup = new azure_native.resources.ResourceGroup("resourceGroup", {});
YAML
name: backstage-azure-infrastructure-yaml
runtime: yaml
description: A minimal Azure Native Pulumi YAML program

    resources:
    resourceGroup:
      type: azure-native:resources:ResourceGroup
Go
package main

    import (
        "fmt"
    
        "github.com/pulumi/pulumi-azure-native-sdk/authorization/v2"
        "github.com/pulumi/pulumi-azure-native-sdk/containerregistry/v2"
        "github.com/pulumi/pulumi-azure-native-sdk/resources/v2"
        "github.com/pulumi/pulumi-azuread/sdk/v5/go/azuread"
        "github.com/pulumi/pulumi-docker/sdk/v4/go/docker"
        "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
        pulumi.Run(func(ctx *pulumi.Context) error {
    
            resourceGroup, err := resources.NewResourceGroup(ctx, "resourceGroup", nil)
            if err != nil {
                return err
            }
    
            return nil
        })
    }
Python
    import pulumi
    import pulumi_azure_native as azure_native
    import pulumi_azuread as azuread
    import pulumi_docker as docker
    
    resource_group = azure_native.resources.ResourceGroup("resourceGroup")
  


As the next step, we will create an Azure Container Registry. This will be the place where we will push our container to.

import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";
import * as azuread from "@pulumi/azuread";
import * as docker from "@pulumi/docker";

const roleDefinitionIds = [
    "b24988ac-6180-42a0-ab88-20f7382dd24c",
    "acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "7f951dda-4ed3-4680-a7ca-43fe172d538d",
];
const resourceGroup = new azure_native.resources.ResourceGroup("resourceGroup", {});
const backstageContainerRegistry = new azure_native.containerregistry.Registry("backstageContainerRegistry", {
    resourceGroupName: resourceGroup.name,
    registryName: "pulumibackstage",
    sku: {
        name: "Basic",
    },
    identity: {
        type: azure_native.containerregistry.ResourceIdentityType.SystemAssigned,
    },
    adminUserEnabled: false,
});
YAML
      backstageContainerRegistry:
        type: azure-native:containerregistry:Registry
        properties:
          resourceGroupName: ${resourceGroup.name}
          registryName: pulumibackstage
          sku:
            name: Basic
          identity:
            type: SystemAssigned
          adminUserEnabled: false
  
Go
    package main
    
    import (
        "fmt"
    
        "github.com/pulumi/pulumi-azure-native-sdk/authorization/v2"
        "github.com/pulumi/pulumi-azure-native-sdk/containerregistry/v2"
        "github.com/pulumi/pulumi-azure-native-sdk/resources/v2"
        "github.com/pulumi/pulumi-azuread/sdk/v5/go/azuread"
        "github.com/pulumi/pulumi-docker/sdk/v4/go/docker"
        "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
        pulumi.Run(func(ctx *pulumi.Context) error {
            resourceGroup, err := resources.NewResourceGroup(ctx, "resourceGroup", nil)
            if err != nil {
                return err
            }
            backstageContainerRegistry, err := containerregistry.NewRegistry(ctx, "backstageContainerRegistry", &containerregistry.RegistryArgs{
                ResourceGroupName: resourceGroup.Name,
                RegistryName:      pulumi.String("pulumibackstage"),
                Sku: &containerregistry.SkuArgs{
                    Name: pulumi.String("Basic"),
                },
                Identity: &containerregistry.IdentityPropertiesArgs{
                    Type: containerregistry.ResourceIdentityTypeSystemAssigned,
                },
                AdminUserEnabled: pulumi.Bool(false),
            })
            if err != nil {
                return err
            }
            
            return nil
        })
    }
  
Python
    import pulumi
    import pulumi_azure_native as azure_native
    import pulumi_azuread as azuread
    import pulumi_docker as docker
    
    role_definition_ids = [
        "b24988ac-6180-42a0-ab88-20f7382dd24c",
        "acdd72a7-3385-48ef-bd42-f606fba81ae7",
        "7f951dda-4ed3-4680-a7ca-43fe172d538d",
    ]
    resource_group = azure_native.resources.ResourceGroup("resourceGroup")
    backstage_container_registry = azure_native.containerregistry.Registry("backstageContainerRegistry",
        resource_group_name=resource_group.name,
        registry_name="pulumibackstage",
        sku=azure_native.containerregistry.SkuArgs(
            name="Basic",
        ),
        identity=azure_native.containerregistry.IdentityPropertiesArgs(
            type=azure_native.containerregistry.ResourceIdentityType.SYSTEM_ASSIGNED,
        ),
        admin_user_enabled=False)
  


Now we need to create a service principal for the Azure Container Registry. This will be used to authenticate with the registry. The service principal will also have some roles assigned to it.

import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";
import * as azuread from "@pulumi/azuread";
import * as docker from "@pulumi/docker";

const roleDefinitionIds = [
    "b24988ac-6180-42a0-ab88-20f7382dd24c",
    "acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "7f951dda-4ed3-4680-a7ca-43fe172d538d",
];
const resourceGroup = new azure_native.resources.ResourceGroup("resourceGroup", {});
const backstageContainerRegistry = new azure_native.containerregistry.Registry("backstageContainerRegistry", {
    resourceGroupName: resourceGroup.name,
    registryName: "pulumibackstage",
    sku: {
        name: "Basic",
    },
    identity: {
        type: azure_native.containerregistry.ResourceIdentityType.SystemAssigned,
    },
    adminUserEnabled: false,
});
const contributorRoleDefinition = azure_native.authorization.getRoleDefinitionOutput({
    roleDefinitionId: roleDefinitionIds[0],
    scope: backstageContainerRegistry.id,
});
const readerRoleDefinition = azure_native.authorization.getRoleDefinitionOutput({
    roleDefinitionId: roleDefinitionIds[1],
    scope: backstageContainerRegistry.id,
});
const acrPullRoleDefinition = azure_native.authorization.getRoleDefinitionOutput({
    roleDefinitionId: roleDefinitionIds[2],
    scope: backstageContainerRegistry.id,
});
const backstageAzureApplication = new azuread.Application("backstageAzureApplication", {displayName: "backstage"});
const backstageAzureServicePrincipal = new azuread.ServicePrincipal("backstageAzureServicePrincipal", {
    applicationId: backstageAzureApplication.applicationId,
    tags: ["backstage"],
});
const backstageAzureApplicationPassword = new azuread.ApplicationPassword("backstageAzureApplicationPassword", {
    applicationObjectId: backstageAzureApplication.objectId,
    endDate: "2099-01-01T00:00:00Z",
});
const roleAssignment0 = new azure_native.authorization.RoleAssignment("roleAssignment0", {
    principalId: backstageAzureServicePrincipal.objectId,
    roleDefinitionId: contributorRoleDefinition.apply(contributorRoleDefinition => contributorRoleDefinition.id),
    scope: backstageContainerRegistry.id,
    principalType: "ServicePrincipal",
});
const roleAssignment1 = new azure_native.authorization.RoleAssignment("roleAssignment1", {
    principalId: backstageAzureServicePrincipal.objectId,
    roleDefinitionId: readerRoleDefinition.apply(readerRoleDefinition => readerRoleDefinition.id),
    scope: backstageContainerRegistry.id,
    principalType: "ServicePrincipal",
});
const roleAssignment2 = new azure_native.authorization.RoleAssignment("roleAssignment2", {
    principalId: backstageAzureServicePrincipal.objectId,
    roleDefinitionId: acrPullRoleDefinition.apply(acrPullRoleDefinition => acrPullRoleDefinition.id),
    scope: backstageContainerRegistry.id,
    principalType: "ServicePrincipal",
});
YAML
    variables:
      roleDefinitionIds:
      - b24988ac-6180-42a0-ab88-20f7382dd24c
      - acdd72a7-3385-48ef-bd42-f606fba81ae7
      - 7f951dda-4ed3-4680-a7ca-43fe172d538d
    
      contributorRoleDefinition:
        fn::invoke:
          function: azure-native:authorization:getRoleDefinition
          arguments:
            roleDefinitionId: ${roleDefinitionIds[0]}
            scope: ${backstageContainerRegistry.id}
    
      readerRoleDefinition:
        fn::invoke:
          function: azure-native:authorization:getRoleDefinition
          arguments:
            roleDefinitionId: ${roleDefinitionIds[1]}
            scope: ${backstageContainerRegistry.id}
    
      acrPullRoleDefinition:
        fn::invoke:
          function: azure-native:authorization:getRoleDefinition
          arguments:
            roleDefinitionId: ${roleDefinitionIds[2]}
            scope: ${backstageContainerRegistry.id}
    
    resources:
      resourceGroup:
        type: azure-native:resources:ResourceGroup
    
      backstageAzureApplication:
        type: azuread:Application
        properties:
          displayName: "backstage"
    
      backstageAzureServicePrincipal:
        type: azuread:ServicePrincipal
        properties:
          applicationId: ${backstageAzureApplication.applicationId}
          tags: ["backstage"]
    
      backstageAzureApplicationPassword:
        type: azuread:ApplicationPassword
        properties:
          applicationObjectId: ${backstageAzureApplication.objectId}
          endDate: "2099-01-01T00:00:00Z"
    
    
      backstageContainerRegistry:
        type: azure-native:containerregistry:Registry
        properties:
          resourceGroupName: ${resourceGroup.name}
          registryName: pulumibackstage
          sku:
            name: Basic
          identity:
            type: SystemAssigned
          adminUserEnabled: false
    
      roleAssignment0:
        type: azure-native:authorization:RoleAssignment
        properties:
          principalId: ${backstageAzureServicePrincipal.objectId}
          roleDefinitionId: ${contributorRoleDefinition.id}
          scope: ${backstageContainerRegistry.id}
          principalType: ServicePrincipal
    
      roleAssignment1:
        type: azure-native:authorization:RoleAssignment
        properties:
          principalId: ${backstageAzureServicePrincipal.objectId}
          roleDefinitionId: ${readerRoleDefinition.id}
          scope: ${backstageContainerRegistry.id}
          principalType: ServicePrincipal
    
      roleAssignment2:
        type: azure-native:authorization:RoleAssignment
        properties:
          principalId: ${backstageAzureServicePrincipal.objectId}
          roleDefinitionId: ${acrPullRoleDefinition.id}
          scope: ${backstageContainerRegistry.id}
          principalType: ServicePrincipal
  
Go
    package main

    import (
        "fmt"
    
        "github.com/pulumi/pulumi-azure-native-sdk/authorization/v2"
        "github.com/pulumi/pulumi-azure-native-sdk/containerregistry/v2"
        "github.com/pulumi/pulumi-azure-native-sdk/resources/v2"
        "github.com/pulumi/pulumi-azuread/sdk/v5/go/azuread"
        "github.com/pulumi/pulumi-docker/sdk/v4/go/docker"
        "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
        pulumi.Run(func(ctx *pulumi.Context) error {
            roleDefinitionIds := []string{
                "b24988ac-6180-42a0-ab88-20f7382dd24c",
                "acdd72a7-3385-48ef-bd42-f606fba81ae7",
                "7f951dda-4ed3-4680-a7ca-43fe172d538d",
            }
            resourceGroup, err := resources.NewResourceGroup(ctx, "resourceGroup", nil)
            if err != nil {
                return err
            }
            backstageContainerRegistry, err := containerregistry.NewRegistry(ctx, "backstageContainerRegistry", &containerregistry.RegistryArgs{
                ResourceGroupName: resourceGroup.Name,
                RegistryName:      pulumi.String("pulumibackstage"),
                Sku: &containerregistry.SkuArgs{
                    Name: pulumi.String("Basic"),
                },
                Identity: &containerregistry.IdentityPropertiesArgs{
                    Type: containerregistry.ResourceIdentityTypeSystemAssigned,
                },
                AdminUserEnabled: pulumi.Bool(false),
            })
            if err != nil {
                return err
            }
            contributorRoleDefinition := authorization.LookupRoleDefinitionOutput(ctx, authorization.LookupRoleDefinitionOutputArgs{
                RoleDefinitionId: pulumi.String(roleDefinitionIds[0]),
                Scope:            backstageContainerRegistry.ID(),
            }, nil)
            readerRoleDefinition := authorization.LookupRoleDefinitionOutput(ctx, authorization.LookupRoleDefinitionOutputArgs{
                RoleDefinitionId: pulumi.String(roleDefinitionIds[1]),
                Scope:            backstageContainerRegistry.ID(),
            }, nil)
            acrPullRoleDefinition := authorization.LookupRoleDefinitionOutput(ctx, authorization.LookupRoleDefinitionOutputArgs{
                RoleDefinitionId: pulumi.String(roleDefinitionIds[2]),
                Scope:            backstageContainerRegistry.ID(),
            }, nil)
            backstageAzureApplication, err := azuread.NewApplication(ctx, "backstageAzureApplication", &azuread.ApplicationArgs{
                DisplayName: pulumi.String("backstage"),
            })
            if err != nil {
                return err
            }
            backstageAzureServicePrincipal, err := azuread.NewServicePrincipal(ctx, "backstageAzureServicePrincipal", &azuread.ServicePrincipalArgs{
                ApplicationId: backstageAzureApplication.ApplicationId,
                Tags: pulumi.StringArray{
                    pulumi.String("backstage"),
                },
            })
            if err != nil {
                return err
            }
            backstageAzureApplicationPassword, err := azuread.NewApplicationPassword(ctx, "backstageAzureApplicationPassword", &azuread.ApplicationPasswordArgs{
                ApplicationObjectId: backstageAzureApplication.ObjectId,
                EndDate:             pulumi.String("2099-01-01T00:00:00Z"),
            })
            if err != nil {
                return err
            }
            _, err = authorization.NewRoleAssignment(ctx, "roleAssignment0", &authorization.RoleAssignmentArgs{
                PrincipalId:      backstageAzureServicePrincipal.ObjectId,
                RoleDefinitionId: contributorRoleDefinition.Id(),
                Scope:            backstageContainerRegistry.ID(),
                PrincipalType:    pulumi.String("ServicePrincipal"),
            })
            if err != nil {
                return err
            }
            _, err = authorization.NewRoleAssignment(ctx, "roleAssignment1", &authorization.RoleAssignmentArgs{
                PrincipalId:      backstageAzureServicePrincipal.ObjectId,
                RoleDefinitionId: readerRoleDefinition.Id(),
                Scope:            backstageContainerRegistry.ID(),
                PrincipalType:    pulumi.String("ServicePrincipal"),
            })
            if err != nil {
                return err
            }
            _, err = authorization.NewRoleAssignment(ctx, "roleAssignment2", &authorization.RoleAssignmentArgs{
                PrincipalId:      backstageAzureServicePrincipal.ObjectId,
                RoleDefinitionId: acrPullRoleDefinition.Id(),
                Scope:            backstageContainerRegistry.ID(),
                PrincipalType:    pulumi.String("ServicePrincipal"),
            })
            if err != nil {
                return err
            }
            return nil
        })
    }
  
Python
    import pulumi
    import pulumi_azure_native as azure_native
    import pulumi_azuread as azuread
    import pulumi_docker as docker
    
    role_definition_ids = [
        "b24988ac-6180-42a0-ab88-20f7382dd24c",
        "acdd72a7-3385-48ef-bd42-f606fba81ae7",
        "7f951dda-4ed3-4680-a7ca-43fe172d538d",
    ]
    resource_group = azure_native.resources.ResourceGroup("resourceGroup")
    backstage_container_registry = azure_native.containerregistry.Registry("backstageContainerRegistry",
                                                                           resource_group_name=resource_group.name,
                                                                           registry_name="pulumibackstage",
                                                                           sku=azure_native.containerregistry.SkuArgs(
                                                                               name="Basic",
                                                                           ),
                                                                           identity=azure_native.containerregistry.IdentityPropertiesArgs(
                                                                               type=azure_native.containerregistry.ResourceIdentityType.SYSTEM_ASSIGNED,
                                                                           ),
                                                                           admin_user_enabled=False)
    contributor_role_definition = azure_native.authorization.get_role_definition_output(role_definition_id=role_definition_ids[0],
                                                                                        scope=backstage_container_registry.id)
    reader_role_definition = azure_native.authorization.get_role_definition_output(role_definition_id=role_definition_ids[1],
                                                                                   scope=backstage_container_registry.id)
    acr_pull_role_definition = azure_native.authorization.get_role_definition_output(role_definition_id=role_definition_ids[2],
                                                                                     scope=backstage_container_registry.id)
    backstage_azure_application = azuread.Application("backstageAzureApplication", display_name="backstage")
    backstage_azure_service_principal = azuread.ServicePrincipal("backstageAzureServicePrincipal",
                                                                 application_id=backstage_azure_application.application_id,
                                                                 tags=["backstage"])
    backstage_azure_application_password = azuread.ApplicationPassword("backstageAzureApplicationPassword",
                                                                       application_object_id=backstage_azure_application.object_id,
                                                                       end_date="2099-01-01T00:00:00Z")
    role_assignment0 = azure_native.authorization.RoleAssignment("roleAssignment0",
                                                                 principal_id=backstage_azure_service_principal.object_id,
                                                                 role_definition_id=contributor_role_definition.id,
                                                                 scope=backstage_container_registry.id,
                                                                 principal_type="ServicePrincipal")
    role_assignment1 = azure_native.authorization.RoleAssignment("roleAssignment1",
                                                                 principal_id=backstage_azure_service_principal.object_id,
                                                                 role_definition_id=reader_role_definition.id,
                                                                 scope=backstage_container_registry.id,
                                                                 principal_type="ServicePrincipal")
    role_assignment2 = azure_native.authorization.RoleAssignment("roleAssignment2",
                                                                 principal_id=backstage_azure_service_principal.object_id,
                                                                 role_definition_id=acr_pull_role_definition.id,
                                                                 scope=backstage_container_registry.id,
                                                                 principal_type="ServicePrincipal")
  


The next step is to build the container and push it to the Azure Container Registry. We will use the docker.Image resource from the Pulumi Docker provider to build the container and push it to the registry.

We also going to output the repoDigest from the docker.Image resource. We will need this in the next chapter to deploy the Backstage instance to Azure.

import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";
import * as azuread from "@pulumi/azuread";
import * as docker from "@pulumi/docker";

const roleDefinitionIds = [
    "b24988ac-6180-42a0-ab88-20f7382dd24c",
    "acdd72a7-3385-48ef-bd42-f606fba81ae7",
    "7f951dda-4ed3-4680-a7ca-43fe172d538d",
];
const resourceGroup = new azure_native.resources.ResourceGroup("resourceGroup", {});
const backstageContainerRegistry = new azure_native.containerregistry.Registry("backstageContainerRegistry", {
    resourceGroupName: resourceGroup.name,
    registryName: "pulumibackstage",
    sku: {
        name: "Basic",
    },
    identity: {
        type: azure_native.containerregistry.ResourceIdentityType.SystemAssigned,
    },
    adminUserEnabled: false,
});
const contributorRoleDefinition = azure_native.authorization.getRoleDefinitionOutput({
    roleDefinitionId: roleDefinitionIds[0],
    scope: backstageContainerRegistry.id,
});
const readerRoleDefinition = azure_native.authorization.getRoleDefinitionOutput({
    roleDefinitionId: roleDefinitionIds[1],
    scope: backstageContainerRegistry.id,
});
const acrPullRoleDefinition = azure_native.authorization.getRoleDefinitionOutput({
    roleDefinitionId: roleDefinitionIds[2],
    scope: backstageContainerRegistry.id,
});
const backstageAzureApplication = new azuread.Application("backstageAzureApplication", {displayName: "backstage"});
const backstageAzureServicePrincipal = new azuread.ServicePrincipal("backstageAzureServicePrincipal", {
    applicationId: backstageAzureApplication.applicationId,
    tags: ["backstage"],
});
const backstageAzureApplicationPassword = new azuread.ApplicationPassword("backstageAzureApplicationPassword", {
    applicationObjectId: backstageAzureApplication.objectId,
    endDate: "2099-01-01T00:00:00Z",
});
const roleAssignment0 = new azure_native.authorization.RoleAssignment("roleAssignment0", {
    principalId: backstageAzureServicePrincipal.objectId,
    roleDefinitionId: contributorRoleDefinition.apply(contributorRoleDefinition => contributorRoleDefinition.id),
    scope: backstageContainerRegistry.id,
    principalType: "ServicePrincipal",
});
const roleAssignment1 = new azure_native.authorization.RoleAssignment("roleAssignment1", {
    principalId: backstageAzureServicePrincipal.objectId,
    roleDefinitionId: readerRoleDefinition.apply(readerRoleDefinition => readerRoleDefinition.id),
    scope: backstageContainerRegistry.id,
    principalType: "ServicePrincipal",
});
const roleAssignment2 = new azure_native.authorization.RoleAssignment("roleAssignment2", {
    principalId: backstageAzureServicePrincipal.objectId,
    roleDefinitionId: acrPullRoleDefinition.apply(acrPullRoleDefinition => acrPullRoleDefinition.id),
    scope: backstageContainerRegistry.id,
    principalType: "ServicePrincipal",
});
const backstageImage = new docker.Image("backstageImage", {
    build: {
        context: "../backstage",
        platform: "linux/amd64",
        builderVersion: docker.BuilderVersion.BuilderBuildKit,
        dockerfile: "../backstage/packages/backend/Dockerfile",
    },
    imageName: pulumi.interpolate`${backstageContainerRegistry.loginServer}/backstage`,
    registry: {
        server: backstageContainerRegistry.loginServer,
        username: backstageAzureServicePrincipal.applicationId,
        password: backstageAzureApplicationPassword.value,
    },
});
export const repoDigest = backstageImage.repoDigest;
YAML
          backstageImage:
          type: docker:Image
          properties:
            build:
              context: "../backstage"
              platform: "linux/amd64"
              builderVersion: BuilderBuildKit
              dockerfile: "../backstage/packages/backend/Dockerfile"
            imageName: ${backstageContainerRegistry.loginServer}/backstage
            registry:
              server: ${backstageContainerRegistry.loginServer}
              username: ${backstageAzureServicePrincipal.applicationId}
              password: ${backstageAzureApplicationPassword.value}

      outputs:
        repoDigest: ${backstageImage.repoDigest}
  
Go
    package main

    import (
        "fmt"
    
        "github.com/pulumi/pulumi-azure-native-sdk/authorization/v2"
        "github.com/pulumi/pulumi-azure-native-sdk/containerregistry/v2"
        "github.com/pulumi/pulumi-azure-native-sdk/resources/v2"
        "github.com/pulumi/pulumi-azuread/sdk/v5/go/azuread"
        "github.com/pulumi/pulumi-docker/sdk/v4/go/docker"
        "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
        pulumi.Run(func(ctx *pulumi.Context) error {
            roleDefinitionIds := []string{
                "b24988ac-6180-42a0-ab88-20f7382dd24c",
                "acdd72a7-3385-48ef-bd42-f606fba81ae7",
                "7f951dda-4ed3-4680-a7ca-43fe172d538d",
            }
            resourceGroup, err := resources.NewResourceGroup(ctx, "resourceGroup", nil)
            if err != nil {
                return err
            }
            backstageContainerRegistry, err := containerregistry.NewRegistry(ctx, "backstageContainerRegistry", &containerregistry.RegistryArgs{
                ResourceGroupName: resourceGroup.Name,
                RegistryName:      pulumi.String("pulumibackstage"),
                Sku: &containerregistry.SkuArgs{
                    Name: pulumi.String("Basic"),
                },
                Identity: &containerregistry.IdentityPropertiesArgs{
                    Type: containerregistry.ResourceIdentityTypeSystemAssigned,
                },
                AdminUserEnabled: pulumi.Bool(false),
            })
            if err != nil {
                return err
            }
            contributorRoleDefinition := authorization.LookupRoleDefinitionOutput(ctx, authorization.LookupRoleDefinitionOutputArgs{
                RoleDefinitionId: pulumi.String(roleDefinitionIds[0]),
                Scope:            backstageContainerRegistry.ID(),
            }, nil)
            readerRoleDefinition := authorization.LookupRoleDefinitionOutput(ctx, authorization.LookupRoleDefinitionOutputArgs{
                RoleDefinitionId: pulumi.String(roleDefinitionIds[1]),
                Scope:            backstageContainerRegistry.ID(),
            }, nil)
            acrPullRoleDefinition := authorization.LookupRoleDefinitionOutput(ctx, authorization.LookupRoleDefinitionOutputArgs{
                RoleDefinitionId: pulumi.String(roleDefinitionIds[2]),
                Scope:            backstageContainerRegistry.ID(),
            }, nil)
            backstageAzureApplication, err := azuread.NewApplication(ctx, "backstageAzureApplication", &azuread.ApplicationArgs{
                DisplayName: pulumi.String("backstage"),
            })
            if err != nil {
                return err
            }
            backstageAzureServicePrincipal, err := azuread.NewServicePrincipal(ctx, "backstageAzureServicePrincipal", &azuread.ServicePrincipalArgs{
                ApplicationId: backstageAzureApplication.ApplicationId,
                Tags: pulumi.StringArray{
                    pulumi.String("backstage"),
                },
            })
            if err != nil {
                return err
            }
            backstageAzureApplicationPassword, err := azuread.NewApplicationPassword(ctx, "backstageAzureApplicationPassword", &azuread.ApplicationPasswordArgs{
                ApplicationObjectId: backstageAzureApplication.ObjectId,
                EndDate:             pulumi.String("2099-01-01T00:00:00Z"),
            })
            if err != nil {
                return err
            }
            _, err = authorization.NewRoleAssignment(ctx, "roleAssignment0", &authorization.RoleAssignmentArgs{
                PrincipalId:      backstageAzureServicePrincipal.ObjectId,
                RoleDefinitionId: contributorRoleDefinition.Id(),
                Scope:            backstageContainerRegistry.ID(),
                PrincipalType:    pulumi.String("ServicePrincipal"),
            })
            if err != nil {
                return err
            }
            _, err = authorization.NewRoleAssignment(ctx, "roleAssignment1", &authorization.RoleAssignmentArgs{
                PrincipalId:      backstageAzureServicePrincipal.ObjectId,
                RoleDefinitionId: readerRoleDefinition.Id(),
                Scope:            backstageContainerRegistry.ID(),
                PrincipalType:    pulumi.String("ServicePrincipal"),
            })
            if err != nil {
                return err
            }
            _, err = authorization.NewRoleAssignment(ctx, "roleAssignment2", &authorization.RoleAssignmentArgs{
                PrincipalId:      backstageAzureServicePrincipal.ObjectId,
                RoleDefinitionId: acrPullRoleDefinition.Id(),
                Scope:            backstageContainerRegistry.ID(),
                PrincipalType:    pulumi.String("ServicePrincipal"),
            })
            if err != nil {
                return err
            }
            backstageImage, err := docker.NewImage(ctx, "backstageImage", &docker.ImageArgs{
                Build: &docker.DockerBuildArgs{
                    Context:        pulumi.String("../../backstage"),
                    Platform:       pulumi.String("linux/amd64"),
                    BuilderVersion: docker.BuilderVersionBuilderBuildKit,
                    Dockerfile:     pulumi.String("../../backstage/packages/backend/Dockerfile"),
                },
                ImageName: backstageContainerRegistry.LoginServer.ApplyT(func(loginServer string) (string, error) {
                    return fmt.Sprintf("%v/backstage", loginServer), nil
                }).(pulumi.StringOutput),
                Registry: &docker.RegistryArgs{
                    Server:   backstageContainerRegistry.LoginServer,
                    Username: backstageAzureServicePrincipal.ApplicationId,
                    Password: backstageAzureApplicationPassword.Value,
                },
            })
            if err != nil {
                return err
            }
            ctx.Export("repoDigest", backstageImage.RepoDigest)
            return nil
        })
    }
  
Python
    import pulumi
    import pulumi_azure_native as azure_native
    import pulumi_azuread as azuread
    import pulumi_docker as docker
    
    role_definition_ids = [
        "b24988ac-6180-42a0-ab88-20f7382dd24c",
        "acdd72a7-3385-48ef-bd42-f606fba81ae7",
        "7f951dda-4ed3-4680-a7ca-43fe172d538d",
    ]
    resource_group = azure_native.resources.ResourceGroup("resourceGroup")
    backstage_container_registry = azure_native.containerregistry.Registry("backstageContainerRegistry",
                                                                           resource_group_name=resource_group.name,
                                                                           registry_name="pulumibackstage",
                                                                           sku=azure_native.containerregistry.SkuArgs(
                                                                               name="Basic",
                                                                           ),
                                                                           identity=azure_native.containerregistry.IdentityPropertiesArgs(
                                                                               type=azure_native.containerregistry.ResourceIdentityType.SYSTEM_ASSIGNED,
                                                                           ),
                                                                           admin_user_enabled=False)
    contributor_role_definition = azure_native.authorization.get_role_definition_output(role_definition_id=role_definition_ids[0],
                                                                                        scope=backstage_container_registry.id)
    reader_role_definition = azure_native.authorization.get_role_definition_output(role_definition_id=role_definition_ids[1],
                                                                                   scope=backstage_container_registry.id)
    acr_pull_role_definition = azure_native.authorization.get_role_definition_output(role_definition_id=role_definition_ids[2],
                                                                                     scope=backstage_container_registry.id)
    backstage_azure_application = azuread.Application("backstageAzureApplication", display_name="backstage")
    backstage_azure_service_principal = azuread.ServicePrincipal("backstageAzureServicePrincipal",
                                                                 application_id=backstage_azure_application.application_id,
                                                                 tags=["backstage"])
    backstage_azure_application_password = azuread.ApplicationPassword("backstageAzureApplicationPassword",
                                                                       application_object_id=backstage_azure_application.object_id,
                                                                       end_date="2099-01-01T00:00:00Z")
    role_assignment0 = azure_native.authorization.RoleAssignment("roleAssignment0",
                                                                 principal_id=backstage_azure_service_principal.object_id,
                                                                 role_definition_id=contributor_role_definition.id,
                                                                 scope=backstage_container_registry.id,
                                                                 principal_type="ServicePrincipal")
    role_assignment1 = azure_native.authorization.RoleAssignment("roleAssignment1",
                                                                 principal_id=backstage_azure_service_principal.object_id,
                                                                 role_definition_id=reader_role_definition.id,
                                                                 scope=backstage_container_registry.id,
                                                                 principal_type="ServicePrincipal")
    role_assignment2 = azure_native.authorization.RoleAssignment("roleAssignment2",
                                                                 principal_id=backstage_azure_service_principal.object_id,
                                                                 role_definition_id=acr_pull_role_definition.id,
                                                                 scope=backstage_container_registry.id,
                                                                 principal_type="ServicePrincipal")
    backstage_image = docker.Image("backstageImage",
                                   build=docker.DockerBuildArgs(
                                       context="../backstage",
                                       platform="linux/amd64",
                                       builder_version=docker.BuilderVersion.BUILDER_BUILD_KIT,
                                       dockerfile="../backstage/packages/backend/Dockerfile",
                                   ),
                                   image_name=backstage_container_registry.login_server.apply(lambda login_server: f"{login_server}/backstage"),
                                   registry=docker.RegistryArgs(
                                       server=backstage_container_registry.login_server,
                                       username=backstage_azure_service_principal.application_id,
                                       password=backstage_azure_application_password.value,
                                   ))
    pulumi.export("repoDigest", backstage_image.repo_digest)
  

Step 2 - Run Pulumi Up

Before you can run pulumi up, you need to be sure that your Azure credentials are set.

pulumi up

This command will show you a preview of all the resources and asks you if you want to deploy them. You can run dedicated commands to see the preview or to deploy the resources.

pulumi preview
# or
pulumi up

You should see the following output:

...
Outputs:
  + repoDigest: "pulumibackstage.azurecr.io/backstage@sha256:38e6095a50f677b87ae95defe00bd0b2deff7c741ea74ec8fcfb46a6d014c56b"

This is the repoDigest of your container!

Stretch Goals

Learn more