# load python
library(reticulate)
use_python('C:/Users/Andrew/Anaconda3/')
use_condaenv(condaenv='my_ml', required=TRUE)
library(knitr)
Azure Static Web Apps is a serverless hosting service that offers streamlined full-stack development from source code to global high availability. In this guide, we will build a static web app, secure it to a limited audience, and setup GitHub actions that build our posts automatically for us.
To begin, we will use Pelican, a static site generator that requires no database or server-side logic.
To use Pelican, we will do the following:
# create env
conda create -n pelican python=3.6
# activate
conda activate pelican
# install pelican
python -m pip install "pelican[markdown]"
# build a new website
pelican-quickstart
# Where do you want to create your new web site? [.]
.
# What will be the title of this web site?
Azure Static Web Apps
# Who will be the author of this web site?
Andrew Fogarty
# What will be the default language of this web site? [English]
En
# Do you want to specify a URL prefix? e.g., https://example.com (Y/n)
n
# Do you want to enable article pagination? (Y/n)
y
# How many articles per page do you want? [10]
5
# What is your time zone? [Europe/Paris]
America/New_York
# Do you want to generate a tasks.py/Makefile to automate generation and publishing? (Y/n)
Y
# Do you want to upload your website using FTP? (y/N)
n
# Do you want to upload your website using SSH? (y/N)
N
# Do you want to upload your website using Dropbox? (y/N)
N
# Do you want to upload your website using S3? (y/N)
N
# Do you want to upload your website using Rackspace Cloud Files? (y/N)
N
# Do you want to upload your website using GitHub Pages? (y/N)
N
# download cleanblog theme:
https://github.com/gilsondev/pelican-clean-blog/archive/refs/heads/master.zip
# install cleanblog
pelican-themes --install <path-to-cleanblog-here> --verbose
# generate a markdown file in website_folder/content
%%writefile /content/post1.md
Title: My super title
Date: 2010-12-03 10:20
Modified: 2010-12-05 19:30
Category: Python
Tags: pelican, publishing
Slug: my-super-post
Authors: Andrew Fogarty
Summary: Short version for index and feeds
This is the content of my super blog post.
# add to pelicanconf.py
THEME='PATH/TO/THEME'
THEME_STATIC_DIR = 'theme'
# run pelican and test live edits
pelican --autoreload --listen
To prevent your website from being accessed by unauthorized individuals, a staticwebapp.config.json
file is required. Inside our Pelcian website, we need to place staticwebapp.config.json
inside the output
folder. The JSON below will rely on Azure Active Directory and check whether or not the user is allowed to be there were allowed users are assigned a specialRoles
role within Azure.
{
"routes": [
{
"route": "/login",
"redirect": "/.auth/login/aad"
},
{
"route": "/logout",
"redirect": "/.auth/logout"
},
{
"route": "/*",
"allowedRoles": ["specialRoles"]
}
],
"responseOverrides": {
"401": {
"statusCode": 302,
"redirect": "/login"
}
}
}
When building the Azure Static Web App, ensure that you select:
Now we can follow the usual GitHub procedures and commit our new website to our repository. Azure Static Web App will generate a GitHub action yaml file that will automatically push our website to the Azure service.
Once the website is deployed, proceed to Role Management
within the Static Web App
and invite the select users to your website. Ensure that they are given the role specialRoles
.
As we update our markdown files, we can automate the building of the HTML files by a clever use of GitHub action scripts that might look like:
name: Run Script
on:
push:
pull_request:
schedule: # run this every day
- cron: '0 7 * * *' # 0700 UTC daily; roughly 0300 EDT
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.6' # Version range or exact version of a Python version to use, using SemVer's version range syntax
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
- name: Run a pull
run: |
git pull
- name: Ensure a commit
run: |
touch " " >> ghost.txt
- name: Install python dependencies
run: |
# install pip
python -m pip install --upgrade pip
# install requirements.txt
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Install pelican
run: |
# download pelican theme
curl -LO https://github.com/gilsondev/pelican-clean-blog/archive/refs/heads/master.zip
# install unzip
sudo apt-get install unzip
# unzip theme -- choose All after to overwrite
unzip master.zip<<<A
# install theme
pelican-themes --install pelican-clean-blog-master --verbose
- name: Alter Markdown
run: |
# update the date every day
# get the date to replace
str3=$(sed -n 2p content/post1.md) # copy the second line
# get the current date to inject
str4=$(date "+%B %_d, %Y")
# search and replace date
sed -i -e "s|$str3|Date: $str4|g" content/post1.md
- name: Build pelican website
run: |
# generate new html
pelican content -s pelicanconf.py
- name: Commit files to git
run: |
git config user.email user.email.here
git config user.name user.name.here
git add .
git commit -m 'update markdown pipeline'
git push -u origin main
build_and_deploy_job:
# paste Azure Static Web App generated yaml file here
In the event we want a more general security whereby all Microsoft employees can access our website, we should use the following staticwebapp.config.json
settings below:
{
"routes": [
{
"route": "/login",
"redirect": "/.auth/login/aad"
},
{
"route": "/logout",
"redirect": "/.auth/logout"
},
{
"route": "/*",
"allowedRoles": ["specialRoles", "authenticated"]
}
],
"responseOverrides": {
"401": {
"statusCode": 302,
"redirect": "/login"
}
},
"auth": {
"identityProviders": {
"azureActiveDirectory": {
"registration": {
"openIdIssuer": "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0",
"clientIdSettingName": "web_id",
"clientSecretSettingName": "web_key"
}
}
}
}
}
Note above that we specify two things, in addition to our tenant: (1) a clientIdSettingName
, and (2) a clientSecretSettingName
.
clientIdSettingName
: Refers to the client ID which is given after you establish an Azure Active Directory App Registration for your static web app.
clientSecretSettingName
: Refers to the Client secret
which is established by the Certificates & secrets
section of the Azure Active Directory App Registration.
While still within our Azure Active Directory App Registration, select Authentication
on the left menu. Select Add a platform
, select Web
and set this value for the Redirect URI
: https://website.azurestaticapps.net/.auth/login/aad/callback
. Note the suffix we added to the website. Next, set the Front-channel logout URL
as: https://website.azurestaticapps.net/logout
. Lastly, check the ID tokens
check-box below.
With one more step to go, head over to the Azure static web app service and select Configuration
on the left menu. Here, add two items: (1) web_id
and (2) web_key
(to match the staticwebapp.config.json
setting). The values for each should be the App Registration client ID and the App Registration client secret respectively.
We also might be interested in collecting metrics related to our website use. We can get Application Insights telemetry by pasting the following code snippet into each HTML page we want to monitor. Note the code below which asks that we paste in our InstrumentationKey
. We get this value from creating an Application Insights service – it is available on the front overview page.
<script type="text/javascript">
!function(T,l,y){var S=T.location,k="script",D="instrumentationKey",C="ingestionendpoint",I="disableExceptionTracking",E="ai.device.",b="toLowerCase",w="crossOrigin",N="POST",e="appInsightsSDK",t=y.name||"appInsights";(y.name||T[e])&&(T[e]=t);var n=T[t]||function(d){var g=!1,f=!1,m={initialize:!0,queue:[],sv:"5",version:2,config:d};function v(e,t){var n={},a="Browser";return n[E+"id"]=a[b](),n[E+"type"]=a,n["ai.operation.name"]=S&&S.pathname||"_unknown_",n["ai.internal.sdkVersion"]="javascript:snippet_"+(m.sv||m.version),{time:function(){var e=new Date;function t(e){var t=""+e;return 1===t.length&&(t="0"+t),t}return e.getUTCFullYear()+"-"+t(1+e.getUTCMonth())+"-"+t(e.getUTCDate())+"T"+t(e.getUTCHours())+":"+t(e.getUTCMinutes())+":"+t(e.getUTCSeconds())+"."+((e.getUTCMilliseconds()/1e3).toFixed(3)+"").slice(2,5)+"Z"}(),iKey:e,name:"Microsoft.ApplicationInsights."+e.replace(/-/g,"")+"."+t,sampleRate:100,tags:n,data:{baseData:{ver:2}}}}var h=d.url||y.src;if(h){function a(e){var t,n,a,i,r,o,s,c,u,p,l;g=!0,m.queue=[],f||(f=!0,t=h,s=function(){var e={},t=d.connectionString;if(t)for(var n=t.split(";"),a=0;a<n.length;a++){var i=n[a].split("=");2===i.length&&(e[i[0][b]()]=i[1])}if(!e[C]){var r=e.endpointsuffix,o=r?e.location:null;e[C]="https://"+(o?o+".":"")+"dc."+(r||"services.visualstudio.com")}return e}(),c=s[D]||d[D]||"",u=s[C],p=u?u+"/v2/track":d.endpointUrl,(l=[]).push((n="SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)",a=t,i=p,(o=(r=v(c,"Exception")).data).baseType="ExceptionData",o.baseData.exceptions=[{typeName:"SDKLoadFailed",message:n.replace(/\./g,"-"),hasFullStack:!1,stack:n+"\nSnippet failed to load ["+a+"] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: "+(S&&S.pathname||"_unknown_")+"\nEndpoint: "+i,parsedStack:[]}],r)),l.push(function(e,t,n,a){var i=v(c,"Message"),r=i.data;r.baseType="MessageData";var o=r.baseData;return o.message='AI (Internal): 99 message:"'+("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) ("+n+")").replace(/\"/g,"")+'"',o.properties={endpoint:a},i}(0,0,t,p)),function(e,t){if(JSON){var n=T.fetch;if(n&&!y.useXhr)n(t,{method:N,body:JSON.stringify(e),mode:"cors"});else if(XMLHttpRequest){var a=new XMLHttpRequest;a.open(N,t),a.setRequestHeader("Content-type","application/json"),a.send(JSON.stringify(e))}}}(l,p))}function i(e,t){f||setTimeout(function(){!t&&m.core||a()},500)}var e=function(){var n=l.createElement(k);n.src=h;var e=y[w];return!e&&""!==e||"undefined"==n[w]||(n[w]=e),n.onload=i,n.onerror=a,n.onreadystatechange=function(e,t){"loaded"!==n.readyState&&"complete"!==n.readyState||i(0,t)},n}();y.ld<0?l.getElementsByTagName("head")[0].appendChild(e):setTimeout(function(){l.getElementsByTagName(k)[0].parentNode.appendChild(e)},y.ld||0)}try{m.cookie=l.cookie}catch(p){}function t(e){for(;e.length;)!function(t){m[t]=function(){var e=arguments;g||m.queue.push(function(){m[t].apply(m,e)})}}(e.pop())}var n="track",r="TrackPage",o="TrackEvent";t([n+"Event",n+"PageView",n+"Exception",n+"Trace",n+"DependencyData",n+"Metric",n+"PageViewPerformance","start"+r,"stop"+r,"start"+o,"stop"+o,"addTelemetryInitializer","setAuthenticatedUserContext","clearAuthenticatedUserContext","flush"]),m.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4};var s=(d.extensionConfig||{}).ApplicationInsightsAnalytics||{};if(!0!==d[I]&&!0!==s[I]){var c="onerror";t(["_"+c]);var u=T[c];T[c]=function(e,t,n,a,i){var r=u&&u(e,t,n,a,i);return!0!==r&&m["_"+c]({message:e,url:t,lineNumber:n,columnNumber:a,error:i}),r},d.autoExceptionInstrumented=!0}return m}(y.cfg);function a(){y.onInit&&y.onInit(n)}(T[t]=n).queue&&0===n.queue.length?(n.queue.push(a),n.trackPageView({})):a()}(window,document,{
src: "https://js.monitor.azure.com/scripts/b/ai.2.min.js", // The SDK URL Source
// name: "appInsights", // Global SDK Instance name defaults to "appInsights" when not supplied
// ld: 0, // Defines the load delay (in ms) before attempting to load the sdk. -1 = block page load and add to head. (default) = 0ms load after timeout,
// useXhr: 1, // Use XHR instead of fetch to report failures (if available),
crossOrigin: "anonymous", // When supplied this will add the provided value as the cross origin attribute on the script tag
// onInit: null, // Once the application insights instance has loaded and initialized this callback function will be called with 1 argument -- the sdk instance (DO NOT ADD anything to the sdk.queue -- As they won't get called)
cfg: { // Application Insights Configuration
instrumentationKey: "INSTRUMENTATION_KEY"
}});
</script>
We can now use Application Insights to monitor our static web app.
We can go a step further and log authenticated user access with our App Insights by adding the following JavaScript to our snippet above. In the code below, we use JavaScript to parse the JSON of our security information found in the URL extension .auth/me
and save it into a few variables. We then send those variables into this line of code, setAuthenticatedUserContext(authenticatedUserId: string, accountId?: string, storeInCookie = false)
as follows: appInsights.setAuthenticatedUserContext(jsondata, jsondata2, true)
... //truncated code snippet
instrumentationKey: "INSTRUMENTATION_KEY"
}});
async function jsonparse1() {
let response = await fetch('https://static_web_App_url/.auth/me')
let users = await response.json()
var jsondata = users.clientPrincipal.userDetails
var jsondata2 = users.clientPrincipal.userId
console.log(users.clientPrincipal.userDetails)
//document.querySelector('#debug').innerText = jsondata
appInsights.setAuthenticatedUserContext(jsondata, jsondata2, true);
}
jsonparse1()
//appInsights.setAuthenticatedUserContext(authUserId, accountId, true);