Skip to content

EXIP base infrastructure build from UK-Export-Finance/exip #93

EXIP base infrastructure build from UK-Export-Finance/exip

EXIP base infrastructure build from UK-Export-Finance/exip #93

Workflow file for this run

# This code snippet is a YAML configuration file for a GitHub Actions workflow. It defines a series of jobs and steps to set up infrastructure, create base
# infrastructure resources, configure security settings, configure a web app, enable logging and diagnostic settings, and set up health check alerts.
# The workflow is triggered on pushes to the 'infrastructure' and 'EMS-2898-database-tier' branches. It uses environment variables to define the product, environment,
# timezone, and target. The jobs are organized into sections for setup, base infrastructure creation, security configuration, web app configuration, logging and diagnostic settings, and health check alerts.
# Each step in the workflow performs a specific task using the Azure CLI, such as creating resource groups, app service plans, log analytics workspaces, container registries,
# virtual networks, subnets, VNET peering, databases, web apps, front doors, WAF policies, private endpoints, DNS configurations, and more.
# The workflow also includes conditional steps based on the target environment, such as different configurations for pre-production and production environments.
# Overall, this code snippet provides a comprehensive configuration for setting up infrastructure and configuring various resources in an Azure environment using GitHub Actions.
#
# Inputs
# ------
# The inputs for this code snippet include the environment, target, and various variables such as region, database SKU, database tier, app service plan SKU, log quota, log retention day, log plan,
# VNET address prefix, VNET subnet database prefix, VNET subnet web app prefix, VNET subnet private prefix, health probe path, health probe protocol, health probe HTTP method, health probe latency,
# load balancing sample size, load balancing samples, load balancing interval, TLS version, web app log level, web app log destination, action group email, and various secrets.
#
# Flow
# ----
# The code starts by setting up infrastructure variables and outputs the environment and timezone.
# It then creates the base infrastructure, including the resource group, app service plan, log analytics workspace, container registry, virtual network,
# subnets, VNET peering, database, web app, and front door.
# Next, it sets up security measures such as private endpoints, private DNS, and WAF policies.
# It then configures the web app, including enabling continuous deployment, setting configuration variables, and creating a database connection.
# The code sets up diagnostic settings for logging and monitoring of various resources.
# Finally, it creates an alert for health checks on the front door.
#
# Outputs
# -------
# The outputs of this code snippet include the environment and various resources created, such as the resource group, app service plan, log analytics workspace, container
# registry, virtual network, subnets, database, web app, front door, and security policies.
#
name: Infrastructure πŸ”¨
run-name: EXIP base infrastructure build from ${{ github.repository }}
on:
push:
branches:
- infrastructure
# TODO: Remove below prior to merge
- feat/EMS-2186-ia-c-key-vault-cost-alerts
env:
PRODUCT: exip
ENVIRONMENT: infrastructure
TIMEZONE: '${{ vars.TIMEZONE }}'
# Deployment environment target i.e., `development`, `staging`, `production`
TARGET: ${{ vars.ENVIRONMENT }}
jobs:
# 1. Setup infrastructure variables
setup:
name: Setup πŸ”§
runs-on: [self-hosted, EXIP, infrastructure]
outputs:
environment: ${{ env.ENVIRONMENT }}
timezone: ${{ env.TIMEZONE }}
steps:
- name: Environment πŸ§ͺ
run: echo "Environment set to ${{ env.ENVIRONMENT }}"
- name: Timezone 🌐
run: echo "Timezone set to ${{ env.TIMEZONE }}"
# 2. Base infrastructure creation
base:
name: Base 🧱
needs: setup
environment: ${{ needs.setup.outputs.environment }}
outputs:
environment: ${{ env.ENVIRONMENT }}
runs-on: [self-hosted, EXIP, infrastructure]
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "feature", "staging"]', env.TARGET)
run: |
echo "TYPE=Preproduction" >> $GITHUB_ENV
echo "DB_SKU=${{ VARS.DB_NON_PROD_SKU }}" >> $GITHUB_ENV
echo "DB_TIER=${{ VARS.DB_NON_PROD_TIER }}" >> $GITHUB_ENV
- name: Production πŸ’«
if: ${{ 'production' == env.TARGET }}
run: |
echo "TYPE=Production" >> $GITHUB_ENV
echo "DB_SKU=${{ VARS.DB_PROD_SKU }}" >> $GITHUB_ENV
echo "DB_TIER=${{ VARS.DB_PROD_TIER }}" >> $GITHUB_ENV
- name: Tags 🏷️
run: echo TAGS='Environment=${{ env.TYPE }}' \
'Product=${{ env.PRODUCT }}' \
'Team=development' >> $GITHUB_ENV
- name: Login πŸ”
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: azure/cli@v2
with:
inlineScript: |
# Basic
az configure --defaults location=${{ vars.REGION }}
az configure --defaults group=rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
- name: Resource group πŸ—οΈ
uses: azure/cli@v2
with:
inlineScript: |
az group create \
--name rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--tags ${{ env.TAGS }}
- name: Plan πŸ“
uses: azure/cli@v2
with:
inlineScript: |
az appservice plan create \
--name appservice-plan-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--is-linux \
--sku ${{ vars.APP_SERVICE_PLAN }} \
--tags ${{ env.TAGS }}
- name: Log analytics workspace πŸ“
uses: azure/cli@v2
with:
inlineScript: |
az monitor log-analytics workspace create \
--name log-workspace-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--quota ${{ vars.LOG_QUOTA }} \
--retention-time ${{ vars.LOG_RETENTION_DAY }} \
--sku ${{ vars.LOG_PLAN }} \
--tags ${{ env.TAGS }}
- name: Container registry πŸ“¦οΈ
uses: azure/cli@v2
with:
inlineScript: |
az acr create \
--name cr${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }} \
--sku Standard \
--admin-enabled true \
--workspace log-workspace-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--tags ${{ env.TAGS }}
- name: Virtual network 🧡
uses: azure/cli@v2
with:
inlineScript: |
az network vnet create \
--name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--address-prefix ${{ vars.VNET_ADDRESS_PREFIX }} \
--tags ${{ env.TAGS }}
- name: Subnets πŸ’«
uses: azure/cli@v2
with:
inlineScript: |
# WebApp
az network vnet subnet create \
--name snet-webapp-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--address-prefixes ${{ vars.VNET_SUBNET_WEBAPP_PREFIX }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
# Private endpoint
az network vnet subnet create \
--name snet-private-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--address-prefixes ${{ vars.VNET_SUBNET_PRIVATE_PREFIX }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
az network vnet subnet create \
--name snet-keyvault-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--address-prefixes ${{ vars.VNET_SUBNET_KEYVAULT_PREFIX }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--service-endpoints Microsoft.KeyVault
- name: VNET Peer - AMI πŸ”€
uses: azure/cli@v2
with:
inlineScript: |
# Azure Managed Instance (AMI) SQL DB VNET peering
# Local VNET peer
az network vnet peering create \
--name vnet-peer-ami-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--remote-vnet $(az network vnet show --subscription ${{ secrets.REMOTE_VNET_SUBSCRIPTION_AMI }} --resource-group ${{ secrets.REMOTE_VNET_RESOURCE_GROUP_AMI }} --name ${{ secrets.REMOTE_VNET_NAME_AMI }} --query 'id' -o tsv) \
--allow-vnet-access 1
# Remote VNET peer
az network vnet peering create \
--name vnet-peer-ami-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name ${{ secrets.REMOTE_VNET_NAME_AMI }} \
--remote-vnet $(az network vnet show --name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} --query 'id' -o tsv) \
--allow-vnet-access 1 \
--subscription ${{ secrets.REMOTE_VNET_SUBSCRIPTION_AMI }} \
--resource-group ${{ secrets.REMOTE_VNET_RESOURCE_GROUP_AMI }}
# Fetch peering state
echo "Peering state: $(az network vnet peering show \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--name vnet-peer-ami-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--query peeringState)"
- name: VNET Peer - APIM πŸ”€
uses: azure/cli@v2
with:
inlineScript: |
# API management (APIM) VNET peering
# Local VNET peer
az network vnet peering create \
--name vnet-peer-apim-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--remote-vnet $(az network vnet show --subscription ${{ secrets.REMOTE_VNET_SUBSCRIPTION_APIM }} --resource-group ${{ secrets.REMOTE_VNET_RESOURCE_GROUP_APIM }} --name ${{ secrets.REMOTE_VNET_NAME_APIM }} --query 'id' -o tsv) \
--allow-vnet-access 1
# Remote VNET peer
az network vnet peering create \
--name vnet-peer-apim-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name ${{ secrets.REMOTE_VNET_NAME_APIM }} \
--remote-vnet $(az network vnet show --name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} --query 'id' -o tsv) \
--allow-vnet-access 1 \
--subscription ${{ secrets.REMOTE_VNET_SUBSCRIPTION_APIM }} \
--resource-group ${{ secrets.REMOTE_VNET_RESOURCE_GROUP_APIM }}
# Fetch peering state
echo "Peering state: $(az network vnet peering show \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--name vnet-peer-apim-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--query peeringState)"
- name: VNET Peer - UKS πŸ”€
uses: azure/cli@v2
with:
inlineScript: |
# UKS VNET peering
# Local VNET peer
az network vnet peering create \
--name vnet-peer-uks-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--remote-vnet $(az network vnet show --subscription ${{ secrets.REMOTE_VNET_SUBSCRIPTION_UKS }} --resource-group ${{ secrets.REMOTE_VNET_RESOURCE_GROUP_UKS }} --name ${{ secrets.REMOTE_VNET_NAME_UKS }} --query 'id' -o tsv) \
--allow-vnet-access 1
# Remote VNET peer
az network vnet peering create \
--name vnet-peer-uks-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name ${{ secrets.REMOTE_VNET_NAME_UKS }} \
--remote-vnet $(az network vnet show --name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} --query 'id' -o tsv) \
--allow-vnet-access 1 \
--subscription ${{ secrets.REMOTE_VNET_SUBSCRIPTION_UKS }} \
--resource-group ${{ secrets.REMOTE_VNET_RESOURCE_GROUP_UKS }}
# Fetch peering state
echo "Peering state: $(az network vnet peering show \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--name vnet-peer-uks-${{ env.TARGET }}-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--query peeringState)"
- name: Database πŸ’Ύ
uses: azure/cli@v2
with:
inlineScript: |
az mysql flexible-server create \
--name sqldb-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--admin-user ${{ secrets.MYSQL_USER }} \
--admin-password ${{ secrets.MYSQL_PASSWORD }} \
--database-name ${{ env.PRODUCT }} \
--storage-size ${{ vars.DB_STORAGE_SIZE_GB }} \
--sku-name ${{ env.DB_SKU }} \
--tier ${{ env.DB_TIER }} \
--version ${{ vars.DB_VERSION }} \
--vnet vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--subnet snet-database-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--address-prefixes ${{ vars.VNET_ADDRESS_PREFIX }} \
--subnet-prefix ${{ vars.VNET_SUBNET_DATABASE_PREFIX }} \
--backup-retention ${{ vars.DB_DR_DAYS }} \
--auto-scale-iops Enabled \
--storage-auto-grow Enabled \
--yes \
--tags ${{ env.TAGS }}
- name: Web app - UI 🌐
uses: azure/cli@v2
with:
inlineScript: |
az webapp create \
--name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--plan appservice-plan-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--deployment-container-image-name cr${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }}.azurecr.io/ui:${{ env.TARGET }} \
--vnet vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--subnet snet-webapp-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--https-only true \
--tags ${{ env.TAGS }}
- name: Web app - API 🌐
uses: azure/cli@v2
with:
inlineScript: |
az webapp create \
--name app-${{ env.PRODUCT }}-api-${{ env.TARGET }}-${{ vars.VERSION }} \
--plan appservice-plan-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--deployment-container-image-name cr${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }}.azurecr.io/api:${{ env.TARGET }} \
--vnet vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--subnet snet-webapp-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--https-only true \
--tags ${{ env.TAGS }}
- name: Front Door πŸšͺ
uses: azure/cli@v2
with:
inlineScript: |
# Profile
az afd profile create \
--profile-name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--sku ${{ secrets.AZURE_FRONT_DOOR_SKU }} \
--tags ${{ env.TAGS }}
# Endpoint
az afd endpoint create \
--endpoint-name ${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--profile-name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--tags ${{ env.TAGS }}
# Origin Group
az afd origin-group create \
--origin-group-name ui \
--probe-path ${{ vars.HEALTH_PROBE_PATH }} \
--probe-protocol ${{ vars.HEALTH_PROBE_PROTOCOL }} \
--probe-request-type ${{ vars.HEALTH_PROBE_HTTP_METHOD }} \
--additional-latency-in-milliseconds ${{ vars.HEALTH_PROBE_LATENCY }} \
--sample-size ${{ vars.LOAD_BALANCING_SAMPLE_SIZE }} \
--successful-samples-required ${{ vars.LOAD_BALANCING_SAMPLES }} \
--probe-interval-in-seconds ${{ vars.LOAD_BALANCING_INTERVAL }} \
--profile-name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
# 3. Security
security:
name: Security πŸ”‘
needs: base
environment: ${{ needs.base.outputs.environment }}
runs-on: [self-hosted, EXIP, infrastructure]
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "feature", "staging"]', env.TARGET)
run: echo "TYPE=Preproduction" >> $GITHUB_ENV
- name: Production πŸ’«
if: ${{ 'production' == env.TARGET }}
run: echo "TYPE=Production" >> $GITHUB_ENV
- name: Tags 🏷️
run: echo TAGS='Environment=${{ env.TYPE }}' \
'Product=${{ env.PRODUCT }}' \
'Team=development' >> $GITHUB_ENV
- name: Login πŸ”
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: azure/cli@v2
with:
inlineScript: |
# Basic
az configure --defaults location=${{ vars.REGION }}
az configure --defaults group=rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
- name: Extension βž•
uses: azure/cli@v2
with:
inlineScript: |
az extension add --name front-door
- name: Key Vault πŸ”‘
uses: azure/cli@v2
with:
inlineScript: |
az keyvault create \
--name kv-${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }} \
--default-action Deny \
--enable-purge-protection true \
--public-network-access Disabled \
--network-acls-ips ${{ secrets.WAF_ALLOWED_IP }} \
--network-acls-vnets $(az network vnet subnet list --vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} --query '[?contains(name, `keyvault`)].id' -o tsv)
- name: Private endpoint πŸ”
uses: azure/cli@v2
with:
inlineScript: |
#UI
az network private-endpoint create \
--name private-endpoint-ui-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--private-connection-resource-id $(az webapp list --query '[?contains(name, `app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }}`)].id' -o tsv) \
--connection-name private-link-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--subnet snet-private-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--group-id sites \
--tags ${{ env.TAGS }}
#API
az network private-endpoint create \
--name private-endpoint-api-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--private-connection-resource-id $(az webapp list --query '[?contains(name, `app-${{ env.PRODUCT }}-api-${{ env.TARGET }}-${{ vars.VERSION }}`)].id' -o tsv) \
--connection-name private-link-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--subnet snet-private-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--group-id sites \
--tags ${{ env.TAGS }}
#Key Vault
az network private-endpoint create \
--name private-endpoint-keyvault${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--private-connection-resource-id $(az keyvault show --name kv-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} --query id -o tsv) \
--connection-name private-link-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--subnet snet-private-${{ env.PRODUCT }}-${{ vars.VERSION }} \
--vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--group-id $(az network private-link-resource list --id $(az keyvault show --name kv-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} --query id -o tsv)) \
--tags ${{ env.TAGS }}
- name: Private DNS 🌍
uses: azure/cli@v2
with:
inlineScript: |
az network private-dns zone create \
--name ${{ secrets.PRIVATE_DNS_ZONE_NAME }} \
--tags ${{ env.TAGS }}
- name: DNS VNET βž•
uses: azure/cli@v2
with:
inlineScript: |
az network private-dns link vnet create \
--zone-name ${{ secrets.PRIVATE_DNS_ZONE_NAME }} \
--name dns-vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--virtual-network vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--registration-enabled false \
--tags ${{ env.TAGS }}
- name: DNS Records πŸ“
uses: azure/cli@v2
with:
inlineScript: |
#UI
az network private-endpoint dns-zone-group create \
--endpoint-name private-endpoint-ui-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--name ${{ env.PRODUCT }} \
--private-dns-zone ${{ secrets.PRIVATE_DNS_ZONE_NAME }} \
--zone-name webapp
#API
az network private-endpoint dns-zone-group create \
--endpoint-name private-endpoint-api-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--name ${{ env.PRODUCT }} \
--private-dns-zone ${{ secrets.PRIVATE_DNS_ZONE_NAME }} \
--zone-name webapp
- name: Front Door πŸšͺ
uses: azure/cli@v2
with:
inlineScript: |
# Origin
az afd origin create \
--origin-name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--host-name $(az webapp show --name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} --query defaultHostName -o tsv) \
--origin-host-header $(az webapp show --name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} --query defaultHostName -o tsv) \
--priority 1 \
--weight 1000 \
--enable-private-link true \
--private-link-location ${{ vars.REGION }} \
--private-link-resource $(az webapp show --name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} --query id -o tsv) \
--private-link-request-message '${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }}' \
--private-link-sub-resource-type $(az network private-link-resource list --id $(az webapp show --name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} --query id -o tsv) --query [].name -o tsv) \
--origin-group-name ui \
--profile-name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--enabled-state Enabled
# Route
az afd route create \
--route-name default \
--forwarding-protocol HttpsOnly \
--https-redirect Enabled \
--supported-protocols Http Https \
--enable-caching false \
--link-to-default-domain Enabled \
--origin-path / \
--origin-group ui \
--endpoint-name ${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--profile-name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--enabled-state Enabled
- name: WAF πŸ”₯
uses: azure/cli@v2
with:
inlineScript: |
# WAF
az network front-door waf-policy create \
--name waf${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }} \
--custom-block-response-body "${{ vars.WAF_BLOCK_MESSAGE }}" \
--custom-block-response-status-code ${{ vars.WAF_BLOCK_CODE }} \
--redirect-url ${{ vars.WAF_REDIRECT_URL }} \
--mode ${{ vars.WAF_MODE }} \
--sku ${{ secrets.AZURE_FRONT_DOOR_SKU }} \
--tags ${{ env.TAGS }}
# Managed rule - DR
az network front-door waf-policy managed-rules add \
--version 2.1 \
--type Microsoft_DefaultRuleSet \
--action Block \
--policy-name waf${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }}
# Managed rule - Bot
az network front-door waf-policy managed-rules add \
--version 1.0 \
--type Microsoft_BotManagerRuleSet \
--action Block \
--policy-name waf${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }}
# Associate FD
az afd security-policy create \
--security-policy-name security-policy-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--waf-policy $(az network front-door waf-policy list --query [].id -o tsv) \
--domains $(az afd endpoint list --profile-name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} --query [].id -o tsv) \
--profile-name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
# Custom rule - RL
az network front-door waf-policy rule create \
--action Block \
--name HTTPRateLimitRule \
--policy-name waf${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }} \
--priority 2 \
--rule-type RateLimitRule \
--rate-limit-duration ${{ vars.WAF_RL_DURATION }} \
--rate-limit-threshold ${{ vars.WAF_RL_THRESHOLD }} \
--defer
# Custom rule - RL condition
az network front-door waf-policy rule match-condition add \
--match-variable ${{ vars.WAF_RL_MATCH }} \
--operator GreaterThanOrEqual \
--values ${{ vars.WAF_RL_MATCH_VALUE }} \
--name HTTPRateLimitRule \
--policy-name waf${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }}
- name: VPN 🧱
if: contains('["dev", "feature", "staging"]', env.TARGET)
uses: azure/cli@v2
with:
inlineScript: |
# Custom rule - IP
az network front-door waf-policy rule create \
--action Redirect \
--name IPAllowListRule \
--policy-name waf${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }} \
--priority 1 \
--rule-type MatchRule \
--defer
# Custom rule - IP condition
az network front-door waf-policy rule match-condition add \
--match-variable ${{ vars.WAF_IP_ADDRESS }} \
--operator IPMatch \
--values ${{ secrets.WAF_ALLOWED_IP }} \
--negate \
--name IPAllowListRule \
--policy-name waf${{ env.PRODUCT }}${{ env.TARGET }}${{ vars.VERSION }}
# 4. WebApp configuration
webapp:
name: Web App πŸ”§
needs: [base, security]
environment: ${{ needs.base.outputs.environment }}
runs-on: [self-hosted, EXIP, infrastructure]
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "feature", "staging"]', env.TARGET)
run: echo "TYPE=Preproduction" >> $GITHUB_ENV
- name: Production πŸ’«
if: ${{ 'production' == env.TARGET }}
run: echo "TYPE=Production" >> $GITHUB_ENV
- name: Login πŸ”
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: azure/cli@v2
with:
inlineScript: |
# Basic
az configure --defaults location=${{ vars.REGION }}
az configure --defaults group=rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
- name: CD πŸ”€
uses: azure/cli@v2
with:
inlineScript: |
# UI
az webapp deployment container config \
--name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--enable-cd true
# API
az webapp deployment container config \
--name app-${{ env.PRODUCT }}-api-${{ env.TARGET }}-${{ vars.VERSION }} \
--enable-cd true
- name: Configuration πŸ”¨
uses: azure/cli@v2
with:
inlineScript: |
# UI
az webapp config set \
--name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--always-on true \
--ftps-state Disabled \
--http20-enabled true \
--min-tls-version '${{ vars.TLS_VERSION }}'
# API
az webapp config set \
--name app-${{ env.PRODUCT }}-api-${{ env.TARGET }}-${{ vars.VERSION }} \
--always-on true \
--ftps-state Disabled \
--http20-enabled true \
--min-tls-version '${{ vars.TLS_VERSION }}'
- name: Variables ✨
run: |
echo "API_URL=$(az webapp show --name app-${{ env.PRODUCT }}-api-${{ env.TARGET }}-${{ vars.VERSION }} --query defaultHostName -o tsv)" >> $GITHUB_ENV
- name: Setting 🧱
uses: azure/cli@v2
with:
inlineScript: |
# UI
az webapp config appsettings set \
--name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--settings \
TZ='${{ vars.TIMEZONE }}' \
NODE_ENV='${{ vars.NODE_ENV }}' \
PORT='${{ vars.UI_PORT }}' \
WEBSITES_PORT='${{ vars.UI_PORT }}' \
TLS_CERTIFICATE='${{ secrets.TLS_CERTIFICATE }}' \
TLS_KEY='${{ secrets.TLS_KEY }}' \
SESSION_SECRET='${{ secrets.SESSION_SECRET }}' \
GOOGLE_ANALYTICS_ID='${{ secrets.GOOGLE_ANALYTICS_ID }}' \
GOOGLE_TAG_MANAGER_ID='${{ secrets.GOOGLE_TAG_MANAGER_ID }}' \
API_URL='https://${{ env.API_URL }}/api/graphql' \
API_KEY='${{ secrets.API_KEY }}' \
APIM_MDM_URL='${{ secrets.APIM_MDM_URL }}' \
APIM_MDM_KEY='${{ secrets.APIM_MDM_KEY }}' \
APIM_MDM_VALUE='${{ secrets.APIM_MDM_VALUE }}'
# API
az webapp config appsettings set \
--name app-${{ env.PRODUCT }}-api-${{ env.TARGET }}-${{ vars.VERSION }} \
--settings \
TZ='${{ vars.TIMEZONE }}' \
NODE_ENV='${{ vars.NODE_ENV }}' \
PORT='${{ vars.API_PORT }}' \
WEBSITES_PORT='${{ vars.API_PORT }}' \
TLS_CERTIFICATE='${{ secrets.TLS_CERTIFICATE }}' \
TLS_KEY='${{ secrets.TLS_KEY }}' \
DATABASE_URL='${{ secrets.DATABASE_URL }}' \
MYSQL_DATABASE='${{ secrets.MYSQL_DATABASE }}' \
MYSQL_ROOT_PASSWORD='${{ secrets.MYSQL_ROOT_PASSWORD }}' \
API_KEY='${{ secrets.API_KEY }}' \
APIM_MDM_URL='${{ secrets.APIM_MDM_URL }}' \
APIM_MDM_KEY='${{ secrets.APIM_MDM_KEY }}' \
APIM_MDM_VALUE='${{ secrets.APIM_MDM_VALUE }}' \
SESSION_SECRET='${{ secrets.SESSION_SECRET }}' \
GOV_NOTIFY_API_KEY='${{ secrets.GOV_NOTIFY_API_KEY }}' \
COMPANIES_HOUSE_API_URL='${{ secrets.COMPANIES_HOUSE_API_URL }}' \
COMPANIES_HOUSE_API_KEY='${{ secrets.COMPANIES_HOUSE_API_KEY }}' \
JWT_SIGNING_KEY='${{ secrets.JWT_SIGNING_KEY }}' \
UNDERWRITING_TEAM_EMAIL='${{ secrets.UNDERWRITING_TEAM_EMAIL }}' \
FEEDBACK_EMAIL_RECIPIENT='${{ secrets.FEEDBACK_EMAIL_RECIPIENT }}'
CRON_SCHEDULE_UNVERIFIED_ACCOUNT='${{ secrets.CRON_SCHEDULE_UNVERIFIED_ACCOUNT }}'
CRON_SCHEDULE_INACTIVE_APPLICATION='${{ secrets.CRON_SCHEDULE_INACTIVE_APPLICATION }}'
CRON_SCHEDULE_SUBMISSION_DEADLINE_REMINDER_EMAIL='${{ secrets.CRON_SCHEDULE_SUBMISSION_DEADLINE_REMINDER_EMAIL }}'
APPLICATION_URL='${{ secrets.APPLICATION_URL }}'
- name: Extension βž•
uses: azure/cli@v2
with:
inlineScript: |
az config set extension.use_dynamic_install=yes_without_prompt
- name: Repository πŸ—ƒοΈ
uses: actions/checkout@v4
- name: Database πŸ’Ύ
uses: azure/cli@v2
with:
inlineScript: |
az webapp connection create mysql-flexible \
--source-id $(az webapp show --name app-${{ env.PRODUCT }}-api-${{ env.TARGET }}-${{ vars.VERSION }} --query id -o tsv) \
--target-id $(az mysql flexible-server list --query [].id -o tsv)/databases/${{ env.PRODUCT }} \
--connection webapp_api_mysqlflexible_${{ env.PRODUCT }}_${{ env.TARGET }}_${{ vars.VERSION }} \
--secret name=${{ secrets.MYSQL_USER }} secret=${{ secrets.MYSQL_PASSWORD }} \
--client-type nodejs
# 5. Public DNS Zone
dns:
name: DNS 🌐
needs: [base, security, webapp]
environment: ${{ needs.base.outputs.environment }}
runs-on: [self-hosted, EXIP, infrastructure]
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "feature", "staging"]', env.TARGET)
run: echo "TYPE=Preproduction" >> $GITHUB_ENV
- name: Production πŸ’«
if: ${{ 'production' == env.TARGET }}
run: echo "TYPE=Production" >> $GITHUB_ENV
- name: Tags 🏷️
run: echo TAGS='Environment=${{ env.TYPE }}' \
'Product=${{ env.PRODUCT }}' \
'Team=development' >> $GITHUB_ENV
- name: Login πŸ”
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: azure/cli@v2
with:
inlineScript: |
# Basic
az configure --defaults location=${{ vars.REGION }}
az configure --defaults group=rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
# AFD
echo FRONTDOOR=$(az afd profile list --query '[].id' -o tsv) >> $GITHUB_ENV
echo ENDPOINT=$(az afd endpoint list --profile-name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} --query '[].name' -o tsv) >> $GITHUB_ENV
- name: DNS Zones 🌐
uses: azure/cli@v2
with:
inlineScript: |
az network dns zone create \
--name ${{ vars.DOMAIN_QUOTE }} \
--tags ${{ env.TAGS }}
az network dns zone create \
--name ${{ vars.DOMAIN_INSURANCE }} \
--tags ${{ env.TAGS }}
- name: A records
uses: azure/cli@v2
with:
inlineScript: |
az network dns record-set a create \
--name "@" \
--zone ${{ vars.DOMAIN_QUOTE }} \
--if-none-match "*" \
--target-resource ${{ env.FRONTDOOR }}/afdendpoints/${{ env.ENDPOINT }} \
--ttl ${{ vars.DNS_TTL }}
az network dns record-set a create \
--name "@" \
--zone ${{ vars.DOMAIN_INSURANCE }} \
--if-none-match "*" \
--target-resource ${{ env.FRONTDOOR }}/afdendpoints/${{ env.ENDPOINT }} \
--ttl ${{ vars.DNS_TTL }}
- name: CNAME records
uses: azure/cli@v2
with:
inlineScript: |
az network dns record-set cname create \
--name "*" \
--zone ${{ vars.DOMAIN_QUOTE }} \
--if-none-match "*" \
--target-resource ${{ env.FRONTDOOR }}/afdendpoints/${{ env.ENDPOINT }} \
--ttl ${{ vars.DNS_TTL }}
az network dns record-set cname create \
--name "*" \
--zone ${{ vars.DOMAIN_INSURANCE }} \
--if-none-match "*" \
--target-resource ${{ env.FRONTDOOR }}/afdendpoints/${{ env.ENDPOINT }} \
--ttl ${{ vars.DNS_TTL }}
- name: TXT records
uses: azure/cli@v2
with:
inlineScript: |
# CA Verification
az network dns record-set txt add-record \
--record-set-name "@" \
--zone ${{ vars.DOMAIN_QUOTE }} \
--value ${{ vars.CA_VERIFICATION }} \
--if-none-match "*"
az network dns record-set txt add-record \
--record-set-name "@" \
--zone ${{ vars.DOMAIN_INSURANCE }} \
--value ${{ vars.CA_VERIFICATION }} \
--if-none-match "*"
- name: CAA records
uses: azure/cli@v2
with:
inlineScript: |
# CREATE
az network dns record-set caa create \
--name "@" \
--zone ${{ vars.DOMAIN_QUOTE }} \
--if-none-match "*" \
--ttl ${{ vars.DNS_TTL }}
# ISSUE
az network dns record-set caa add-record \
--record-set-name "@" \
--flags ${{ vars.CAA_FLAG }} \
--tag "issue" \
--value ${{ vars.CAA_ISSUE_VALUE }} \
--zone ${{ vars.DOMAIN_QUOTE }} \
--ttl ${{ vars.DNS_TTL }}
# IODEF
az network dns record-set caa add-record \
--record-set-name "@" \
--flags ${{ vars.CAA_FLAG }} \
--tag "iodef" \
--value ${{ vars.CAA_IODEF_VALUE }} \
--zone ${{ vars.DOMAIN_QUOTE }} \
--ttl ${{ vars.DNS_TTL }}
# CREATE
az network dns record-set caa create \
--name "@" \
--zone ${{ vars.DOMAIN_INSURANCE }} \
--if-none-match "*" \
--ttl ${{ vars.DNS_TTL }}
# ISSUE
az network dns record-set caa add-record \
--record-set-name "@" \
--flags ${{ vars.CAA_FLAG }} \
--tag "issue" \
--value ${{ vars.CAA_ISSUE_VALUE }} \
--zone ${{ vars.DOMAIN_INSURANCE }} \
--ttl ${{ vars.DNS_TTL }}
# IODEF
az network dns record-set caa add-record \
--record-set-name "@" \
--flags ${{ vars.CAA_FLAG }} \
--tag "iodef" \
--value ${{ vars.CAA_IODEF_VALUE }} \
--zone ${{ vars.DOMAIN_INSURANCE }} \
--ttl ${{ vars.DNS_TTL }}
# 6. Logs and dignostic settings
log:
name: Log πŸ“’
needs: [base, security, webapp, dns]
environment: ${{ needs.base.outputs.environment }}
runs-on: [self-hosted, EXIP, infrastructure]
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "feature", "staging"]', env.TARGET)
run: echo "TYPE=Preproduction" >> $GITHUB_ENV
- name: Production πŸ’«
if: ${{ 'production' == env.TARGET }}
run: echo "TYPE=Production" >> $GITHUB_ENV
- name: Login πŸ”
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: azure/cli@v2
with:
inlineScript: |
# Basic
az configure --defaults location=${{ vars.REGION }}
az configure --defaults group=rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
- name: Log analytics workspace πŸ“
uses: azure/cli@v2
with:
inlineScript: |
az monitor diagnostic-settings create \
--name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--resource $(az monitor log-analytics workspace list --query [].id -o tsv) \
--workspace $(az monitor log-analytics workspace list --query [].id -o tsv) \
--logs "[{categoryGroup:allLogs,enabled:true}]" \
--metrics "[{category:allMetrics,enabled:true}]"
- name: Database πŸ’Ύ
uses: azure/cli@v2
with:
inlineScript: |
# UI
az webapp log config \
--name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--detailed-error-messages true \
--failed-request-tracing true \
--level ${{ vars.WEBAPP_LOG_LEVEL }} \
--application-logging ${{ vars.WEBAPP_LOG_DESTINATION }} \
--docker-container-logging ${{ vars.WEBAPP_LOG_DESTINATION }} \
--web-server-logging ${{ vars.WEBAPP_LOG_DESTINATION }}
- name: WebApp 🌐
uses: azure/cli@v2
with:
inlineScript: |
# UI
az webapp log config \
--name app-${{ env.PRODUCT }}-ui-${{ env.TARGET }}-${{ vars.VERSION }} \
--detailed-error-messages true \
--failed-request-tracing true \
--level ${{ vars.WEBAPP_LOG_LEVEL }} \
--application-logging ${{ vars.WEBAPP_LOG_DESTINATION }} \
--docker-container-logging ${{ vars.WEBAPP_LOG_DESTINATION }} \
--web-server-logging ${{ vars.WEBAPP_LOG_DESTINATION }}
# API
az webapp log config \
--name app-${{ env.PRODUCT }}-api-${{ env.TARGET }}-${{ vars.VERSION }} \
--detailed-error-messages true \
--failed-request-tracing true \
--level ${{ vars.WEBAPP_LOG_LEVEL }} \
--application-logging ${{ vars.WEBAPP_LOG_DESTINATION }} \
--docker-container-logging ${{ vars.WEBAPP_LOG_DESTINATION }} \
--web-server-logging ${{ vars.WEBAPP_LOG_DESTINATION }}
- name: Front Door πŸšͺ
uses: azure/cli@v2
with:
inlineScript: |
az monitor diagnostic-settings create \
--name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--resource $(az afd profile list --query [].id -o tsv) \
--workspace $(az monitor log-analytics workspace list --query [].id -o tsv) \
--logs "[{categoryGroup:allLogs,enabled:true}]" \
--metrics "[{category:allMetrics,enabled:true}]"
- name: Private links πŸ”
uses: azure/cli@v2
with:
inlineScript: |
# UI
az monitor diagnostic-settings create \
--name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--resource $(az network nic list --query ['?contains(name, `ui`)'].id -o tsv) \
--workspace $(az monitor log-analytics workspace list --query [].id -o tsv) \
--metrics "[{category:allMetrics,enabled:true}]"
# API
az monitor diagnostic-settings create \
--name frontdoor-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--resource $(az network nic list --query ['?contains(name, `api`)'].id -o tsv) \
--workspace $(az monitor log-analytics workspace list --query [].id -o tsv) \
--metrics "[{category:allMetrics,enabled:true}]"
# 7. Various alerts
alert:
name: Alert πŸ“’
needs: [base, security, webapp, dns]
environment: ${{ needs.base.outputs.environment }}
runs-on: [self-hosted, EXIP, infrastructure]
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "feature", "staging"]', env.TARGET)
run: echo "TYPE=Preproduction" >> $GITHUB_ENV
- name: Production πŸ’«
if: ${{ 'production' == env.TARGET }}
run: echo "TYPE=Production" >> $GITHUB_ENV
- name: Tags 🏷️
run: echo TAGS='Environment=${{ env.TYPE }}' \
'Product=${{ env.PRODUCT }}' \
'Team=development' >> $GITHUB_ENV
- name: Login πŸ”
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: azure/cli@v2
with:
inlineScript: |
# Basic
az configure --defaults location=${{ vars.REGION }}
az configure --defaults group=rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
- name: Action group πŸ“©
uses: azure/cli@v2
with:
inlineScript: |
az monitor action-group create \
--name action-group-email-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--short-name Email \
--action email ${{ secrets.ACTION_GROUP_EMAIL }} \
--location global \
--tags ${{ env.TAGS }}
- name: Monitor alert πŸ“©
uses: azure/cli@v2
with:
inlineScript: |
# Dimension
az monitor metrics alert dimension create \
--name "OriginGroup" \
--value "ui" \
--operator Include
# Condition
az monitor metrics alert condition create \
--type static \
--dimension "_where_ OriginGroup includes ui" \
--aggregation Average \
--metric OriginHealthPercentage \
--operator LessThanOrEqual \
--threshold 99 \
--num-periods 1 \
--num-violations 5 \
--sensitivity High
# Alert
az monitor metrics alert create \
--name alert-healthcheck-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--description "1 minute health check" \
--condition "avg 'OriginHealthPercentage' <= 99.0 where OriginGroup includes ui" \
--scope $(az afd profile list --query [].id -o tsv) \
--action $(az monitor action-group list --query [].id -o tsv) \
--auto-mitigate true \
--severity 0 \
--tags ${{ env.TAGS }}