How do I replace a Policy Record with a simple A record that points to a CloudFront Distribution without traffic interruption? - amazon-cloudfront

My first question would be, what's the best way to do this? It looks to me like the only way to do it without interrupting traffic would be through the AWS CLI, but if there's an easier way I'm all ears! If the CLI is the way to do it, I'm running into issues with the following command:
aws route53 change-resource-record-sets --hosted-zone-id XXXXXXXXXXXX --change-batch file://update-record.json
The update-record.json contains the following:
{
"Comment": "Swaps the Policy Record for a simple routing policy",
"Changes": [
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "www.example.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "XXXXXXXXXXXX",
"DNSName": "xxxxxxxxxxxxx.cloudfront.net",
"EvaluateTargetHealth": false
}
}
}
]
}
The error I'm getting is:
An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation: [Tried to create an alias that targets xxxxxxxxxxxxx.cloudfront.net., type A in zone XXXXXXXXXXXX, but the alias target name does not lie within the target zone, Tried to create an alias that targets xxxxxxxxxxxxx.cloudfront.net., type A in zone XXXXXXXXXXXX, but that target was not found]
The Hosted Zone ID for the record in the account is accurate, as is the distribution DNS name. The record name, www.example.com, also exists in the account. The distribution has an alternate domain name (CNAME) of www.example.com.
I've also tried doing the change as a delete and create, rather than an upsert:
{
"Comment": "Swaps the Policy Record for a simple routing policy",
"Changes": [
{
"Action": "DELETE",
"ResourceRecordSet": {
"Name": "www.example.com.",
"Type": "A",
"TrafficPolicyInstanceId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
},
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "www.example.com.",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "XXXXXXXXXXXX",
"DNSName": "xxxxxxxxxxxxx.cloudfront.net",
"EvaluateTargetHealth": false
}
}
}
]
}
Doing this in two steps yields the same error message.

Each domain and subdomain in Route 53 is considered a "Hosted Zone", and as such has a "Hosted Zone ID". However, while the change-resource-record-sets documentation is extensive, it's not very clear about Alias records that point to a CloudFront distribution. I had to dig deeper, and look into the documentation around AliasTarget specifically. There, the documentation states:
Specify Z2FDTNDATAQYW2. This is always the hosted zone ID when you create an alias record that routes traffic to a CloudFront distribution.
This means that even though I was trying to create an Alias record in the hosted zone with an id of XXXXXXXXXXXX, the ID to use for any Alias record pointing to any CloudFront distribution is Z2FDTNDATAQYW2. Changing the update-record.json to the following works:
{
"Comment": "Swaps the Policy Record for a simple routing policy",
"Changes": [
{
"Action": "DELETE",
"ResourceRecordSet": {
"Name": "www.example.com.",
"Type": "A",
"TrafficPolicyInstanceId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
},
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "www.example.com.",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z2FDTNDATAQYW2",
"DNSName": "xxxxxxxxxxxxx.cloudfront.net",
"EvaluateTargetHealth": false
}
}
}
]
}
Doing this as a change-resource-record-set prevents interruption in traffic, as:
Change batches are considered transactional changes. Route 53 validates the changes in the request and then either makes all or none of the changes in the change batch request. This ensures that DNS routing isn't adversely affected by partial changes to the resource record sets in a hosted zone.

Related

Get zone id for an exisiting zone or create a new zone with zone name

I am using cdktf to generate terraform code, and want to get the zone_id for an existing zone so I can create new records in it.
"aws_route53_zone": {
"typescript-aws_get_zone_id_C1732EA4": {
"name": "mydomain.com",
"//": {
"metadata": {
"path": "custom_stack/typescript-aws/get_zone_id",
"uniqueId": "typescript-aws_get_zone_id_C1732EA4"
}
}
}
},
...
"aws_route53_record": {
"typescript-aws_aws_cloudfront_mydomaincom_aws_cloudfront_mydomaincom_record_5E08FD7F": {
"name": "mydomain.com",
"type": "A",
"zone_id": "${aws_route53_zone.typescript-aws_get_zone_id_C1732EA4.zone_id}",
"alias": [
{
"evaluate_target_health": false,
"name": "${aws_cloudfront_distribution.typescript-aws_aws_cloudfront_mydomaincom_4EF84BC8.domain_name}",
"zone_id": "${aws_cloudfront_distribution.typescript-aws_aws_cloudfront_mydomaincom_4EF84BC8.hosted_zone_id}"
}
],
"//": {
"metadata": {
"path": "custom_stack/typescript-aws/aws_cloudfront_mydomain.com/aws_cloudfront_mydomain.com_record",
"uniqueId": "typescript-aws_aws_cloudfront_mydomaincom_aws_cloudfront_mydomaincom_record_5E08FD7F"
}
}
},
However, this always creates a new zone (whose name matches an exisiting zone, but both have separate zone ids.
I referred to this question ( Fetch zone_id of hosted domain on route53 using terraform ) but seems like the flag suggested in the approved answer is no longer available? At least not as per the docs ( https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_zone )
In Terraform resources create resources and data sources load the data. You should be able to use data_route53_zone, which should be aws.route53. DataAwsRoute53Zone.

Azure policy modify effect

I have a azure custom policy, it checks all storage account, if there's no VNet and subnet setup on them as selected network, it would go and modify them to have VNet integration according to the parameters I entered. The parameter I entered is an array of subnet info as following
"allowedNetworks": {
"type": "array",
"metadata": {
"description": "The list of allowed virtual networks",
"displayName": "Allowed Networks"
},
"defaultValue": [
{
"id": "/subscriptions/xxx/resourceGroups/test3/providers/Microsoft.Network/virtualNetworks/rogertest3-vnet/subnets/default",
"action": "Allow",
"state": "Succeeded"
},
{
"id": "/subscriptions/xxx/resourceGroups/test3/providers/Microsoft.Network/virtualNetworks/rogertest3-vnet/subnets/AzureBastionSubnet",
"action": "Allow",
"state": "Succeeded"
}
]
}
and the effect is as following
"then": {
"effect": "[parameters('effect')]",
"details": {
"roleDefinitionIds": [
"/providers/microsoft.authorization/roleDefinitions/17d1049b-9a84-46fb-8f53-869881c3d3ab"
],
"conflictEffect": "audit",
"operations": [
{
"operation": "addOrReplace",
"field": "Microsoft.Storage/storageAccounts/networkAcls.virtualNetworkRules",
"value": "[parameters('allowednetworks')]"
},
{
"operation": "addOrReplace",
"field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction",
"value": "Deny"
}
]
}
}
it works well, however there're some behaviours around this modify effect I'm bit confused about.
If I create a new storage account, and it falls under the scope of this policy. I notice it would automatically adds this VNet integration, even if I select "all networks" at the time of creation
If I try manually change any storage account to all network, the UI would quickly revert to VNet integration, so it's not doing anything, and it would not give an error message. Doing with powershell gives the same result.
This is a bit contradictory to what I understand as modify effect, I thought modify effect is not mandatory, it would only apply to storage accounts, if you go with remediation
actually It is by design, just found out.
Modify effect gives this desired state configuration effect, so when you create something, policy will evaluate it, if it fits with the policy, Policy will take effect.

Azure CORS Variable Allowed Origins

Whenever a new release pipeline is ran in Azure DevOps, the URL Is changed.. currently my ARM template has a hard-coded URL which can be annoying to keep on adding in manually.
"cors": {
"allowedOrigins": [
"[concat('https://',parameters('storage_account_name'),'.z10.web.core.windows.net')]"
}
The only thing that changes is the 10 part in the z10 so essentially i want it to be something like
[concat('https://',parameters('storage_account_name'),'.z', '*', '.web.core.windows.net')] I dont know if something like that is valid but essentially its so that the cors policy will accept the URL regardless of the z number.
Basically speaking this is not possible, because of the CORS standard (see docs).
which allows only for exact origins, wildcard, or null.
For instance, ARM for Azure Storage is also following this pattern allowing you to put a list of exact origins or a wildcard (see ARM docs)
However, if you know your website name, in your ARM you can receive the full host and use it in your CORS:
"[reference(resourceId('Microsoft.Web/sites', parameters('SiteName')), '2018-02-01').defaultHostName]"
The same with a static website (which is your case I guess) if you know the storage account name:
"[reference(concat('Microsoft.Storage/storageAccounts/', variables('storageAccountName')), '2019-06-01', 'Full').properties.primaryEndpoints.web]"
Advance reference output manipulation
Answering on comment - if you would like to replace some characters in the output from the reference function the easiest way is to use build-in replace function (see docs)
In case you need a more advanced scenario I am pasting my solution by introducing a custom function which is removing https:// and / from the end so https://contonso.com/ is transformed to contonso.com:
"functions": [
{
"namespace": "lmc",
"members": {
"replaceUri": {
"parameters": [
{
"name": "uriString",
"type": "string"
}
],
"output": {
"type": "string",
"value": "[replace(replace(parameters('uriString'), 'https://',''), '/','')]"
}
}
}
}
],
# ...(some code)...
"resources": [
# ... (some resource)...:
"properties": {
"hostName": "[lmc.replaceUri(reference(variables('storageNameCdn')).primaryEndpoints.blob)]"
}
]

Enable HTTPS on Azure Front Door custom domain with ARM template deployment

I am deploying an Azure Front Door via an ARM template, and attempting to enable HTTPS on a custom domain.
According to the Azure documentation for Front Door, there is a quick start template to "Add a custom domain to your Front Door and enable HTTPS traffic for it with a Front Door managed certificate generated via DigiCert." However, while this adds a custom domain, it does not enable HTTPS.
Looking at the ARM template reference for Front Door, I can't see any obvious way to enable HTTPS, but perhaps I'm missing something?
Notwithstanding the additional information below, I'd like to be able to enable HTTPS on a Front Door custom domain via an ARM template deployment. Is this possible at this time?
Additional information
Note that there is a REST operation to enable HTTPS, but this does not seem to work with a Front Door managed certificate -
POST https://management.azure.com/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/frontDoors/{frontDoorName}/frontendEndpoints/{frontendEndpointName}/enableHttps?api-version=2019-05-01
{
"certificateSource": "FrontDoor",
"protocolType": "ServerNameIndication",
"minimumTLSVersion": "1.2"
}
There is also a Az PowerShell cmdlet to enable HTTP, which does work.
Enable-AzFrontDoorCustomDomainHttps -ResourceGroupName "lmk-bvt-accounts-front-door" -FrontDoorName "my-front-door" -FrontendEndpointName "my-front-door-rg"
UPDATE: This implementation currently seems to be unstable and is working only intermittently, which indicates it may not be production ready yet.
This now actually seems to be possible with ARM templates, after tracking down the latest Front Door API (2020-01-01) specs (which don't appear to be fully published in the MS reference websites yet):
https://github.com/Azure/azure-rest-api-specs/tree/master/specification/frontdoor/resource-manager/Microsoft.Network/stable/2020-01-01
There's a new customHttpsConfiguration property in the frontendEndpoint properties object:
"customHttpsConfiguration": {
"certificateSource": "AzureKeyVault" // or "FrontDoor",
"minimumTlsVersion":"1.2",
"protocolType": "ServerNameIndication",
// Depending on "certificateSource" you supply either:
"keyVaultCertificateSourceParameters": {
"secretName": "<secret name>",
"secretVersion": "<secret version>",
"vault": {
"id": "<keyVault ResourceID>"
}
}
// Or:
"frontDoorCertificateSourceParameters": {
"certificateType": "Dedicated"
}
}
KeyVault Managed SSL Certificate Example
Note: I have tested this and appears to work.
{
"type": "Microsoft.Network/frontdoors",
"apiVersion": "2020-01-01",
"properties": {
"frontendEndpoints": [
{
"name": "[variables('frontendEndpointName')]",
"properties": {
"hostName": "[variables('customDomain')]",
"sessionAffinityEnabledState": "Enabled",
"sessionAffinityTtlSeconds": 0,
"webApplicationFirewallPolicyLink": {
"id": "[variables('wafPolicyResourceId')]"
},
"resourceState": "Enabled",
"customHttpsConfiguration": {
"certificateSource": "AzureKeyVault",
"minimumTlsVersion":"1.2",
"protocolType": "ServerNameIndication",
"keyVaultCertificateSourceParameters": {
"secretName": "[parameters('certKeyVaultSecret')]",
"secretVersion": "[parameters('certKeyVaultSecretVersion')]",
"vault": {
"id": "[resourceId(parameters('certKeyVaultResourceGroupName'),'Microsoft.KeyVault/vaults',parameters('certKeyVaultName'))]"
}
}
}
}
}
],
...
}
}
Front Door Managed SSL Certificate Example
Looks like for a FrontDoor managed certificate you would need to set:
Note: I have not tested this
{
"type": "Microsoft.Network/frontdoors",
"apiVersion": "2020-01-01",
"properties": {
"frontendEndpoints": [
{
"name": "[variables('frontendEndpointName')]",
"properties": {
"hostName": "[variables('customDomain')]",
"sessionAffinityEnabledState": "Enabled",
"sessionAffinityTtlSeconds": 0,
"webApplicationFirewallPolicyLink": {
"id": "[variables('wafPolicyResourceId')]"
},
"resourceState": "Enabled",
"customHttpsConfiguration": {
"certificateSource": "FrontDoor",
"minimumTlsVersion":"1.2",
"protocolType": "ServerNameIndication",
"frontDoorCertificateSourceParameters": {
"certificateType": "Dedicated"
}
}
}
}
],
...
}
}
I was able to successfully make an enableHttps REST Call using the Azure Management API.
I got a successful response and can see the resource results in the portal.azure.com and resource.azure.com sites.
However I am pretty sure the Management API, and PowerShell methods are the only ways supported right now. Since there is likely some validation required on the Certificate and Handling, they didn't include that yet in the ARM Templates. Given validation can be quite important, it is best you confirm your configuration is workable in the UI first, before automating it (IMHO).
According to this discussion this seems only possible via the REST API (see e.g. this answer) and not (yet) via ARM.
I managed to get this working with an ARM template. The below link shows you how to do this using Azure Front Door as a certificate source:
https://github.com/Azure/azure-quickstart-templates/blob/master/101-front-door-custom-domain/azuredeploy.json
I drew inspiration from this for deploying a certificate from Azure Key Vault for a custom domain. Here are the relevant elements from the ARM template that I am using:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"hubName": {
"type": "string",
"metadata": {
"description": "Name to assign to the hub. This name will prefix all resources contained in the hub."
}
},
"frontdoorName": {
"type": "string",
"metadata": {
"description": "Name to assign to the Frontdoor instance"
}
},
"frontdoorCustomDomain": {
"type": "string",
"metadata": {
"description": "The custom domain name to be applied to the provisioned Azure Frontdoor instance"
}
},
"keyVaultCertificateName": {
"type": "string",
"metadata": {
"description": "Name of the TLS certificate in the Azure KeyVault to be deployed to Azure Frontdoor for supporting TLS over a custom domain",
"assumptions": [
"Azure KeyVault containing the TLS certificate is deployed to the same resource group as the resource group where Azure Frontdoor will be deployed to",
"Azure KeyVault name is the hub name followed by '-keyvault' (refer to variable 'keyVaultName' in this template)"
]
}
},
...
},
"variables": {
"frontdoorName": "[concat(parameters('hubName'), '-', parameters('frontdoorName'))]",
"frontdoorEndpointName": "[concat(variables('frontdoorName'), '-azurefd-net')]",
"customDomainFrontdoorEndpointName": "[concat(variables('frontdoorName'), '-', replace(parameters('frontdoorCustomDomain'), '.', '-'))]",
"keyVaultName": "[concat(parameters('hubName'), '-keyvault')]",
"frontdoorHostName": "[concat(variables('frontdoorName'), '.azurefd.net')]",
...
},
"resources": [
{
"type": "Microsoft.Network/frontdoors",
"apiVersion": "2020-05-01",
"name": "[variables('frontdoorName')]",
"location": "Global",
"properties": {
"resourceState": "Enabled",
"backendPools": [...],
"healthProbeSettings": [...],
"frontendEndpoints": [
{
"id": "[concat(resourceId('Microsoft.Network/frontdoors', variables('frontdoorName')), concat('/FrontendEndpoints/', variables('frontdoorEndpointName')))]",
"name": "[variables('frontdoorEndpointName')]",
"properties": {
"hostName": "[variables('frontdoorHostName')]",
"sessionAffinityEnabledState": "Enabled",
"sessionAffinityTtlSeconds": 0,
"resourceState": "Enabled"
}
},
{
"id": "[concat(resourceId('Microsoft.Network/frontdoors', variables('frontdoorName')), concat('/FrontendEndpoints/', variables('customDomainFrontdoorEndpointName')))]",
"name": "[variables('customDomainFrontdoorEndpointName')]",
"properties": {
"hostName": "[parameters('frontdoorCustomDomain')]",
"sessionAffinityEnabledState": "Enabled",
"sessionAffinityTtlSeconds": 0,
"resourceState": "Enabled"
}
}
],
"loadBalancingSettings": [...],
"routingRules": [...],
"backendPoolsSettings": {
"enforceCertificateNameCheck": "Enabled",
"sendRecvTimeoutSeconds": 30
},
"enabledState": "Enabled",
"friendlyName": "[variables('frontdoorName')]"
}
},
{
"type": "Microsoft.Network/frontdoors/frontendEndpoints/customHttpsConfiguration",
"apiVersion": "2020-07-01",
"name": "[concat(variables('frontdoorName'), '/', variables('customDomainFrontdoorEndpointName'), '/default')]",
"dependsOn": [
"[resourceId('Microsoft.Network/frontdoors', variables('frontdoorName'))]"
],
"properties": {
"protocolType": "ServerNameIndication",
"certificateSource": "AzureKeyVault",
"minimumTlsVersion": "1.2",
"keyVaultCertificateSourceParameters": {
"secretName": "[parameters('keyVaultCertificateName')]",
"vault": {
"id": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults', variables('keyVaultName'))]"
}
}
}
}
]
}
Azure Front Door classic now seems to support both managed certificates and custom certificates for custom domains. At least there are quickstart templates in the official repo from Microsoft exactly for these cases:
managed certificate
custom certificate
They both use Microsoft.Network/frontdoors/frontendEndpoints/customHttpsConfiguration subresource of the Front Door, currently with API version 2020-07-01. Only the parent subresource is documented in the templates reference, though.
The name of the customHttpsConfiguration resource is "default", so when the resource is specified as a top-level resource in the template, its complete name is something like "myfrontdoorafd/www-example-com/default".
Using Bicep (which transpiles to JSON ARM templates and which I highly recommend), the important part of the template looks like this:
param frontDoorName string
param customDomainName string
var frontEndEndpointCustomName = replace(customDomainName, '.', '-')
resource frontDoor 'Microsoft.Network/frontDoors#2020-01-01' = {
name: frontDoorName
properties: {
frontendEndpoints: [
{
name: frontEndEndpointCustomName
properties: {
hostName: customDomainName
...
}
}
...
]
...
}
...
resource frontendEndpoint 'frontendEndpoints' existing = {
name: frontEndEndpointCustomName
}
}
// This resource enables a Front Door-managed TLS certificate on the frontend.
resource customHttpsConfiguration 'Microsoft.Network/frontdoors/frontendEndpoints/customHttpsConfiguration#2020-07-01' = {
parent: frontDoor::frontendEndpoint
name: 'default'
properties: {
protocolType: 'ServerNameIndication'
certificateSource: 'FrontDoor'
frontDoorCertificateSourceParameters: {
certificateType: 'Dedicated'
}
minimumTlsVersion: '1.2'
}
}
Note that the deployment will be in progress till the certificate is actually issued and deployed to all points of presence (PoP) of Azure. This may take really long and even fail due to RequestTimeout. If you want to just start the operation and let it complete asynchronously, use e.g. the enable-https subcommand in Azure CLI. Even after the failure, the customHttpsProvisioningState is Pending and the certificate provisioning process may complete successfully.
Also note that when you have many frontend endpoints and changes happen frequently but most frontend endpoints stay unchanged, the pattern from this template cannot be generalized just by specifying multiple customHttpsConfiguration instances for multiple frontend endpoints. Such a generalization is not efficient and likely hits the rate limit of the underlying API (429 TooManyRequests) because the API is called even when the endpoint already has the HTTPS configuration.
In such a case, I was able to use nested templates and conditional deployment to deploy the customHttpsConfiguration subresource only when the frontend endpoint's property customHttpsProvisioningState has the value of Disabled. This works OK even with tens of frontend endpoints when a new frontend endpoint is added (and it should get a managed certificate). Even in deployment mode Complete, the once-applied configuration persists.

Policy to deny dynamic public IP configuration in Azure Application Gateway

I am trying to create an Azure policy to deny the creation of application gateway if the public IP address allocation is "Dynamic".
I tried using the "publicIPAllocationMethod" as a strong type but it throws me an error.
I am seeing these fields are coming in the property and not a type.
{
"mode": "all",
"policyRule": {
"if": {
"allOf": [
{
"field": "publicIPAddress",
"equals": "dynamic"
}
]
},
"then": {
"effect": "deny"
}
},
"parameters": {}
}
policy works on an aliases basis so the field must be an aliases. Upon my research, it looks like the aliases for publicIDAddress within Application Gateway is Microsoft.Network/applicationGateways/frontendIPConfigurations[*].publicIPAddress.
However the value in my resource payload is an id.
The only thing that I see that can be labeled as dynamic is Microsoft.Network/applicationGateways/frontendIPConfigurations[*].privateIPAllocationMethod but that could just be how my resource is configured.
I would recommend seeing here on how to find aliases: https://learn.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure#aliases

Resources