An azd
(Azure Developer CLI) template for getting a Next.js app running on Azure Container Apps with CDN and Application Insights.
The Next.js app included with the template has been generated with create-next-app
and has some additional code and components specific to this template that provide:
Of course with this being an azd
template you are free to build on top of the sample app, replace the sample app with your own, or cherry-pick what you want to keep or remove.
Before starting, be sure you have set all of the required secrets and environment variables in your Github repo, under Settings -> Environments. Please see Using environments for deployment for more details on how to set up environments.
The minimum environment variables and secrets you will need to set are:
azd config show
locally to retrieve your subscription ID.There are other variables available but those are optional.
The quickest way to try this azd
template out is using GitHub Codespaces or in a VS Code Dev Container:
Then from a Terminal:
# install dependencies
npm i
# create a `.env.local` file from the provided template
npm run env:init
# follow the prompts to sign in to your Azure account
azd auth login
# follow the prompts to provision the infrastructure resources in Azure
azd provision
# deploy the app to the provisioned infrastructure
azd deploy
The output from the
azd deploy
command includes a link to the Resource Group in your Azure Subscription where you can see the provisioned infrastructure resources. A link to the Next.js app running in Azure is also included so you can quickly navigate to your Next.js app that is now hosted in Azure.
π You now have a Next.js app running in Container Apps in Azure with a CDN for fast delivery of static files and Application Insights attached for monitoring!
π₯ When you're done testing you can run azd down
in the terminal and that will delete the Resource Group and all of the resources in it.
If you do not have access to or do not want to work in Codespaces or a Dev Container you can of course work locally, but you will need to ensure you have the following pre-requisites installed:
nvm
or fnm
is recommended
azd
supports several development environments. This template was developed in VS Code, and has been tested in GitHub Codespaces and Dev Containers (via VS Code). Visual Studio has not been tested.
npm
is used as it is the "safest" default. You should be able to switch out for the package manager of your choice, but onlynpm
has been tested.
βοΈ Once you have everything installed you can clone this repo and start developing or deploy to Azure with the azd
CLI.
azd
CLITo deploy your app from your Terminal with azd
run:
# install dependencies (if you have not already done so)
npm i
# follow the prompts to sign in to your Azure account (if you are not already signed in)
azd auth login
# create a `.env.local` file from the provided template (if you don't already have a `.env.local` file - this will be a no-op if you have)
npm run env:init
# follow the prompts to provision the infrastructure resources in Azure
azd provision
# deploy the app to the provisioned infrastructure
azd deploy
Then when you're finished with the deployment run:
# delete the app and its infrastructure from Azure
azd down
azd
has anazd up
command, which the docs describe as "You can runazd up
to perform bothazd provision
andazd deploy
in a single step". Runningazd up
is actually the equivalent ofazd package
->azd provision
->azd deploy
though, which does not work for this template because outputs from theazd provision
step such as the app's URL and the CDN endpoint URL are required bynext build
, which is run inside theDockerfile
duringazd package
. So unless the behaviour ofazd up
can be changed in future you will need to continue to runazd provision
->azd deploy
.
azd
in a CI/CD pipelineThis template supports automated provisioning and deployment of your application and its infrastructure via a CI/CD pipeline running in GitHub Actions or Azure DevOps via the same azd
process that you can run locally:
The template includes instrumentation and components to enable server-side and client-side instrumentation and logging via App Insights when deployed to Azure.
Server-side instrumentation is implemented using the Application Insights for Node.js (Beta) package. The package is initialised via Next.js's Instrumentation feature and leverages Next.js's support for OpenTelemetry.
The template also provides a logging implementation to allow for explicit logging of errors, warnings etc in your app's server-side code (including inside server components). The logger is implemented using pino
and sends logs to Application Insights via a pino
transport. To use the logger in your app you can import { logger } from '@/lib/instrumentation/logger'
.
π‘ To prevent runtime errors the applicationinsights
and pino
packages are opted-out of Next.js's bundling process via serverComponentsExternalPackages
.
Server-side instrumentation and logging to Application Insights requires a connection string to an Application Insights resource to be provided via the environment variable
APPLICATIONINSIGHTS_CONNECTION_STRING
. This environment variable is provided to the app automatically byazd provision
.If the connection string is not available, for example when running in your development environment outside of
azd provision
, the instrumentation will not be initialised and the logger will fallback to using thepino-pretty
transport to log toconsole
.
Client-side instrumentation is implemented using the Microsoft Application Insights JavaScript SDK - React Plugin package. The package is initialised in a AppInsightsProvider
server component, which is a wrapper around a client component that renders the AppInsightsContext
component from the React Plugin. You can import { AppInsightsProvider } from '@/components/instrumentation/AppInsightsProvider'
and place it somewhere fairly high up in the component tree, for example this template renders the component inside its Root Layout.
Client-side logging can be performed by using the useAppInsightsContext
hook from within client components as described in the documentation for the React Plugin.
Client-side instrumentation and logging to Application Insights requires a connection string to an Application Insights resource to be provided via the environment variable
APPLICATIONINSIGHTS_CONNECTION_STRING
. This environment variable is provided to the app automatically byazd provision
.If the connection string is not available, for example when running in your development environment outside of
azd provision
, theAppInsightsProvider
will not render anduseAppInsightsContext
will returnundefined
.
π Thank you to Jonathan Rupp who very kindly shared his implementation for client-side instrumentation in this GitHub discussion, which was mostly reused for the implementation in this template.
An Azure CDN endpoint is deployed and configured to work in "origin pull" mode, which means the first request made for a resource to the CDN will proxy through to the Container App (the origin) and the response will be cached on the CDN for subsequent requests.
For this to work the static assets from the Next.js build output and public
folder are included in the Docker image that is created during the azd deploy
step and deployed to your Container App.
To configure the CDN to be used for requests to Next.js's static assets the assetPrefix
configuration option is set in next.config.js
.
To use the CDN for requests to other resources (such as those in the public
folder) you can import { getCdnUrl } from '@/lib/url'
and use the getCdnUrl
function to generate a URL that will proxy the request through the CDN.
The template also includes a function that allows you to generate an absolute URL from a relative path if you require it (i.e. direct to the origin without proxying through the CDN). To use the function you can import { getAbsoluteUrl } from '@/lib/url'
.
For an example of how
getCdnUrl
can be used seepage.tsx
. An example ofgetAbsoluteUrl
can be seen inrobots.ts
.
As well as assetPrefix
there are some other related configuration options set in next.config.js
:
compress
option is set to true
by default because although the CDN will provide compression for static assets pulled from the origin the CDN doesn't cover dynamic assetsremotePatterns
option is set to allow CDN URLs to be used by the Next.js's <Image>
componentCache-Control
header is set via the headers
option for requests that include the buildId
in the URL (the getCdnUrl
function adds this by default as a cache-busting strategy)π‘ The template also adds a preconnect
for the CDN in layout.tsx
.
The features described above require the presence of the environment variables
NEXT_PUBLIC_CDN_URL
,NEXT_PUBLIC_CDN_HOSTNAME
,NEXT_COMPRESS
,NEXT_PUBLIC_BUILD_ID
andNEXT_PUBLIC_BASE_URL
. These are all provided to the app automatically with the exception ofNEXT_COMPRESS
, which is provided by.env.production
.If these environment variables are not provided, for example when running in your development environment outside of
azd provision
, theassetPrefix
,remotePatterns
andheaders
will not be set, thegetCdnUrl
andgetAbsoluteUrl
functions will return the relative path that was provided as input to the function, and thepreconnect
will not be added.
The template includes functions for checking the environment that the application is currently running in. You can import { environment, currentEnvironment } from '@/lib/environment'
and then add conditional logic where required, for example:
if (currentEnvironment === environment.production) {
// Do production stuff
} else {
// Do non-production stuff
}
π‘ If you want to change the environment names or add support for additional environments you can edit the environments in src/lib/environment.ts
.
currentEnvironment
is set using an environment variableNEXT_PUBLIC_APP_ENV
. This is provided automatically byazd provision
or by.env.development
when running the development server.If the environment variable is not set for some reason the default value for
currentEnvironment
isenvironment.development
.
When developing your app you should use environment variables as per the Next documentation:
.env
for default vars for all builds and environments.env.development
for default development build (i.e. next dev
) vars.env.production
for default production build (i.e. next build
) vars.env.local
for secrets, environment-specific values, or overrides of development or production build defaults set in any of the other files above
.env.local
should never be committed to your repo, but this repo includes a.env.local.template
file that should be maintained as an example of what environment variables your app can support or is expecting in.env.local
.The
.env.local.template
file is also used in CI/CD pipelines to generate aenv.local
file for the target environment. It is therefore important to keep this file updated as and when you add additional environment variables to your app.
azd
uses environment variables in this templateWhen running azd provision
:
preprovision
hook runs the .azd/hooks/preprovision.ps1
script
.azd/scripts/create-infra-env-vars.ps1
script runs.env
, .env.production
and .env.local
files (if they exist) are read and merged together (matching entries from the later files override entries from earlier files)infra/env-vars.json
file as key value pairs (values are always of type string
)azd
runs the main.bicep
file
infra/env-vars.json
file created by the preprovision
hook is loaded into a variable named envVars
to be used during provisioning of the infrastructureenvVars
can be used to set properties of the infratructure resources defined in the main.bicep
script such as min/max scale replicas, custom domain name, and to pass environment variables through to the Container App that are required at runtimeazd
writes any output
(s) from the main.bicep
file to .azure/{AZURE_ENV_NAME}/.env
azd provision
and not specific to this templatepostprovision
hook runs the .azd/hooks/postprovision.ps1
script
.azure/{AZURE_ENV_NAME}/.env
file are merged with the .env.local
file (if one exists) and the results are written to a .env.azure
file.env.azure
file will be used by azd deploy
The
main.bicep
script will error if it is expecting a key to be present in yourinfra/env-vars.json
file, but it is missing. This is why you must keep your environment variables updated.The
infra/env-vars.json
and.env.azure
files should not be committed to your repo as they may contain secret or sensitive values from your.env.local
file.
When running azd deploy
:
Dockerfile
copies all .env*
files from the local disk.env.azure
and renames and overwrites the .env.local
file with itnext build
then runs, which loads in env files as normal including the .env.local
file.env.local
file is generated when running in a pipelineThe .env.local
file is required to provision, build and deploy the app, but it should never be committed to your repository and so is not available to the CI/CD pipeline when it clones your repo.
To overcome this problem the pipelines provided in this template are capable of generating an env.local
file by reading environment variables from the pipeline build agent context and merging them with the .env.local.template
file.
Exactly how the environment variables are surfaced to the build agent is slightly different depending on whether you are using an Azure DevOps (AZDO) or GitHub Actions pipeline due to the specific capabilities of each, but the approach used to generate the .env.local
file is broadly the same:
.env.local.template
filenpm run env:init
, which merges the contents of the .env.local.template
file with the environment variables in the build agent context and outputs the result to .env.local
β‘ azd provision
and azd deploy
then run as they would locally, using the env.local
file created during the current pipeline run.
This template includes support for running a CI/CD pipeline in GitHub Actions or Azure DevOps Pipelines. The specifics of the pipelines does differ due to the different capabilities and behaviour of each platform, but an effort has been made to keep the two pipelines broadly in line with each other so that the steps are comparable:
refs/heads/main
-> production
npm run env:init
to generate a .env.local
file from the environment variables loaded into the build contextazd provision
azd deploy
Below are some instructions for how to setup and configure the pipelines included with this template for:
azd
includes anazd pipeline config
command that can be used to help initialise a pipeline on either platform. This is not recommended by this template because a) it requires creating target environments locally and having access to their environment variables, which doesn't "feel right" (i.e. having access to production secrets in a development environment doesn't "feel right"); and b) it creates "global" environment variables in GitHub, but this template recommends that you scope environment variables to specific target environments.Hopefully in future
azd
will offer hooks into theazd pipeline
commands that allow for the below steps to be automated, but for now they are manual steps.
π‘ The instructions below are written as if you are adding a production
environment as that is assumed to be required and is catered for "out of the box" with the template, but you can add support for other environments also. For example you could map pipeline runs triggered by a push to a canary
branch on your repo to a uat
target environment.
You don't need to do anything specific to add the workflow in GitHub Actions, the presence of the .github/workflows/azure-dev.yml
file is enough, but you will need to:
Settings
-> Environments
New environment
, name it production
, and click Configure environment
You can read more about creating environments in the GitHub documentation. Note that there are limitations with Environments in GitHub if you are using a Free acount and your repository is private.
Microsoft Entra ID
-> App registrations
New registration
name
for your Service principal, and click Register
Application ID
and Directory (tenant) ID
- we will need those laterCertificates & secrets
Federated credentials
and click Add credential
GitHub Actions deploying Azure resources
scenario, and fill in the required information
Organization
: your GitHub usernameRepository
: your GitHub repository nameEntity type
: Environment
GitHub environment name
: the environment name (production
)Name
: a name for the scenario{Organization}-{Repository}-{GitHub environment name}
Add
Subscriptions
Subscription ID
- we will need this laterAccess control (IAM)
-> Role assignments
Contributor
role
Add
-> Add role assignment
Privileged administrator roles
-> Contributor
Next
Select members
and select your Service principalReview + assign
and complete the Role assignmentRole Based Access Control Administrator
role
Add
-> Add role assignment
Privileged administrator roles
-> Role Based Access Control Administrator
Next
Select members
and select your Service principalNext
Constrain roles
and only allow assignment of the AcrPull
roleReview + assign
and complete the Role assignmentPINECONE_API_KEY={your Pinecone API key}
OPENAI_API_KEY={your OpenAI API key}
PINECONE_REGION={your Pinecone region}
PINECONE_INDEX={your index name}
pinecone-rag-demo-azd
.AZURE_ENV_NAME=prod
AZURE_TENANT_ID={tenant_id}
{tenant_id}
with your Tenant's Tenant ID
AZURE_SUBSCRIPTION_ID={subscription_id}
{subscription_id}
with your Subscription's Subscription ID
AZURE_CLIENT_ID={service_principal_id}
{service_principal_id}
with your Service principal's Application ID
AZURE_LOCATION={location_name}
{location_name}
with your desired region nameaz account list-locations -o table
SERVICE_WEB_CONTAINER_MIN_REPLICAS=1
.env.local.template
file) then you can continue to do so e.g. SERVICE_WEB_CONTAINER_MAX_REPLICAS=5
main.bicep
fileYou need to manually create a pipeline in Azure DevOps - the presence of the .azdo/pipelines/azure-dev.yml
file is not enough by itself - you will need to:
Pipelines
-> Pipelines
New pipeline
Configure your pipeline
, select Existing Azure Pipelines YAML file
and select the .azdo/pipelines/azure-dev.yml
fileSave
(don't Run
) the pipelineProject settings
-> Service connections
New service connection
Azure Resource Manager
Service pincipal (automatic)
Subscription
that you wish to deploy your resources toResource group
azconnection
azd
- feel free to change it, but if you do you will need to update your azure-dev.yml
file alsoDescription
if you wantGrant access permissions to all pipelines
Save
Manage Service Principal
, which will take you to the Service Principal in the Azure PortalDisplay name
Branding & properties
and change the Name
Directory (tenant) ID
- we will need that laterManage service connection roles
, which will take you to the Subscription in the Azure PortalRole assignments
Role Based Access Control Administrator
role
Add
-> Add role assignment
Privileged administrator roles
-> Role Based Access Control Administrator
Next
Select members
and select your Service principalNext
Constrain roles
and only allow assignment of the AcrPull
roleReview + assign
and complete the Role assignmentOverview
tab of your SubscriptionSubscription ID
- we will need this laterPipelines
-> Environments
production
environment
Description
if you wantResource
select None
Pipelines
-> Library
Variable group
called production
AZURE_ENV_NAME=prod
AZURE_TENANT_ID={tenant_id}
{tenant_id}
with your Tenant's Tenant ID
AZURE_SUBSCRIPTION_ID={subscription_id}
{subscription_id}
with your Subscription's Subscription ID
AZURE_LOCATION={location_name}
{location_name}
with your desired region nameaz account list-locations -o table
SERVICE_WEB_CONTAINER_MIN_REPLICAS=1
.env.local.template
file) then you can continue to do so e.g. SERVICE_WEB_CONTAINER_MAX_REPLICAS=5
main.bicep
fileπ‘ If you add additional environment variables for use in your app and want to override them in this environment then you can come back here later to add or change anything as needed.
The first time you run the pipeline it will ask you to permit access to the
production
Environment and Variable group that you just created, which you should allow for the pipeline to run succesfully.
Azure supports adding custom domain names with free managed SSL certificates to Container Apps. The Bicep scripts included in this template are setup to provide this capability, but before we can add a custom domain name and managed certificate Azure requires that DNS records be created to verify domain ownership.
The verification process is described in steps 7 and 8 of the Container Apps documentation, so please refer to that for specifics, but in summary you must add the following records via your DNS provider:
TXT
record containing a domain verification code; andA
record containing the static IP address of the Container Apps Environment; orCNAME
record containing the FQDN of the Container AppTo get the information that you require for these DNS records you can:
azd
locally
azd provision
(if you have not already)npm run env:dv
azd
in a pipeline
Domain Verification
taskIncluded in the output are the Static IP
, FQDN
and Verification code
- use these values to set your DNS records as per the Container Apps documentation (linked above).
To set your custom domain name on your Container App you will need to add (or update) an environment variable named SERVICE_WEB_CUSTOM_DOMAIN_NAME
:
For example, to set the domain name for the container app to
www.example.com
you would add an environment variableSERVICE_WEB_CUSTOM_DOMAIN_NAME=www.example.com
.
.env.local
fileproduction
)production
)You will then need to:
azd
locally
azd provision
azd
in a pipeline
π‘ When you add a custom domain name a redirect rule is automatically added so that if you attempt to navigate to the default domain of the Container App there will be a permanent redirect to the custom domain name - this redirect is configured in next.config.js
. The getAbsoluteUrl
function provided by this template will also use the custom domain name you have set rather than the default domain of the Container App.
The final step is to create a free managed SSL certificate for your custom domain name and add it to your Container App:
Certificates
-> Managed certificate
Add certificate
Custom domain
nameHostname record type
Validate
the custom domain nameAdd
the certificateCertificate ID
Certificate Status
to become Suceeded
Certificate ID
is not exposed in a convenient place in the Azure Portal, but you can work it out from the information provided:
Certificate Name
Overview
-> JSON View
Resource ID
Certificate ID
using the pattern:{Resource ID}/managedCertificates/{Certificate Name}
You will then need to add (or update) an environment variable named SERVICE_WEB_CUSTOM_DOMAIN_CERT_ID
and with the value of your Certificate ID
:
.env.local
fileproduction
)production
)And finally you will need to:
azd
locally
azd provision
azd deploy
azd
in a pipeline
β‘ The custom domain and SSL certificate will now be bound to your Container App.
It is possible to automate the creation of managed certificates through Bicep, which would be preferable to the above manual process, but there are a few "chicken and egg" issues that make automation difficult at the moment. In the context of this template it was decided that a manual solution is the most pragmatic solution.
The situation with managed certificates is discussed on this GitHub issue so hopefully there will be better support for automation in the future - one to keep an eye on!
If a manual approach is not scaleable for your needs then have a read through the links provided above for some ideas of how others have approached an automated solution.
This template uses the following Azure resources:
Here's a high level architecture diagram that illustrates these components. Notice that these are all contained within a single resource group that will be created for you when you create the resources.
This template provisions resources to an Azure subscription that you will select upon provisioning them. Refer to the Pricing calculator for Microsoft Azure to estimate the cost you might incur when this template is running on Azure and, if needed, update the included Azure resource definitions found in infra/main.bicep
to suit your needs.
This template creates a Managed Identity for your app inside your Azure Active Directory tenant. It is used to permit the Container App to pull images from the Container Registry.
To view your managed identity in the Azure Portal follow these steps.