Skip to content

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

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

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

Workflow file for this run

# Introduction
# ***************
# This code snippet is a GitHub Actions workflow file that automates the setup and configuration of the EXIP project's infrastructure using Azure CLI commands.
# It creates various Azure resources such as resource groups, app service plans, log analytics workspaces, container registries, virtual networks, subnets, VNET peerings,
# web apps, front doors, private endpoints, DNS configurations, and diagnostic settings. It also sets environment variables and configures the web apps with app settings and logging options.
#
# Naming conventions
# ******************
# Standard Azure naming convention has been followed:
# https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming
# A minor modification to standard naming convention has been made to not include the region.
#
# Execution
# *********
# GHA is only invoked when following conditions are satisfied:
# 1. Push to the `infrastructure` branch only.
#
# Note
# ****
# Azure CLI will merely ignore the new resource creation if already exist with the same name.
#
name: Infrastructure πŸ”¨
run-name: EXIP base infrastructure build from ${{ github.repository }}
on:
push:
branches:
- infrastructure
- feat/iac-database
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: ubuntu-latest
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "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@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: Azure/[email protected]
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/[email protected]
# with:
# inlineScript: |
# az group create \
# --name rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
# --tags ${{ env.TAGS }}
# - name: Plan πŸ“
# uses: Azure/[email protected]
# 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/[email protected]
# 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/[email protected]
# 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/[email protected]
# 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/[email protected]
# with:
# inlineScript: |
# # Database
# az network vnet subnet create \
# --name snet-database-${{ env.PRODUCT }}-${{ vars.VERSION }} \
# --address-prefixes ${{ vars.VNET_SUBNET_DATABASE_PREFIX }} \
# --vnet-name vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
# # 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 }}
# - name: VNET Peer - AMI πŸ”€
# uses: Azure/[email protected]
# 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/[email protected]
# 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: Database πŸ’Ύ
# uses: Azure/[email protected]
# 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 }} \
# --sku-name Standard_B1ms \
# --tier Burstable \
# --vnet vnet-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
# --subnet snet-database-${{ env.PRODUCT }}-${{ vars.VERSION }} \
# --backup-retention 30 \
# --yes \
# --tags ${{ env.TAGS }}
# - name: Web app - UI 🌐
# uses: Azure/[email protected]
# 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/[email protected]
# 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/[email protected]
# 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 }}
- name: Extension βž•
uses: Azure/[email protected]
with:
inlineScript: |
az config set extension.use_dynamic_install=yes_without_prompt
- name: Repository πŸ—ƒοΈ
uses: actions/checkout@v4
- name: Import ⬇
uses: Azure/[email protected]
with:
inlineScript: |
az mysql flexible-server execute \
--name sqldb-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--admin-user ${{ secrets.MYSQL_USER }} \
--admin-password ${{ secrets.MYSQL_PASSWORD }} \
--database-name ${{ env.PRODUCT }} \
--file-path "./database/exip.sql"
- name: Database πŸ’Ύ
uses: Azure/[email protected]
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 secret=${{ secrets.MYSQL_PASSWORD }} \
--client-type nodejs
# 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", "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@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: Azure/[email protected]
with:
inlineScript: |
# Basic
az configure --defaults location=${{ vars.REGION }}
az configure --defaults group=rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
- name: Extension βž•
uses: Azure/[email protected]
with:
inlineScript: |
az extension add --name front-door
- name: Private endpoint πŸ”
uses: Azure/[email protected]
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 }}
- name: Private DNS 🌍
uses: Azure/[email protected]
with:
inlineScript: |
az network private-dns zone create \
--name ${{ secrets.PRIVATE_DNS_ZONE_NAME }} \
--tags ${{ env.TAGS }}
- name: DNS VNET βž•
uses: Azure/[email protected]
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/[email protected]
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/[email protected]
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/[email protected]
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", "staging"]', env.TARGET)
uses: Azure/[email protected]
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", "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@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: Azure/[email protected]
with:
inlineScript: |
# Basic
az configure --defaults location=${{ vars.REGION }}
az configure --defaults group=rg-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }}
- name: CD πŸ”€
uses: Azure/[email protected]
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/[email protected]
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/[email protected]
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 }}' \
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='mysql://${{ secrets.MYSQL_USER }}:${{ secrets.MYSQL_PASSWORD }}@$(az mysql flexible-server list --query [].fullyQualifiedDomainName -o tsv):${{ vars.DB_PORT }}/${{ vars.PRODUCT }}' \
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 }}'
- name: Database πŸ’Ύ
uses: Azure/[email protected]
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 secret=${{ secrets.MYSQL_PASSWORD }} \
--client-type nodejs
# 5. Logs and dignostic settings
log:
name: Log πŸ“’
needs: [base, security, webapp]
environment: ${{ needs.base.outputs.environment }}
runs-on: [self-hosted, EXIP, infrastructure]
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "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@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: Azure/[email protected]
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/[email protected]
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/[email protected]
with:
inlineScript: |
az monitor diagnostic-settings create \
--name sqldb-${{ env.PRODUCT }}-${{ env.TARGET }}-${{ vars.VERSION }} \
--resource $(az mysql flexible-server 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: WebApp 🌐
uses: Azure/[email protected]
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/[email protected]
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/[email protected]
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}]"
# 6. Health check alerts
alert:
name: Alert πŸ“’
needs: [base, security, webapp]
environment: ${{ needs.base.outputs.environment }}
runs-on: [self-hosted, EXIP, infrastructure]
steps:
- name: Pre-production πŸ’«
if: contains('["dev", "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@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure defaults ✨
uses: Azure/[email protected]
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/[email protected]
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: Alert πŸ“©
uses: Azure/[email protected]
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 }}