Skip to content

Commit

Permalink
Improve support for and documentation of adding a custom domain name …
Browse files Browse the repository at this point in the history
…and managed cert
  • Loading branch information
CMeeg committed Mar 3, 2024
1 parent def990f commit e5bdda0
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 23 deletions.
30 changes: 14 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,6 @@ TODO: Write this

### Azure DevOps Pipelines

TODO: Run through this process and check it for correctness

You 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:

1. [Create the Pipeline](#create-the-pipeline)
Expand Down Expand Up @@ -403,17 +401,15 @@ You can also run the pipeline manually:

## Adding a custom domain name

TODO: Run through this process and check it for correctness

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.

### Verify domain ownership

The verification process is described in steps 7 and 8 of the [Container Apps documentation](https://learn.microsoft.com/en-us/azure/container-apps/custom-domains-managed-certificates?pivots=azure-portal#add-a-custom-domain-and-managed-certificate), so please refer to that for specifics, but in summary you must add the following records via your DNS provider:
Azure requires that you add the following DNS records:

* A `TXT` record containing a domain verification code; and
* An `A` record containing the static IP address of the Container Apps Environment; or
* A `CNAME` record containing the FQDN of the Container App
* If you are using an apex domain, an `A` record containing the static IP address of the Container Apps Environment; or
* If you are using a subdomain, a `CNAME` record containing the FQDN of the Container App

To get the information that you require for these DNS records you can:

Expand All @@ -422,9 +418,15 @@ To get the information that you require for these DNS records you can:
* Check the output in your terminal
* When running `azd` in a pipeline
* Run the pipeline (if you have not already)
* Check the output of the `Provision infrastructure` task in the logs
* Check the output of the `Provision infrastructure` task in the pipeline run logs

In the output you should find a line `[postprovision] === Container apps domain verification ===` and under that are the `Static IP`, `FQDN` and `Verification code` - use these values to set your DNS records as per the Container Apps documentation (linked above).
In the output you should find a line `[postprovision] === Container apps domain verification ===` and under that are 3 values that you need to create the DNS records:

* `Static IP`
* `FQDN`
* `Verification code`

You can use these values to add the DNS records as described in steps 7 and 8 of the [Container Apps documentation](https://learn.microsoft.com/en-us/azure/container-apps/custom-domains-managed-certificates?pivots=azure-portal#add-a-custom-domain-and-managed-certificate).

### Set your custom domain name

Expand All @@ -436,14 +438,14 @@ To set your custom domain name on your Container App you will need to add (or up
* In GitHub Actions: as an [Environment variable](#add-environment-variables) in the target Environment (e.g. `production`)
* In Azure DevOps: as a [Variable in the Variable group](#create-a-variable-group-for-your-environment) for the target Environment (e.g. `production`)

You will then need to:
You will then need to provision your infrastructure to add the domain name to your container app:

* When running `azd` locally
* Run `azd provision`
* When running `azd` in a pipeline
* Run the 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 `./server/redirect.js`. The [`getAbsoluteUrl`](#azure-cdn) function provided by this template will also use the custom domain name you have set rather than the default domain of the Container App.
> Ideally we would add the domain name and SSL certificate at the same time and automate it through Bicep, but unfortunately Azure makes this difficult to do because it doesn't allow you to create a managed certificate for a domain name that hasn't been added to the Container App, but you can't add a domain name binding without an SSL certificate. So the method documented here adds the domain name without a binding first, which allows the managed certificate and binding to be created in the next step. Hopefully [issues with managed certificates](https://github.com/microsoft/azure-container-apps/issues/607) will be addressed in future to allow for automation of this process.
### Add a free managed certificate for your custom domain

Expand Down Expand Up @@ -484,11 +486,7 @@ And finally you will need to:

⚡ 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](https://johnnyreilly.com/azure-container-apps-bicep-managed-certificates-custom-domains) 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](https://github.com/microsoft/azure-container-apps/issues/607) 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.
💡 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 `./server/redirect.js`. The [`getAbsoluteUrl`](#azure-cdn) function provided by this template will also use the custom domain name you have set rather than the default domain of the Container App.

## Application architecture

Expand Down
6 changes: 3 additions & 3 deletions infra/containers/container-app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ param containerAppEnvironmentId string
param containerRegistryName string
param userAssignedIdentityId string
param allowedOrigins array = []
param certificateId string = ''
param containerCpuCoreCount string = '0.5'
param containerMaxReplicas int = 1
param containerMemory string = '1.0Gi'
param containerMinReplicas int = 0
param containerName string = 'main'
param customDomainName string = ''
param customDomainCertificateId string = ''
param env array = []
param external bool = true
param imageName string = ''
Expand Down Expand Up @@ -46,8 +46,8 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
customDomains: !empty(customDomainName) ? [
{
name: customDomainName
certificateId: !empty(certificateId) ? certificateId : null
bindingType: !empty(certificateId) ? 'SniEnabled' : 'Disabled'
certificateId: !empty(customDomainCertificateId) ? customDomainCertificateId : null
bindingType: !empty(customDomainCertificateId) ? 'SniEnabled' : 'Disabled'
}
] : null
} : null
Expand Down
4 changes: 3 additions & 1 deletion infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ module containerAppEnvironment './containers/container-app-environment.bicep' =
var webAppServiceContainerAppName = buildServiceResourceName(abbrs.appContainerApps, projectName, webAppServiceName, environmentName, resourceToken, true)

var webAppServiceCustomDomainName = stringOrDefault(webAppConfig.infraSettings.customDomainName, '')
var webAppServiceCustomDomainCertificateId = stringOrDefault(webAppConfig.infraSettings.customDomainCertificateId, '')

var webAppServiceHostName = !empty(webAppServiceCustomDomainName) ? webAppServiceCustomDomainName : '${webAppServiceContainerAppName}.${containerAppEnvironment.outputs.defaultDomain}'
var webAppServiceHostName = !empty(webAppServiceCustomDomainName) && !empty(webAppServiceCustomDomainCertificateId) ? webAppServiceCustomDomainName : '${webAppServiceContainerAppName}.${containerAppEnvironment.outputs.defaultDomain}'

var webAppServiceUri = 'https://${webAppServiceHostName}'

Expand Down Expand Up @@ -179,6 +180,7 @@ module webApp './web-app.bicep' = {
exists: webAppExists
appConfig: webAppConfig
customDomainName: webAppServiceCustomDomainName
customDomainCertificateId: webAppServiceCustomDomainCertificateId
env: [
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
Expand Down
2 changes: 1 addition & 1 deletion infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
"webAppConfig": {
"value": {
"infraSettings": {
"certificateId": "${SERVICE_WEB_APP_CUSTOM_DOMAIN_CERT_ID}",
"containerCpuCoreCount": "${SERVICE_WEB_APP_CONTAINER_CPU_CORE_COUNT}",
"containerMemory": "${SERVICE_WEB_APP_CONTAINER_MEMORY}",
"containerMinReplicas": "${SERVICE_WEB_APP_CONTAINER_MIN_REPLICAS}",
"containerMaxReplicas": "${SERVICE_WEB_APP_CONTAINER_MAX_REPLICAS}",
"customDomainCertificateId": "${SERVICE_WEB_APP_CUSTOM_DOMAIN_CERT_ID}",
"customDomainName": "${SERVICE_WEB_APP_CUSTOM_DOMAIN_NAME}"
},
"appSettings": [
Expand Down
4 changes: 2 additions & 2 deletions infra/web-app.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ param exists bool
@secure()
param appConfig object
param allowedOrigins array = []
param certificateId string = ''
param containerCpuCoreCount string = '0.5'
param containerMaxReplicas int = 1
param containerMemory string = '1.0Gi'
param containerMinReplicas int = 0
param containerName string = 'main'
param customDomainName string = ''
param customDomainCertificateId string = ''
param env array = []
param external bool = true
param ingressEnabled bool = true
Expand Down Expand Up @@ -77,13 +77,13 @@ module containerApp './containers/container-app.bicep' = {
containerRegistryName: containerRegistry.name
userAssignedIdentityId: identity.id
allowedOrigins: allowedOrigins
certificateId: empty(appConfig.infraSettings.certificateId) ? certificateId : appConfig.infraSettings.certificateId
containerCpuCoreCount: empty(appConfig.infraSettings.containerCpuCoreCount) ? containerCpuCoreCount : appConfig.infraSettings.containerCpuCoreCount
containerMaxReplicas: empty(appConfig.infraSettings.containerMaxReplicas) ? containerMaxReplicas : int(appConfig.infraSettings.containerMaxReplicas)
containerMemory: empty(appConfig.infraSettings.containerMemory) ? containerMemory : appConfig.infraSettings.containerMemory
containerMinReplicas: empty(appConfig.infraSettings.containerMinReplicas) ? containerMinReplicas : int(appConfig.infraSettings.containerMinReplicas)
containerName: containerName
customDomainName: empty(appConfig.infraSettings.customDomainName) ? customDomainName : appConfig.infraSettings.customDomainName
customDomainCertificateId: empty(appConfig.infraSettings.customDomainCertificateId) ? customDomainCertificateId : appConfig.infraSettings.customDomainCertificateId
env: union(
env,
appEnv,
Expand Down
9 changes: 9 additions & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createRequestHandler } from "@remix-run/express"
import { installGlobals } from "@remix-run/node"
import express from "express"
import { useRewrite } from "./rewrite.js"
import { useRedirect } from "./redirect.js"
import { useCompression } from "./compression.js"
import { useSecurity } from "./security.js"
import { initAppInsights } from "./app-insights.js"
Expand All @@ -10,6 +11,9 @@ import { useLogging } from "./logging.js"
const mode = process.env.NODE_ENV
const isProductionMode = mode === "production"

const buildId = process.env.BUILD_ID
const isProductionBuild = !!buildId

if (!isProductionMode) {
// Load .env file when not in production mode
await import("dotenv").then((dotenv) => dotenv.config())
Expand Down Expand Up @@ -41,12 +45,17 @@ const getRequestHandler = async () => {

const app = express()

// Trust the X-Forwarded-* headers set by Azure Container Apps in a production build
app.set("trust proxy", isProductionBuild)

const appInsights = initAppInsights()

app.use(appInsights.logRequests)

useRewrite(app)

useRedirect(app)

useCompression(app)

useSecurity(app)
Expand Down
50 changes: 50 additions & 0 deletions server/redirect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export function useRedirect(app) {
const baseUrl = process.env.BASE_URL
let baseHostname = ""
let baseProtocol = ""

if (baseUrl) {
const url = new URL(baseUrl)
baseHostname = url.hostname
baseProtocol = url.protocol.replace(":", "")
}

if (baseHostname) {
// Redirect to the canonical hostname
app.use((req, res, next) => {
// The `x-azure-ref` is set when the requst comes from the CDN
// https://learn.microsoft.com/en-us/azure/frontdoor/front-door-http-headers-protocol#from-the-front-door-to-the-backend
if (req.hostname !== baseHostname && !req.headers["x-azure-ref"]) {
res.redirect(308, `${baseProtocol}://${baseHostname}${req.originalUrl}`)
return
}

next()
})
}

if (baseProtocol === "https") {
// Ensure HTTPS only
app.use((req, res, next) => {
if (req.protocol === "http") {
res.redirect(`https://${req.hostname}${req.originalUrl}`)
return
}

next()
})
}

// No trailing slashes on GET requests
app.get("*", (req, res, next) => {
if (req.path.endsWith("/") && req.path.length > 1) {
const query = req.url.slice(req.path.length)
const safepath = req.path.slice(0, -1).replace(/\/+/g, "/")

res.redirect(301, safepath + query)
return
}

next()
})
}

0 comments on commit e5bdda0

Please sign in to comment.