Use Dagger with Azure Pipelines and Azure Container Instances
Introduction
This tutorial teaches you how to use Dagger to continuously build and deploy a Node.js application to Azure Container Instances with Azure Pipelines. You will learn how to:
- Configure an Azure resource group and service principal
- Create a Dagger pipeline using a Dagger SDK
- Run the Dagger pipeline on your local host to manually build and deploy the application to Azure Container Instances
- Use the same Dagger pipeline with Azure Pipelines to automatically build and deploy the application to Azure Container Instances on every repository commit
Requirements
This tutorial assumes that:
- You have a basic understanding of the JavaScript programming language.
- You have a basic understanding of Azure DevOps and Azure Container Instances. If not, learn about Azure DevOps and Azure Container Instances.
- You have a Go, Python or Node.js development environment. If not, install Go, Python or Node.js.
- You have Docker installed and running on the host system. If not, install Docker.
- You have the Dagger CLI installed in your development environment. If not, install the Dagger CLI.
- You have the Azure CLI installed. If not, install the Azure CLI.
- You have a Docker Hub account. If not, register for a free Docker Hub account.
- You have an Azure subscription with "Owner" (or higher) privileges. If not, register for an Azure account.
- You have an Azure DevOps project containing a Node.js Web application. This repository should also be cloned locally in your development environment. If not, follow the steps in Appendix A to create and populate a local and Azure DevOps repository with an example Express application.
Step 1: Create an Azure resource group and service principal
The first step is to create an Azure resource group for the container instance, as well as an Azure service principal for the Dagger pipeline.
-
Log in to Azure using the Azure CLI:
az login
-
Create a new Azure resource group (in this example, a group named
mygroup
in theuseast
location):az group create --location eastus --name my-group
Note the resource group ID (
id
field) in the output, as you will need it when creating the service principal. -
Create a service principal for the application (here, a principal named
mydaggerprincipal
) and assign it the "Contributor" role. Replace theRESOURCE-GROUP-ID
placeholder in the command with the resource group ID obtained from the previous command.az ad sp create-for-rbac --name my-dagger-principal --role Contributor --scopes RESOURCE-GROUP-ID
infoThe "Contributor" role gives the service principal access to manage all resources in the group, including container instances.
The output of the previous command contains the credentials for the service principal, including the client ID (
appId
field), tenant ID (tenant
field) and client secret (password
field). Note these values carefully, as they will not be shown again and you will need them in subsequent steps.
Step 2: Create the Dagger pipeline
The next step is to create a Dagger pipeline to do the heavy lifting: build a container image of the application, release it to Docker Hub and deploy it on Azure Container Instances using the service principal from the previous step.
- Go
- Node.js
- Python
-
In the application directory, install the Dagger SDK and the Azure SDK client libraries:
go mod init main
go get dagger.io/dagger@latest
go get github.com/Azure/azure-sdk-for-go/sdk/azcore
go get github.com/Azure/azure-sdk-for-go/sdk/azidentity
go get github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerinstance/armcontainerinstance/v2 -
Create a new sub-directory named
ci
. Within theci
directory, create a file namedmain.go
and add the following code to it. Modify the region (useast
) and resource group name (my-group
) if you specified different values when creating the Azure resource group in Step 1.package main
import (
"context"
"fmt"
"log"
"os"
"dagger.io/dagger"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerinstance/armcontainerinstance/v2"
)
func main() {
// configure container group, name and location
containerName := "my-app"
containerGroupName := "my-app"
containerGroupLocation := "eastus"
resourceGroupName := "my-group"
// check for required variables in host environment
vars := []string{"DOCKERHUB_USERNAME", "DOCKERHUB_PASSWORD", "AZURE_SUBSCRIPTION_ID", "AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"}
for _, v := range vars {
if os.Getenv(v) == "" {
log.Fatalf("Environment variable %s is not set", v)
}
}
// initialize Dagger client
ctx := context.Background()
daggerClient, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stderr))
if err != nil {
panic(err)
}
defer daggerClient.Close()
// set registry password as Dagger secret
dockerHubPassword := daggerClient.SetSecret("dockerHubPassword", os.Getenv("DOCKERHUB_PASSWORD"))
// get reference to the project directory
source := daggerClient.Host().Directory(".", dagger.HostDirectoryOpts{
Exclude: []string{"ci", "node_modules"},
})
// get Node image
node := daggerClient.Container(dagger.ContainerOpts{Platform: "linux/amd64"}).
From("node:18")
// mount source code directory into Node image
// install dependencies
// set entrypoint
ctr := node.WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"cp", "-R", ".", "/home/node"}).
WithWorkdir("/home/node").
WithExec([]string{"npm", "install"}).
WithEntrypoint([]string{"npm", "start"})
// publish image
dockerHubUsername := os.Getenv("DOCKERHUB_USERNAME")
addr, err := ctr.WithRegistryAuth("docker.io", dockerHubUsername, dockerHubPassword).
Publish(ctx, fmt.Sprintf("%s/my-app", dockerHubUsername))
if err != nil {
panic(err)
}
// print ref
fmt.Println("Published at:", addr)
// initialize Azure credentials
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
panic(err)
}
// initialize Azure client
azureClient, err := armcontainerinstance.NewClientFactory(os.Getenv("AZURE_SUBSCRIPTION_ID"), cred, nil)
if err != nil {
panic(err)
}
// define deployment request
containerGroup := armcontainerinstance.ContainerGroup{
Properties: &armcontainerinstance.ContainerGroupPropertiesProperties{
Containers: []*armcontainerinstance.Container{
{
Name: to.Ptr(containerName),
Properties: &armcontainerinstance.ContainerProperties{
Command: []*string{},
EnvironmentVariables: []*armcontainerinstance.EnvironmentVariable{},
Image: to.Ptr(addr),
Ports: []*armcontainerinstance.ContainerPort{
{
Port: to.Ptr[int32](3000),
}},
Resources: &armcontainerinstance.ResourceRequirements{
Requests: &armcontainerinstance.ResourceRequests{
CPU: to.Ptr[float64](1),
MemoryInGB: to.Ptr[float64](1.5),
},
},
},
}},
IPAddress: &armcontainerinstance.IPAddress{
Type: to.Ptr(armcontainerinstance.ContainerGroupIPAddressTypePublic),
Ports: []*armcontainerinstance.Port{
{
Port: to.Ptr[int32](3000),
Protocol: to.Ptr(armcontainerinstance.ContainerGroupNetworkProtocolTCP),
}},
},
OSType: to.Ptr(armcontainerinstance.OperatingSystemTypesLinux),
RestartPolicy: to.Ptr(armcontainerinstance.ContainerGroupRestartPolicyOnFailure),
},
Location: to.Ptr(containerGroupLocation),
}
poller, err := azureClient.NewContainerGroupsClient().BeginCreateOrUpdate(ctx, resourceGroupName, containerGroupName, containerGroup, nil)
if err != nil {
panic(err)
}
// send request and wait until done
res, err := poller.PollUntilDone(ctx, nil)
if err != nil {
panic(err)
}
fmt.Printf(
"Deployment for image %s now available at http://%s:%d\n",
addr,
*res.ContainerGroup.Properties.IPAddress.IP,
*res.ContainerGroup.Properties.IPAddress.Ports[0].Port,
)
}This file performs the following operations:
- It imports the Dagger and Azure SDK libraries.
- It checks for various required credentials in the host environment.
- It creates a Dagger client with
Connect()
. This client provides an interface for executing commands against the Dagger engine. - It uses the client's
SetSecret()
method to set the Docker Hub registry password as a secret for the Dagger pipeline. - It uses the client's
Host().Directory()
method to obtain a reference to the current directory on the host, excluding thenode_modules
andci
directories. This reference is stored in thesource
variable. - It uses the client's
Container().From()
method to initialize a new container from a base image. The additionalPlatform
argument to theContainer()
method instructs Dagger to build for a specific architecture. In this example, the base image is thenode:18
image and the architecture islinux/amd64
. This method returns aContainer
representing an OCI-compatible container image. - It uses the previous
Container
object'sWithDirectory()
method to return the container image with the host directory written at the/src
path, and theWithWorkdir()
method to set the working directory in the container image. - It chains the
WithExec()
method to copy the contents of the working directory to the/home/node
directory in the container image and then uses theWithWorkdir()
method to change the working directory in the container image to/home/node
. - It chains the
WithExec()
method again to install dependencies withnpm install
and sets the container entrypoint using theWithEntrypoint()
method. - It uses the container object's
WithRegistryAuth()
method to set the registry credentials (including the password set as a secret previously) and then invokes thePublish()
method to publish the container image to Docker Hub. It also prints the SHA identifier of the published image. - It creates an Azure client (using the Azure credentials set in the host environment)
- It defines a deployment request to create or update a container in the Azure Container Instances service. This deployment request includes the container name, image, port configuration, location and other details.
- It submits the deployment request to the Azure Container Instances service and waits for a response. If successful, it prints the public IP address of the running container image.
-
Run the following command to update
go.sum
:go mod tidy
-
In the application directory, install the Dagger SDK and the Azure SDK client libraries as development dependencies:
npm install @dagger.io/dagger@latest @azure/arm-containerinstance @azure/identity --save-dev
-
Create a new sub-directory named
ci
. Within theci
directory, create a file namedindex.mjs
and add the following code to it. Modify the region (useast
) and resource group name (my-group
) if you specified different values when creating the Azure resource group in Step 1.import { ContainerInstanceManagementClient } from "@azure/arm-containerinstance"
import { DefaultAzureCredential } from "@azure/identity"
import { connect } from "@dagger.io/dagger"
// check for required variables
const vars = [
"DOCKERHUB_USERNAME",
"DOCKERHUB_PASSWORD",
"AZURE_SUBSCRIPTION_ID",
"AZURE_TENANT_ID",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
]
vars.forEach((v) => {
if (!process.env[v]) {
console.log(`${v} variable must be set`)
process.exit()
}
})
// configure container group, name and location
const containerName = "my-app"
const containerGroupName = "my-app"
const containerGroupLocation = "eastus"
const resourceGroupName = "my-group"
// initialize Dagger client
connect(
async (daggerClient) => {
// set registry password as Dagger secret
const secret = daggerClient.setSecret(
"password",
process.env.DOCKERHUB_PASSWORD,
)
// get reference to the project directory
const source = daggerClient
.host()
.directory(".", { exclude: ["node_modules/", "ci/"] })
// get Node image
const node = daggerClient
.container({ platform: "linux/amd64" })
.from("node:18")
// mount cloned repository into Node image
// install dependencies
// set entrypoint
const ctr = node
.withDirectory("/src", source)
.withWorkdir("/src")
.withExec(["cp", "-R", ".", "/home/node"])
.withWorkdir("/home/node")
.withExec(["npm", "install"])
.withEntrypoint(["npm", "start"])
// publish image
const dockerHubUsername = process.env.DOCKERHUB_USERNAME
const address = await ctr
.withRegistryAuth("docker.io", dockerHubUsername, secret)
.publish(`${dockerHubUsername}/my-app`)
// print ref
console.log(`Published at: ${address}`)
// initialize Azure client
const azureClient = new ContainerInstanceManagementClient(
new DefaultAzureCredential(),
process.env.AZURE_SUBSCRIPTION_ID,
)
// define deployment request
const containerGroup = {
containers: [
{
name: containerName,
image: address,
ports: [{ port: 3000 }],
resources: { requests: { cpu: 1, memoryInGB: 1.5 } },
},
],
ipAddress: { type: "Public", ports: [{ port: 3000, protocol: "TCP" }] },
osType: "Linux",
location: containerGroupLocation,
restartPolicy: "OnFailure",
}
// send request and wait until done
const result = await azureClient.containerGroups.beginCreateOrUpdateAndWait(
resourceGroupName,
containerGroupName,
containerGroup,
)
// print ref
console.log(
`Deployment for image ${address} now available at http://${result.ipAddress.ip}:${result.ipAddress.ports[0].port}`,
)
},
{ LogOutput: process.stderr },
)This file performs the following operations:
- It imports the Dagger and Azure SDK libraries.
- It checks for various required credentials in the host environment.
- It creates a Dagger client with
connect()
. This client provides an interface for executing commands against the Dagger engine. - It uses the client's
setSecret()
method to set the Docker Hub registry password as a secret for the Dagger pipeline. - It uses the client's
host().directory()
method to obtain a reference to the current directory on the host, excluding thenode_modules
andci
directories. This reference is stored in thesource
variable. - It uses the client's
container().from()
method to initialize a new container from a base image. The additionalplatform
argument to thecontainer()
method instructs Dagger to build for a specific architecture. In this example, the base image is thenode:18
image and the architecture islinux/amd64
. This method returns aContainer
representing an OCI-compatible container image. - It uses the previous
Container
object'swithDirectory()
method to return the container image with the host directory written at the/src
path, and thewithWorkdir()
method to set the working directory in the container image. - It chains the
withExec()
method to copy the contents of the working directory to the/home/node
directory in the container image and then uses thewithWorkdir()
method to change the working directory in the container image to/home/node
. - It chains the
withExec()
method again to install dependencies withnpm install
and sets the container entrypoint using thewithEntrypoint()
method. - It uses the container object's
withRegistryAuth()
method to set the registry credentials (including the password set as a secret previously) and then invokes thepublish()
method to publish the container image to Docker Hub. It also prints the SHA identifier of the published image. - It creates an Azure client (using the Azure credentials set in the host environment)
- It defines a deployment request to create or update a container in the Azure Container Instances service. This deployment request includes the container name, image, port configuration, location and other details.
- It submits the deployment request to the Azure Container Instances service and waits for a response. If successful, it prints the public IP address of the running container image.
-
In the application directory, create a virtual environment and install the Dagger SDK and the Azure SDK client libraries:
pip install dagger-io aiohttp azure-identity azure-mgmt-containerinstance
-
Create a new sub-directory named
ci
. Within theci
directory, create a file namedmain.py
and add the following code to it. Modify the region (useast
) and resource group name (my-group
) if you specified different values when creating the Azure resource group in Step 1.import os
import sys
from enum import StrEnum, auto
import anyio
from azure.identity.aio import DefaultAzureCredential
from azure.mgmt.containerinstance.aio import ContainerInstanceManagementClient
import dagger
# configure container group, name and location
CONTAINER_NAME = "my-app"
CONTAINER_GROUP_NAME = "my-app"
CONTAINER_GROUP_LOCATION = "eastus"
RESOURCE_GROUP_NAME = "my-group"
class Env(StrEnum):
"""Required environment variables."""
def _generate_next_value_(self, *_) -> str:
if self not in os.environ:
msg = f"Environment variable must be set: {self}"
raise OSError(msg)
return os.environ[self]
DOCKERHUB_USERNAME = auto()
DOCKERHUB_PASSWORD = auto()
AZURE_SUBSCRIPTION_ID = auto()
AZURE_TENANT_ID = auto()
AZURE_CLIENT_ID = auto()
AZURE_CLIENT_SECRET = auto()
def as_secret(self, client: dagger.Client) -> dagger.Secret:
return client.set_secret(self.name.lower(), self.value)
async def main():
# initialize Dagger client
async with dagger.Connection(dagger.Config(log_output=sys.stderr)) as dagger_client:
# get reference to the project directory
source = dagger_client.host().directory(".", exclude=["node_modules", "ci"])
# get Node image
node = dagger_client.container(platform=dagger.Platform("linux/amd64")).from_(
"node:18"
)
# mount source code directory into Node image
# install dependencies
# set entrypoint
ctr = (
node.with_directory("/src", source)
.with_workdir("/src")
.with_exec(["cp", "-R", ".", "/home/node"])
.with_workdir("/home/node")
.with_exec(["npm", "install"])
.with_entrypoint(["npm", "start"])
)
# publish image
addr = await ctr.with_registry_auth(
"docker.io",
Env.DOCKERHUB_USERNAME,
Env.DOCKERHUB_PASSWORD.as_secret(dagger_client),
).publish(f"{Env.DOCKERHUB_USERNAME}/my-app")
print(f"Published at: {addr}")
# define deployment request
container_group = {
"containers": [
{
"name": CONTAINER_NAME,
"image": addr,
"ports": [{"port": "3000"}],
"resources": {"requests": {"cpu": "1", "memoryInGB": "1.5"}},
}
],
"ipAddress": {
"type": "Public",
"ports": [{"port": "3000", "protocol": "TCP"}],
},
"osType": "Linux",
"location": CONTAINER_GROUP_LOCATION,
"restartPolicy": "OnFailure",
}
# initialize Azure client
async with DefaultAzureCredential() as credential, ContainerInstanceManagementClient(
credential=credential,
subscription_id=Env.AZURE_SUBSCRIPTION_ID,
) as azure_client:
# send request and wait until done
response = await azure_client.container_groups.begin_create_or_update(
RESOURCE_GROUP_NAME,
CONTAINER_GROUP_NAME,
container_group,
)
result = await response.result()
print(
f"Deployment for image {addr} now available at "
f"http://{result.ip_address.ip}:{result.ip_address.ports[0].port}"
)
anyio.run(main)This file performs the following operations:
- It imports the Dagger and Azure SDK libraries.
- It checks for various required credentials in the host environment.
- It creates a Dagger client with
dagger.Connection()
. This client provides an interface for executing commands against the Dagger engine. - It uses the client's
set_secret()
method to set the Docker Hub registry password as a secret for the Dagger pipeline. - It uses the client's
host().directory()
method to obtain a reference to the current directory on the host, excluding thenode_modules
andci
directories. This reference is stored in thesource
variable. - It uses the client's
container().from_()
method to initialize a new container from a base image. The additionalplatform
argument to thecontainer()
method instructs Dagger to build for a specific architecture. In this example, the base image is thenode:18
image and the architecture islinux/amd64
. This method returns aContainer
representing an OCI-compatible container image. - It uses the previous
Container
object'swith_directory()
method to mount the host directory into the container image at the/src
mount point, and thewith_workdir()
method to set the working directory in the container image. - It chains the
with_exec()
method to copy the contents of the working directory to the/home/node
directory in the container image and then uses thewith_eorkdir()
method to change the working directory in the container image to/home/node
. - It chains the
with_exec()
method again to install dependencies withnpm install
and sets the container entrypoint using thewith_entrypoint()
method. - It uses the container object's
with_registry_auth()
method to set the registry credentials (including the password set as a secret previously) and then invokes thepublish()
method to publish the container image to Docker Hub. It also prints the SHA identifier of the published image. - It creates an Azure client (using the Azure credentials set in the host environment)
- It defines a deployment request to create or update a container in the Azure Container Instances service. This deployment request includes the container name, image, port configuration, location and other details.
- It submits the deployment request to the Azure Container Instances service and waits for a response. If successful, it prints the public IP address of the running container image.
Most Container
object methods return a revised Container
object representing the new state of the container. This makes it easy to chain methods together. Dagger evaluates pipelines "lazily", so the chained operations are only executed when required - in this case, when the container is published. Learn more about lazy evaluation in Dagger.
Step 3: Test the Dagger pipeline on the local host
Configure credentials for the Docker Hub registry and the Azure SDK on the local host by executing the commands below, replacing the placeholders as follows:
- Replace the
TENANT-ID
,CLIENT-ID
andCLIENT-SECRET
placeholders with the service principal credentials obtained at the end of Step 1. - Replace the
SUBSCRIPTION-ID
placeholder with your Azure subscription ID. - Replace the
USERNAME
andPASSWORD
placeholders with your Docker Hub username and password respectively.
export AZURE_TENANT_ID=TENANT-ID
export AZURE_CLIENT_ID=CLIENT-ID
export AZURE_CLIENT_SECRET=CLIENT-SECRET
export AZURE_SUBSCRIPTION_ID=SUBSCRIPTION-ID
export DOCKERHUB_USERNAME=USERNAME
export DOCKERHUB_PASSWORD=PASSWORD
Once credentials are configured, test the Dagger pipeline by running the command below:
- Go
- Node.js
- Python
dagger run go run ci/main.go
dagger run node ci/index.mjs
dagger run python ci/main.py
Dagger performs the operations defined in the pipeline script, logging each operation to the console. At the end of the process, the built container is deployed to Azure Container Instances and a message similar to the one below appears in the console output:
Deployment for image docker.io/.../my-app@sha256... now available at ...
Browse to the URL shown in the deployment message to see the running application.
If you deployed the example application from Appendix A, you should see a page similar to that shown below:
Step 4: Create an Azure Pipeline for Dagger
Dagger executes your pipelines entirely as standard OCI containers. This means that the same pipeline will run the same, whether on on your local machine or a remote server.
This also means that it's very easy to move your Dagger pipeline from your local host to Azure Pipelines - all that's needed is to commit and push the Dagger pipeline script from your local clone to your Azure DevOps repository, and then define an Azure Pipeline to run it on every commit.
-
Commit and push the Dagger pipeline script to the application's repository:
git add .
git commit -a -m "Added pipeline"
git push -
Create a new Azure Pipeline:
az pipelines create --name dagger --repository my-app --branch master --repository-type tfsgit --yml-path azure-pipelines.yml --skip-first-run true
-
Configure credentials for the Docker Hub registry and the Azure SDK in the Azure Pipeline by executing the commands below, replacing the placeholders as follows:
- Replace the
TENANT-ID
,CLIENT-ID
andCLIENT-SECRET
placeholders with the service principal credentials obtained at the end of Step 1. - Replace the
SUBSCRIPTION-ID
placeholder with your Azure subscription ID. - Replace the
USERNAME
andPASSWORD
placeholders with your Docker Hub username and password respectively.
az pipelines variable create --name AZURE_TENANT_ID --value TENANT-ID --pipeline-name dagger
az pipelines variable create --name AZURE_CLIENT_ID --value CLIENT-ID --pipeline-name dagger
az pipelines variable create --name AZURE_CLIENT_SECRET --value CLIENT-SECRET --pipeline-name dagger --secret true
az pipelines variable create --name AZURE_SUBSCRIPTION_ID --value SUBSCRIPTION-ID --pipeline-name dagger
az pipelines variable create --name DOCKERHUB_USERNAME --value USERNAME --pipeline-name dagger
az pipelines variable create --name DOCKERHUB_PASSWORD --value PASSWORD --pipeline-name dagger --secret true - Replace the
-
In the repository, create a new file at
azure-pipelines.yml
with the following content:- Go
- Node.js
- Python
trigger:
- master
pool:
name: 'Azure Pipelines'
vmImage: ubuntu-latest
steps:
- task: GoTool@0
inputs:
version: '1.20'
displayName: 'Install Go'
- script: |
go get dagger.io/dagger@latest
go get github.com/Azure/azure-sdk-for-go/sdk/azcore
go get github.com/Azure/azure-sdk-for-go/sdk/azidentity
go get github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerinstance/armcontainerinstance/v2
displayName: 'Install Dagger Go SDK and related'
- script: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
displayName: 'Install Dagger CLI'
- script: dagger run go run ci/main.go
displayName: 'Run Dagger'
env:
DOCKERHUB_PASSWORD: $(DOCKERHUB_PASSWORD)
AZURE_CLIENT_SECRET: $(AZURE_CLIENT_SECRET)trigger:
- master
pool:
name: 'Azure Pipelines'
vmImage: ubuntu-latest
steps:
- task: NodeTool@0
inputs:
versionSpec: '18.x'
displayName: 'Install Node.js'
- script: npm install @dagger.io/dagger @azure/arm-containerinstance @azure/identity
displayName: 'Install Dagger and Azure SDKs'
- script: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
displayName: 'Install Dagger CLI'
- script: dagger run node ci/index.mjs
displayName: 'Run Dagger'
env:
DOCKERHUB_PASSWORD: $(DOCKERHUB_PASSWORD)
AZURE_CLIENT_SECRET: $(AZURE_CLIENT_SECRET)trigger:
- master
pool:
name: 'Azure Pipelines'
vmImage: ubuntu-latest
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.11'
displayName: 'Install Python'
- script: pip install dagger-io aiohttp azure-identity azure-mgmt-containerinstance
displayName: 'Install Dagger and Azure SDKs'
- script: cd /usr/local && { curl -L https://dl.dagger.io/dagger/install.sh | sh; cd -; }
displayName: 'Install Dagger CLI'
- script: dagger run python ci/main.py
displayName: 'Run Dagger'
env:
DOCKERHUB_PASSWORD: $(DOCKERHUB_PASSWORD)
AZURE_CLIENT_SECRET: $(AZURE_CLIENT_SECRET)This Azure Pipeline runs on every commit to the repository
master
branch. It consists of a single job with four steps, as below:- The first step uses a language-specific task to download and install the programing language on the CI runner.
- The second and third steps download and install the required dependencies (such as the Dagger SDK, the Azure SDK and the Dagger CLI) on the CI runner.
- The fourth step adds executes the Dagger pipeline. It also explicity adds those variables defined as secret to the CI runner environment (other variables are automatically injected by Azure Pipelines).
tipAzure Pipelines automatically transfers pipeline variables to the CI runner environment, except for those marked as secret. Secret variables need to be explicitly defined in the Azure Pipelines configuration file.
Step 5: Test the Dagger pipeline in Azure Pipelines
Test the Dagger pipeline by committing a change to the repository.
If you are using the example application described in Appendix A, the following commands modify and commit a simple change to the application's index page:
git pull
sed -i 's/Dagger/Dagger on Azure/g' routes/index.js
git add routes/index.js
git commit -m "Update welcome message"
git push
The commit triggers the Azure Pipeline defined in Step 5. The Azure Pipeline runs the various steps of the job, including the Dagger pipeline script.
At the end of the process, a new version of the built container image is released to Docker Hub and deployed on Azure Container Instances. A message similar to the one below appears in the Azure Pipelines log:
Deployment for image docker.io/.../my-app@sha256:... now available at ...
Browse to the URL shown in the deployment message to see the running application. If you deployed the example application with the additional modification above, you see a page similar to that shown below:
Conclusion
This tutorial walked you through the process of creating a Dagger pipeline to continuously build and deploy a Node.js application on Azure Container Instances. It used the Dagger SDKs and explained key concepts, objects and methods available in the SDKs to construct a Dagger pipeline.
Dagger executes your pipelines entirely as standard OCI containers. This means that pipelines can be tested and debugged locally, and that the same pipeline will run consistently on your local machine, a CI runner, a dedicated server, or any container hosting service. This portability is one of Dagger's key advantages, and this tutorial demonstrated it in action by using the same pipeline on the local host and with Azure Pipelines.
Use the API Key Concepts page and the Go, Node.js and Python SDK References to learn more about Dagger.
Appendix A: Create an Azure DevOps repository with an example Express application
This tutorial assumes that you have an Azure DevOps repository with a Node.js Web application. If not, follow the steps below to create an Azure DevOps repository and commit an example Express application to it.
-
Create a directory for the Express application:
mkdir my-app
cd my-app -
Create a skeleton Express application:
npx express-generator
-
Make a minor modification to the application's index page:
sed -i -e 's/Express/Dagger/g' routes/index.js
-
Initialize a local Git repository for the application:
git init
-
Add a
.gitignore
file and commit the application code:echo node_modules >> .gitignore
git add .
git commit -a -m "Initial commit" -
Log in to Azure using the Azure CLI:
az login
-
Create a new Azure DevOps project and repository in your Azure DevOps organization. Replace the
ORGANIZATION-URL
placeholder with your Azure DevOps organization URL (usually of the formhttps://dev.azure.com/...
).az devops configure --defaults organization=ORGANIZATION-URL
az devops project create --name my-app -
List the available repositories and note the value of the
sshUrl
andwebUrl
fields:az repos list --project my-app | grep "sshUrl\|webUrl"
-
Browse to the URL shown in the
webUrl
field and configure SSH authentication for the repository. -
Add the Azure DevOps repository as a remote and push the application code to it. Replace the
SSH-URL
placeholder with the value of thesshUrl
field from the previous command.git remote add origin SSH-URL
git push -u origin --all