{
"$type": "site.standard.document",
"canonicalUrl": "https://johnnyreilly.com/posts/azure-container-apps-dapr-bicep-github-actions-debug-devcontainer",
"description": "Build and deploy two Azure Container Apps using Bicep and GitHub Actions, communicate using dapr, build, run and debug in VS Code using a devcontainer.",
"path": "/posts/azure-container-apps-dapr-bicep-github-actions-debug-devcontainer",
"publishedAt": "2022-01-22T00:00:00.000Z",
"site": "at://did:plc:yy3apqjlms24kso7ahn7lbmb/site.standard.publication/3mova7c4nho2b",
"tags": [
"bicep",
"github actions",
"azure container apps"
],
"textContent": "This post shows how to build and deploy two Azure Container Apps using Bicep and GitHub Actions. These apps will communicate using dapr, be built in VS Code using a devcontainer. It will be possible to debug in VS Code and run with docker-compose.\n\nThis follows on from the previous post which built and deployed a simple web application to Azure Container Apps using Bicep and GitHub Actions using the GitHub container registry.\n\n\n\nUpdated 02/05/2022\n\nThis post has been updated to reflect the migration of Azure Container Apps from the Microsoft.Web namespace to the Microsoft.App namespace in March 2022. See: https://github.com/microsoft/azure-container-apps/issues/109\n\nWhat we're going to build\n\nAs an engineer, I'm productive when:\n\n- Integrating different services together is a turnkey experience and\n- I'm able to easily debug my code\n\nI've found that using dapr and VS Code I'm able to achieve both of these goals. I can build an application made up of multiple services, compose them together using dapr and deploy them to Azure Container Apps with relative ease.\n\nIn this post we're going to build an example of that from scratch, with a koa/node.js (built with TypeScript) front end that will communicate with a dotnet service via dapr.\n\nAll the work done in this post can be found in the dapr-devcontainer-debug-and-deploy repo. As a note, if you're interested in this topic it's also worth looking at the Azure-Samples/container-apps-store-api-microservice repo.\n\nSetting up our devcontainer\n\nThe first thing we'll do is set up our devcontainer. We're going to use a tweaked version of the docker-in-docker image from the vscode-dev-containers repo.\n\nIn the root of our project we'll create a .devcontainer folder, and within that a library-scripts folder. There's a number of communal scripts from the vscode-dev-containers repo which we're going to lift and shift into in our library-scripts folder:\n\n- docker-in-docker-debian.sh - for installing Docker in Docker\n- azcli-debian.sh - for installing the Azure CLI\n\nIn the .devcontainer folder we want to create a Dockerfile:\n\nThe above is a loose riff on the docker-in-docker Dockerfile, lovingly mixed with the Azure-Samples container-apps Dockerfile.\n\nIt installs the following:\n\n- Dot Net\n- Node.js\n- the Azure CLI\n- Docker\n- Bicep\n- Dapr\n\nNow we have our Dockerfile, we need a devcontainer.json to go with it:\n\nThe above will:\n\n- install Node 16 / dotnet 6 and the latest Azure CLI\n- install a number of VS Code extensions related to dapr / Docker / Bicep / Azure / C- install dapr when the container starts\n\nWe're ready! Reopen your repo in a container (it will take a while first time out) and you'll be ready to go.\n\nCreate a dotnet service\n\nNow we're going to create a dotnet service. The aim of this post is not to build a specific application, but rather to demonstrate how simple service to service communication is with dapr. So we'll use the web api template that ships with dotnet 6. That arrives with a fake weather API included, so we'll name our service accordingly:\n\nInside the created Program.cs, find the following line and delete it:\n\nHTTPS is important, however Azure Container Apps are going to tackle that for us.\n\nCreate a Node.js service (with Koa)\n\nCreating our dotnet service was very simple. We're now going to create a web app with Node.js and Koa that calls our dotnet service. This will be a little more complicated - but still surprisingly simple thanks to the great API choices of dapr.\n\nLet's make that service:\n\nWe're installing the following:\n\n- koa - the web framework we're going to use\n- axios - to make calls to our dotnet service via HTTP / dapr\n- TypeScript and associated type definitions, so we can take advantage of static typing. Admittedly since we're building a minimal example this is not super beneficial; but TS makes me happy and I'd certainly want static typing in place if going beyond a simple example. Start as you mean to go on.\n\nWe'll create a tsconfig.json:\n\nWe'll update the scripts section of our package.json like so:\n\nSo we can build and start our web app. Now let's write it!\n\nWe're going to create an index.ts file:\n\nThe above code is fairly simple but is achieving quite a lot. It:\n\n- uses various environment variables to construct the URLs / headers which allow connecting to the dapr sidecar running alongside the app, and consequently to the weather service through the dapr sidecar running alongside the weather service. We're going to set up the environment variables which this code relies upon later.\n- spins up a web server with koa on port 3000\n- that web server, when sent an HTTP request, will call the weatherForecast endpoint of the dotnet app. It will grab what comes back, take the first entry in there and surface that up as the weather forecast.\n- We're also defining a WeatherForecast interface to represent the type of the data that comes back from the dotnet service\n\nIt's worth dwelling for a moment on the simplicity that dapr is affording us here. We're able to make HTTP requests to our dotnet service just like they were any other service running locally. What's actually happening is illustrated by the diagram below:\n\nWe're making HTTP requests from the web service, which look like they're going directly to the weather service. But in actual fact, they're being routed through dapr sidecars until they reach their destination. Why is this fantastic? Well there's two things we aren't having to think about here:\n\n- certificates\n- inter-service authentication\n\nBoth of these can be complex and burn a large amount of engineering time. Because we're using dapr it's not a problem we have to solve. Isn't that great?\n\nDebugging dapr in VS Code\n\nWe want to be able to debug this code. We can achieve that in VS Code by setting a launch.json and a tasks.json file.\n\nFirst of all we'll create a launch.json file in the .vscode folder of our repo:\n\nThe things to note about this are:\n\n- we create a Node.js (\"WebService\") and a dotnet (\"WeatherService\") configuration. These are referenced by the All Container Apps compound. Kicking off that will start both the Node.js and the dotnet apps.\n- The Node.js app runs a daprd-debug-node task prior to launch and a daprd-down-node task when debugging completes. Comparable tasks are run by the dotnet container - we'll look at these in a moment.\n- Various environment variables are configured, most of which control the behaviour of dapr. When we're debugging locally we'll be using some non-typical ports to accomodate multiple dapr sidecars being in play at the same time. Note also the \"WEATHER_SERVICE_NAME\": \"dotnet-app\" - it's this that allows the WebService to communicate with the WeatherService - dotnet-app is the appId used to identify a service with dapr. We'll see that as we configure our tasks.json.\n\nHere's the tasks.json we must make:\n\nThere's two sets of tasks here; one for the WeatherService and one for the WebService. You'll see some commonalities here. For each service there's a daprd task that depends upon the relevant service being built and passes the various ports for the dapr sidecar to run on that runs just before debugging kicks off. To go with that, there's a daprd-down task for each service that runs when debugging finishes and shuts down dapr.\n\nWe're now ready to debug our app. Let's hit F5.\n\nAnd if we look at our browser:\n\nIt works! We're running a Node.js WebService which, when called, is communicating with our dotnet WeatherService and surfacing up the results. Brilliant!\n\nContainerising our services with Docker\n\nBefore we can deploy each of our services, they need to be containerised.\n\nFirst let's add a Dockerfile to the WeatherService folder:\n\nThen we'll add a Dockerfile to the WebService folder:\n\nLikely these Dockerfiles could be optimised further; but we're not focussed on that just now. What we have now are two simple Dockerfiles that will give us images we can run. Given that one depends on the other it makes sense to bring them together with a docker-compose.yml file which we'll place in the root of the repo:\n\nWith this in place we can run docker-compose up and bring up our application locally.\n\nAnd now we have docker images built, we can look at deploying them.\n\nDeploying to Azure\n\nAt this point we have pretty much everything we need in terms of application code and the ability to build and debug it. Now we'd like to deploy it to Azure.\n\nLet's begin with the Bicep required to deploy our Azure Container Apps.\n\nIn our repository we'll create an infra directory, into which we'll place a main.bicep file which will contain our Bicep template:\n\nThis will deploy two container apps - one for our WebService and one for our WeatherService. Alongside that we've resources for logging and environments.\n\nSetting up a resource group\n\nWith our Bicep in place, we're going to need a resource group to send it to. Right now, Azure Container Apps aren't available everywhere. So we're going to create ourselves a resource group in North Europe which does support ACAs:\n\nSecrets for GitHub Actions\n\nWe're aiming to set up a GitHub Action to handle our deployment. This will depend upon a number of secrets:\n\nWe'll need to create each of these secrets.\n\nAZURE_CREDENTIALS - GitHub logging into Azure\n\nSo GitHub Actions can interact with Azure on our behalf, we need to provide it with some credentials. We'll use the Azure CLI to create these:\n\nRemember to replace the {subscription-id} with your subscription id and {resource-group} with the name of your resource group (rg-aca if you're following along). This command will pump out a lump of JSON that looks something like this:\n\nTake this and save it as the AZURE_CREDENTIALS secret in Azure.\n\nPACKAGES_TOKEN - Azure accessing the GitHub container registry\n\nWe also need a secret for accessing packages from Azure. We're going to be publishing packages to the GitHub container registry. Azure is going to need to be able to access this when we're deploying. ACA deployment works by telling Azure where to look for an image and providing any necessary credentials to do the acquisition. To facilitate this we'll set up a PACKAGES_TOKEN secret. This is a GitHub personal access token with the read:packages scope. Follow the instructions here to create the token.\n\nDeploying with GitHub Actions\n\nWith our secrets configured, we're now well placed to write our GitHub Action. We'll create a .github/workflows/build-and-deploy.yaml file in our repository and populate it thusly:\n\nThere's a lot in this workflow. Let's dig into the build and deploy jobs to see what's happening.\n\nbuild - building our image\n\nThe build job is all about building our container images and pushing then to the GitHub registry. It's heavily inspired by Jeff Hollan's Azure sample app GHA. When we look at the strategy we can see a matrix of services consisting of two services; our node app and our dotnet app:\n\nThis is a matrix because a typical use case of an Azure Container Apps will be multi-container - just as this is. The outputs pumps out the details of our image-node and image-dotnet images to be used later:\n\nWith that understanding in place, let's examine what each of the steps in the build job does\n\n- Log into registry - logs into the GitHub container registry\n- Extract Docker metadata - acquire tags which will be used for versioning\n- Build and push Docker image - build the docker image and if this is not a PR: tag, label and push it to the registry\n- Output image tag - write out the image tag for usage in deployment\n\ndeploy - shipping our image to Azure\n\nThe deploy job runs the az deployment group create command which performs a deployment of our main.bicep file.\n\nIn either case we pass the same set of parameters:\n\nThese are either:\n\n- secrets we set up earlier\n- special github variables\n- environment variables declared at the start of the script or\n- outputs from the build step - this is where we acquire our node and dotnet images\n\nRunning it\n\nWhen the GitHub Action has been run you'll find that Azure Container Apps are now showing up inside the Azure Portal in your resource group, alongside the other resources:\n\nIf we take a look at our web ACA we'll see\n\nAnd when we take a closer look at the container app, we find a URL we can navigate to:\n\nCongratulations! You've built and deployed a simple web app to Azure Container Apps with Bicep and GitHub Actions and secrets.\n\nThe subscription '**' cannot have more than 2 environments.\n\nBefore signing off, it's probably worth sharing this gotcha. If you've been playing with Azure Container Apps you may have already deployed an \"environment\" (Microsoft.Web/kubeEnvironments). It's fairly common to have a limit of one environment per subscription, which is what this message is saying. So either delete other environments, share the one you have or arrange to raise the limit on your subscription.",
"title": "Azure Container Apps: dapr, devcontainer, debug and deploy"
}