-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathweb_app.Rmd
336 lines (260 loc) · 14.3 KB
/
web_app.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
---
title: 'Azure: Static Web Apps'
author: "Andrew Fogarty"
date: "06/26/2021"
output:
html_document:
toc: yes
df_print: paged
rmarkdown::html_vignette:
toc: yes
number_sections: yes
editor_options:
chunk_output_type: console
---
```{r, message = FALSE, warning = FALSE}
# load python
library(reticulate)
use_python('C:/Users/Andrew/Anaconda3/')
use_condaenv(condaenv='my_ml', required=TRUE)
library(knitr)
```
# Introduction
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.
# Pelican
To begin, we will use [Pelican](https://blog.getpelican.com/), a static site generator that requires no database or server-side logic.
To use Pelican, we will do the following:
## Create a Pelican Environment
```{python, eval=FALSE}
# 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
```{python, eval=FALSE}
# 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 Blog Post
```{python, eval=FALSE}
# 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.
```
## Set the Theme
```{python, eval=FALSE}
# add to pelicanconf.py
THEME='PATH/TO/THEME'
THEME_STATIC_DIR = 'theme'
```
## View the Blog Post
```{python, eval=FALSE}
# run pelican and test live edits
pelican --autoreload --listen
```
# Azure Static Web App Security
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.
```{python, eval=FALSE}
{
"routes": [
{
"route": "/login",
"redirect": "/.auth/login/aad"
},
{
"route": "/logout",
"redirect": "/.auth/logout"
},
{
"route": "/*",
"allowedRoles": ["specialRoles"]
}
],
"responseOverrides": {
"401": {
"statusCode": 302,
"redirect": "/login"
}
}
}
```
# Generate Azure Static Web App
When building the Azure Static Web App, ensure that you select:
* Plan type: Standard (needed if we want to protect access)
* Source: GitHub (and sign into GitHub)
* Build Presets: Custom
* App location: /
* Api location: blank
* Output location: /output/
# Push the Website to GitHub
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.
# Establish Security
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`.
# Automate GitHub Building
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:
```{bash, eval=FALSE}
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
```
# Switch Security to Azure Active Directory - Microsoft Tenant
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:
```{python, eval=FALSE}
{
"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.
# Establish Application Insights
We also might be interested in collecting metrics related to our website use. We can get [Application Insights telemetry](https://github.com/Microsoft/ApplicationInsights-JS#snippet-setup-ignore-if-using-npm-setup) 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.
```{python, eval=FALSE}
<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.