├── icon.png ├── staticwebapp.config.json ├── readme.md ├── index.html ├── style.css ├── .github └── workflows │ └── azure-static-web-apps-mango-wave-014305a1e.yml └── FailoverRunbook.ps1 /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scottmckendry/BlogMaintenancePage/main/icon.png -------------------------------------------------------------------------------- /staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "responseOverrides": { 3 | "404": { 4 | "redirect": "/", 5 | "statusCode": 302 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Blog Maintenance page 2 | 3 | This is a simple landing page that shows when my self hosted blog goes down. 4 | 5 | Learn more about it [here](https://scottmckendry.tech/maintenance-web-app-azure/). -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Be Right Back! 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |

Thanks for visiting

18 |

If you're seeing this page, it probably means I've broken something... But don't worry! I should have things fixed soon.

19 |

Be sure to come back later when I'm back up and running again!

20 |

While you're waiting, feel free to connect with me on LinkedIn and let me know you stopped by 🙋‍♂️ 22 |

Cheers,
Scott

23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Inter; 3 | background-color: #f1f2f3; 4 | -webkit-font-smoothing: antialiased; 5 | text-align: center; 6 | } 7 | 8 | .messageContainer { 9 | background-color: #fff; 10 | font-size: 1rem; 11 | width: 36%; 12 | height: 40vh; 13 | margin-left: 32%; 14 | margin-top: 30vh; 15 | border-radius: 30px; 16 | box-shadow: 0 50px 100px -20px rgb(50 50 93 / 8%), 0 30px 60px -30px rgb(0 0 0 / 13%), 0 10px 20px -10px rgb(0 0 0 / 8%); 17 | } 18 | 19 | .messageContent { 20 | margin: 0; 21 | position: absolute; 22 | top: 50%; 23 | left: 50%; 24 | -ms-transform: translate(-50%, -50%); 25 | transform: translate(-50%, -50%); 26 | max-width: 30%; 27 | } 28 | 29 | h1 { 30 | line-height: 1em; 31 | font-weight: 700; 32 | letter-spacing: -0.02em; 33 | font-family: sans-serif; 34 | } 35 | 36 | p { 37 | margin: 0.5em; 38 | opacity: 0.7; 39 | font-weight: 400; 40 | } 41 | 42 | @media screen and (max-width: 900px) { 43 | .messageContainer { 44 | height: 70vh; 45 | width: 90%; 46 | margin-left: 5%; 47 | margin-top: 15vh; 48 | } 49 | .messageContent { 50 | max-width: 80%; 51 | left: 28%; 52 | transform: translate(-20%, -50%); 53 | } 54 | } -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-mango-wave-014305a1e.yml: -------------------------------------------------------------------------------- 1 | name: Azure Static Web Apps CI/CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened, closed] 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build_and_deploy_job: 14 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 15 | runs-on: ubuntu-latest 16 | name: Build and Deploy Job 17 | steps: 18 | - uses: actions/checkout@v2 19 | with: 20 | submodules: true 21 | - name: Build And Deploy 22 | id: builddeploy 23 | uses: Azure/static-web-apps-deploy@v1 24 | with: 25 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_WAVE_014305A1E }} 26 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 27 | action: "upload" 28 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 29 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 30 | app_location: "/" # App source code path 31 | api_location: "" # Api source code path - optional 32 | output_location: "" # Built app content directory - optional 33 | ###### End of Repository/Build Configurations ###### 34 | 35 | close_pull_request_job: 36 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 37 | runs-on: ubuntu-latest 38 | name: Close Pull Request Job 39 | steps: 40 | - name: Close Pull Request 41 | id: closepullrequest 42 | uses: Azure/static-web-apps-deploy@v1 43 | with: 44 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_MANGO_WAVE_014305A1E }} 45 | action: "close" 46 | -------------------------------------------------------------------------------- /FailoverRunbook.ps1: -------------------------------------------------------------------------------- 1 | # FailoverRunbook.ps1 2 | # Scott McKendry - Feburary 2023 3 | #---------------------------------------------------------------- 4 | # Checks domains to see if up, failing over to the SWA if it is down. 5 | # Runs after failover check for a given expected outage period and revert the changes after that period completes. 6 | 7 | # Retrieve Azure Automation Account Credentials 8 | $cloudflareCredentials = Get-AutomationPSCredential -Name "AAC-Cloudflare" 9 | $domainsCredentials = Get-AutomationPSCredential -Name "AAC-Domains" # Username is treated like CSV row (no spaces allowed) 10 | $failoverUrlsCredentials = Get-AutomationPSCredential -Name "AAC-FailoverUrls" # Username is treated like CSV row (no spaces allowed) 11 | $zonesAndIpCredentials = Get-AutomationPSCredential -Name "AAC-ZonesAndIP" # Username is treated like CSV row (no spaces allowed) 12 | 13 | # Variables (domains, failoverUrls and zones must all be in same order) 14 | $revertAfterMins = 29 15 | $cloudflareEmail = $cloudflareCredentials.Username 16 | $cloudflareApiKey = $cloudflareCredentials.GetNetworkCredential().Password 17 | $domains = $domainsCredentials.Username.Split(",") 18 | $failoverUrls = $failoverUrlsCredentials.Username.Split(",") 19 | $zones = $zonesAndIpCredentials.Username.Split(",") 20 | $ip = $zonesAndIpCredentials.GetNetworkCredential().Password 21 | 22 | # Cloudflare API Headers 23 | $headers = @{ 24 | "X-Auth-Email" = $cloudflareEmail 25 | "X-Auth-Key" = $cloudflareApiKey 26 | } 27 | 28 | $domainIndex = 0 29 | foreach ($domain in $domains) { 30 | 31 | # Get domain specific info 32 | $failover = $failoverUrls[$domainIndex] 33 | $zone = $zones[$domainIndex] 34 | 35 | # Get A record from the cloudflare 36 | $requestUrl = "https://api.cloudflare.com/client/v4/zones/$($zone)/dns_records/?name=$($domain)&type=A" 37 | $recordToCheck = Invoke-RestMethod -Uri $requestUrl -Method Get -Headers $headers 38 | $recordId = $recordToCheck.result.id 39 | 40 | # A Record exists, check to see if the site is up 41 | if ($recordId) { 42 | $targetUrl = "https://$($domain)" 43 | $websiteResponse = Invoke-WebRequest -uri $targetUrl -SkipHttpErrorCheck 44 | $returnCode = $websiteResponse.StatusCode 45 | 46 | if ($returnCode -eq 200) { 47 | Write-Host "$domain is up." 48 | } 49 | else { 50 | # Delete A record 51 | $requestUrl = "https://api.cloudflare.com/client/v4/zones/$($zone)/dns_records/$recordId" 52 | Invoke-WebRequest -Uri $requestUrl -Method Delete -Headers $headers | Out-Null 53 | 54 | # Create CNAME record 55 | $newCnameRecord = @{ 56 | "type" = "CNAME" 57 | "name" = "@" 58 | "content" = $failover 59 | "proxied" = $true 60 | } 61 | $body = $newCnameRecord | ConvertTo-Json 62 | $requestUrl = "https://api.cloudflare.com/client/v4/zones/$($zone)/dns_records" 63 | Invoke-WebRequest -Uri $requestUrl -Method Post -Headers $headers -Body $body -ContentType "application/json" | Out-Null 64 | Write-Host "$domain is down. Failed over to SWA." 65 | } 66 | } 67 | 68 | # No A record == Failed Over in a previous run 69 | else { 70 | # Get the CNAME record 71 | $requestUrl = "https://api.cloudflare.com/client/v4/zones/$($zone)/dns_records/?name=$($domain)&type=CNAME" 72 | $recordToCheck = Invoke-RestMethod -Uri $requestUrl -Method Get -Headers $headers 73 | $recordId = $recordToCheck.result.id 74 | 75 | # Get offset of created time vs current time 76 | $recordCreatedTime = $recordToCheck.Result.created_on 77 | $currentTime = Get-Date -AsUtc 78 | $offset = $currentTime - $recordCreatedTime 79 | 80 | # If created longer than revertAfter, revert changes 81 | if ($offset.TotalMinutes -gt $revertAfterMins) { 82 | # Delete CNAME record 83 | $requestUrl = "https://api.cloudflare.com/client/v4/zones/$($zone)/dns_records/$recordId" 84 | Invoke-WebRequest -Uri $requestUrl -Method Delete -Headers $headers | Out-Null 85 | 86 | # Create A record 87 | $newCnameRecord = @{ 88 | "type" = "A" 89 | "name" = "@" 90 | "content" = $ip 91 | "proxied" = $true 92 | } 93 | $body = $newCnameRecord | ConvertTo-Json 94 | $requestUrl = "https://api.cloudflare.com/client/v4/zones/$($zone)/dns_records" 95 | Invoke-WebRequest -Uri $requestUrl -Method Post -Headers $headers -Body $body -ContentType "application/json" | Out-Null 96 | Write-Host "$domain expected outage time complete. Reverting DNS Changes" 97 | } 98 | else { 99 | Write-Host "$domain within expected outage time. No change to DNS." 100 | } 101 | } 102 | $domainIndex++ 103 | } --------------------------------------------------------------------------------