');-webkit-mask-image:url('data:image/svg+xml;charset=utf-8, ');-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:1.8rem;mask-size:1.8rem;outline:none;padding:0 1rem}.icomoon-github,.icomoon-github:visited,.icomoon-linkedin,.icomoon-linkedin:visited{background-color:var(--color-background-primary);border:1px solid var(--color-border);border-radius:100%;color:var(--color-font-link);margin:0;padding:.5rem;text-decoration:none!important;vertical-align:middle}.icomoon-github:hover,.icomoon-linkedin:hover{border:1px solid var(--color-border-hover)}*,:after,:before{box-sizing:inherit}a,abbr,acronym,address,applet,article,aside,audio,big,blockquote,body,canvas,caption,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,ul,var,video{border:0;font:inherit;margin:0;outline:0;padding:0;vertical-align:baseline}audio,canvas,iframe,img,svg,video{vertical-align:middle}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}html{-moz-text-size-adjust:100%;text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:#000;box-sizing:border-box;font-size:10px;overflow-x:hidden;overflow-y:scroll}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:var(--color-background-secondary);color:var(--color-font);font-family:var(--font-body);font-size:1.7rem;font-style:normal;font-weight:var(--font-regular);line-height:1.3;text-rendering:optimizeLegibility}::selection{background-color:#cbeafb;text-shadow:none}a{background-color:transparent;color:var(--color-font-link);display:inline;font-weight:var(--font-regular);overflow-wrap:anywhere;text-decoration:none!important;word-break:normal}a:hover{text-decoration:none}b{font-weight:700}blockquote{border-left:.5rem solid var(--color-border-blockquote);margin:1rem 0 2.5rem;padding:0 1rem;quotes:none}blockquote:after,blockquote:before{content:"";content:none}blockquote p{font-size:1.5rem;line-height:1.3;margin:0;padding:0}blockquote small{display:inline-block;font-size:1.2rem;margin:.8rem 0 .8rem 1.5rem;opacity:.8}blockquote small:before{content:"\2014 \00A0"}button{background-color:transparent;overflow:visible}details{border:1px solid var(--color-border-blockquote);margin:0 0 1.5rem;width:100%}dd,details{padding:1rem}dd{background-color:var(--color-background-featured);border:1px solid var(--color-border);margin:1.5rem 0;text-align:left;white-space:pre-line}dfn{font-style:italic}dl{font-family:var(--font-body);margin:0 0 1.5rem}dt{float:left;margin:0 2rem 0 0;text-align:right;width:12rem}em,figcaption{font-style:italic}figcaption{font-size:1.5rem;font-weight:var(--font-light);margin:0 auto;padding:1rem;text-align:center}fieldset{border:0;margin:0;padding:0}h1,h2,h3,h4,h5,h6{color:var(--color-font-headers);font-weight:var(--font-medium);line-height:1.2;margin:0;padding:0;text-rendering:optimizeLegibility;width:100%}h1{font-size:3.5rem;padding:2.9rem 0 2rem}@media (min-width:600px){h1{font-size:3.9rem}}@media (min-width:900px){h1{font-size:4.2rem}}@media (min-width:1200px){h1{font-size:4.5rem}}@media (min-width:1800px){h1{font-size:5.5rem}}h2{font-size:3.2rem;padding:3rem 0 2rem}h3{font-size:2.5rem;padding:2.5rem 0 1rem}h4{padding:2rem 0}h4,h5{font-size:2.1rem}h5{padding:2rem 0 .5rem}h5,h6{font-weight:var(--font-bold)}h6{font-size:1.8rem;padding:1.6rem 0 .5rem}hr{border:0;border-top:1px solid var(--color-border);display:block;height:.1rem;margin:.5rem 0;position:relative;width:100%}i{font-style:italic}img{border:0;height:auto;margin:1rem 0 0;max-width:100%}.two-panel{display:flex;margin:0 auto}.two-panel img{-o-object-fit:contain;object-fit:contain;width:48%}.two-panel span{display:block;width:4%}input:focus{outline:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{box-sizing:content-box}label{font-family:Open Sans,sans-serif}legend{border:0;padding:0}mark{background-color:#fdffb6}li{font-weight:var(--font-light);line-height:1.2;padding:0 0 .8rem}ol,ul{margin:0 0 1.5rem 1rem;max-width:100%;padding:0 2rem}ol{list-style:decimal}ul{list-style:disc}ol ol,ol ul,p,ul ol,ul ul{margin:0}p{color:var(--color-font);font-family:var(--font-body);font-size:1.8rem;font-weight:var(--font-regular);padding:0 0 2rem;word-spacing:0}pre{padding:0 0 1.5rem}q{quotes:none}q:after,q:before{content:"";content:none}samp{color:var(--color-font-sample);font-size:1.5rem;white-space:pre-wrap;word-break:break-all}strong{font-weight:var(--font-semi-bold)}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25rem}summary{color:var(--color-border-blockquote);font-size:1.5rem;font-weight:600}sup{top:-.5rem}svg:not(:root){overflow:hidden}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}textarea{overflow:auto;resize:vertical}.element{-webkit-box-orient:vertical;-webkit-line-clamp:3;line-clamp:3;display:-webkit-box;overflow:hidden}.layout{display:flex;flex-direction:column;justify-content:flex-start;min-height:100vh}.layout footer,.layout header,.layout main{margin:0 auto;max-width:180rem;width:100%}.layout header{align-items:center;background-color:var(--color-background-primary);background-position:50%;background-size:cover;border-bottom:1px solid var(--color-border);display:flex;justify-content:flex-start;opacity:1;padding:.5rem 2rem;position:-webkit-sticky;position:sticky;top:0;transition:opacity 1s,visibility 1s;visibility:visible;z-index:100}.layout main{flex:1 0 auto}.layout footer{align-items:center;background-color:var(--color-background-secondary);border-top:1px solid var(--color-border);display:flex;flex-direction:column;font-size:1.5rem;gap:1rem;justify-content:space-between;padding:2rem}@media (min-width:900px){.layout footer{flex-direction:row}}.layout header.sticky{-webkit-overflow-scrolling:touch;-webkit-animation:smoothScrollReverse 3s forwards;animation:smoothScrollReverse 3s forwards;opacity:0;position:relative;transform:translateZ(0);-webkit-transform:translateZ(0);visibility:hidden}@-webkit-keyframes smoothScrollReverse{0%{-webkit-transform:translateY(36rem);transform:translateY(36rem)}to{-webkit-transform:translateY(0);transform:translateY(0)}}@keyframes smoothScrollReverse{0%{-webkit-transform:translateY(36rem);transform:translateY(36rem)}to{-webkit-transform:translateY(0);transform:translateY(0)}}.header__logo{height:5rem;margin:0}.nav-menu__desktop{display:none}@media (min-width:600px){.nav-menu__desktop{display:initial}}.nav-menu__mobile{display:none}@media (max-width:599px){.nav-menu__mobile{background-color:var(--color-background-primary);border-bottom:1px solid var(--color-border);box-shadow:var(--shadow-normal),0 0 0 transparent;-moz-box-shadow:var(--shadow-normal),0 0 0 transparent;-webkit-box-shadow:var(--shadow-normal),0 0 0 transparent;display:none;left:0;padding:0;position:absolute;top:6rem;width:100%;z-index:1000}.nav-menu__mobile.active{display:block}}.nav-menu__desktop a,.nav-menu__mobile a{border:0;color:var(--color-font-link);display:inline-block;font-family:var(--font-body);font-size:1.5rem;font-weight:var(--font-semi-bold);letter-spacing:.1rem;margin:0;padding:0;text-decoration:none!important;text-transform:uppercase}.nav-menu__desktop a{margin:0 0 0 2rem}.nav-menu__mobile a{border-top:1px solid var(--color-border);display:block;padding:2rem 3rem}.header-controls{display:flex;flex:1 1 auto;justify-content:flex-end;text-align:right}.header-controls__label{color:var(--color-font-link);display:inline-block;font-family:var(--font-body);font-size:1.4rem;height:2.1rem;margin:0 .6rem 0 0;vertical-align:middle}.moreless-button{color:var(--color-font);display:none;padding:0 0 0 1rem}@media (max-width:599px){.moreless-button{display:initial}}.banner-figure{background-position:50%;background-repeat:no-repeat;background-size:cover;display:flex;flex-direction:column;padding:2rem;text-align:center}.banner-figure.four-o-four{background-position:bottom;height:100vmin}.banner-figure.index{background-color:var(--color-background-home);height:55vmin}.banner-figure.page{background-color:var(--color-background-image);height:55vmin}@media (max-width:599px){.banner-figure.four-o-four{height:100vmax}.banner-figure.index{font-size:1.5rem;height:60vmax}.banner-figure.page{height:40vmax}}@media (min-width:1200px){.banner-figure.index,.banner-figure.page{height:35rem}}.banner-figure.index h1{color:#333;font-weight:var(--font-thin);margin:0}.banner-figure h5{text-align:center;text-transform:uppercase}.banner-figure.index h5{color:#333}.banner-figure.four-o-four h5{color:#fff;margin:0 auto;max-width:25rem}.banner-figure.index span{color:var(--color-font);font-size:2rem}@media (max-width:599px){.banner-figure.index span{display:none}}.banner-dot{background-color:var(--color-background-image);border-radius:100%;display:block;height:12rem;margin:3rem auto 0;padding:0;text-align:center;width:12rem}.container{margin:0 auto;padding:1rem 2rem}@media (min-width:600px){.container{padding:2rem}}@media (min-width:1800px){.container{padding:2rem 0}}.container h2{text-align:center}article{margin:0 auto;max-width:80rem}@media (min-width:1200px){article{margin:0 auto}}@media (min-width:1800px){article{margin:0 auto}}.post-tag{margin:0 auto;max-width:78rem;padding:0;text-align:center}.post-tag__link{display:inline-block;padding:.25rem 1rem}.post-tag__link a{font-weight:var(--font-semi-bold);font-weight:var(--font-regular)}.post-tag__link sup{color:var(--color-font-tertiary);font-size:1rem;margin:0 0 0 .25rem}.page-title,.post-published,.tag-name{text-align:center}.post-published{color:var(--color-font);font-size:1.4rem}section.post-content img{width:100%}.search{text-align:center}.search form{margin:1rem}.search label{display:block}.search input{background-color:var(--color-background-secondary);border:1px solid var(--color-border);border-radius:var(--radius);font-size:1.6rem;font-weight:var(--font-semi-bold);height:3.8rem;line-height:1;max-width:50rem;padding:0 1.5rem;white-space:nowrap;width:-webkit-fill-available;width:-moz-available}.search input:hover{border:1px solid var(--color-font-link)}.search-results{margin:1rem auto;max-width:80rem}.search-results__count{display:block;font-size:1.5rem;margin:0 auto;padding:1rem;text-align:center}.search-results__tag-group-list{background-color:#242628;list-style:none;margin:1rem 0;padding:0;text-align:left}.search-results__tag-group{border-radius:var(--radius);margin:0;padding:2rem;width:100%}.search-results__tag-group img{background-color:var(--color-background-image);border-radius:100%;margin:0;width:4rem}.search-results__tag-group h5{display:inline;padding:0 0 0 1rem;vertical-align:middle}.search-results__tag-list{list-style:none;margin:0;padding:0}.search-results__tag{margin:1rem 0 0;padding:0}.search-results__extract,.search-results__heading{padding:0;word-break:normal}.search-results__extract{font-size:1.4rem}.search-results__date{color:var(--color-font-tertiary);font-size:1.4rem;font-style:italic;padding:0}.search-results__query-term-found{font-weight:var(--font-bold)}.themecontrol{padding-right:1rem}.footer__owner{color:var(--color-font)}.footer__topics{display:flex;padding:0}.footer__topics a{background-color:var(--color-background-primary);border:1px solid var(--color-border);border-radius:var(--radius);display:inline-block;font-size:1.4rem;font-weight:var(--font-semi-bold);line-height:1;margin:0 1rem;padding:1rem 1.5rem;text-decoration:none!important;white-space:nowrap}.footer__topics a:hover{border:1px solid var(--color-border-hover)}.footer__controls,.footer__social{align-self:center;display:flex}.footer__social{align-items:center;gap:1.5rem;justify-content:flex-end}@media (min-width:900px){.footer__social{align-self:flex-start}}.page h1{text-align:center}.page p{position:relative;z-index:1}.page p img{display:block;margin:2rem auto 0;width:100%}.page p em{color:var(--color-font-tertiary);font-size:1.5rem;line-height:1.2}.page p ul{margin-top:-3rem}.page li{font-size:1.6rem}.page figure{font-size:2.8rem;font-weight:var(--font-semi-bold);margin:1rem 0}.page-feed{margin:4rem 0 0}@media (min-width:600px){.page-feed{grid-gap:2rem;display:grid;grid-template-columns:repeat(2,1fr);grid-template-rows:auto}}@media (min-width:1800px){.page-feed.about{grid-template-columns:repeat(4,1fr)}.page-feed.initiatives{grid-template-columns:repeat(5,1fr)}}.page-feed a{background-color:var(--color-background-primary);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:var(--shadow-normal),0 0 0 transparent;color:var(--primary-font);display:flex;flex-direction:column;font-family:var(--font-body);margin:0;padding:2rem 0;text-decoration:none!important;transition:all .25s cubic-bezier(.02,.01,.47,1)}@media (max-width:599px){.page-feed a{margin:2rem 0}}.page-feed a:hover{box-shadow:var(--shadow-hover),0 0 0 transparent;-webkit-transform:translateY(-.5rem);transform:translateY(-.5rem)}.page-feed img{background-color:var(--color-background-image);border:1px solid var(--color-border);border-radius:100%;height:10rem;margin:0 auto;width:10rem}.page-feed h4,.page-feed p{text-align:center}.page-feed p{font-weight:300;margin:0 5%}.post-feed,.tag-feed{-webkit-box-pack:space-evenly;display:flex;display:-webkit-flexbox;display:-ms-flexbox;flex-wrap:wrap;-webkit-flex-wrap:wrap;justify-content:flex-start;padding:0;transition:all .25s cubic-bezier(.02,.01,.47,1)}.post-feed{gap:0}.tag-feed{gap:2.5rem}.post-feed p{text-align:left}.post-feed h5{margin:3rem 0 0}.post-feed img{margin:1rem auto 3.5rem}.post-card__header{flex:0 1 auto;padding:1rem 0 0}.post-card__main{flex:1 0 auto}.post-card__footer{display:flex;flex:0 1 auto;font-style:italic;font-weight:var(--font-light);justify-content:space-between;margin:0}.post-card__footer p{font-size:1.4rem;padding:0}.post-card{-webkit-box-pack:end;-ms-flex-pack:end;background-color:var(--color-background-primary);border:1px solid var(--color-border);border-radius:var(--radius);box-shadow:var(--shadow-normal),0 0 0 transparent;color:var(--color-font);display:flex;display:-webkit-flexbox;flex:1 1 25rem;flex-direction:column;min-width:30rem;padding:2rem;text-decoration:none!important;transition:all .25s cubic-bezier(.02,.01,.47,1)}.post-card:hover{box-shadow:var(--shadow-hover),0 0 0 transparent;-webkit-transform:translateY(-.5rem);transform:translateY(-.5rem)}.post-card figure{background-color:var(--color-background-primary);background-position:50%;background-repeat:no-repeat;background-size:cover;height:14rem;margin:0 auto;max-width:35rem}.post-card h4{text-align:center}.load-external-scripts>p{margin:0 auto}.post-card-meta,.post-meta{background-color:var(--color-background-featured);margin:1.5rem auto;padding:1.5rem;text-align:left;width:100%}.post-card-meta.center-col,.post-meta.center-col{align-items:center;background-color:transparent;display:flex;flex-direction:column;margin:0 0 1.5rem;padding:0;text-align:center}.post-card-meta li,.post-meta li{padding:0}.post-card-meta i,.post-meta i{display:block;padding:0 0 .5rem}.post-card-meta p,.post-meta p{font-size:1.4rem;padding:0}.post-card-meta div,.post-meta div{padding:1rem 0}.post-card-meta.flex-col,.post-meta.flex-col{padding:0}.post-card-meta.flex-col div,.post-meta.flex-col div{display:inline-block;padding:2rem;vertical-align:top;width:30%}@media (max-width:599px){.post-card-meta.flex-col div,.post-meta.flex-col div{width:100%}}.content-centered-text{background-color:var(--color-background-featured);display:flex;flex-direction:column;margin:2rem 0;padding:3.5rem}.content-centered-text div{align-self:center}.setting{margin:0 1rem 0 0}.kg-bookmark-card{margin-top:0;width:100%}.kg-bookmark-container{border-radius:var(--radius);display:flex;font-family:var(--font-body);min-height:14.8rem}.kg-bookmark-container,.kg-bookmark-container:hover{box-shadow:0 2px 5px -1px #00000026,0 0 1px #00000017;color:var(--color-font);text-decoration:none}.kg-bookmark-content{align-items:flex-start;display:flex;flex-direction:column;flex-grow:1;justify-content:flex-start;padding:2rem}.kg-bookmark-description{-webkit-line-clamp:2;-webkit-box-orient:vertical;line-clamp:2;color:var(--color-font);display:-webkit-box;font-size:1.5rem;font-weight:400;margin-top:1.2rem;max-height:4.8rem;overflow-y:hidden}.kg-bookmark-title{color:var(--color-background-primary);font-weight:600;transition:color .2s ease-in-out}.kg-bookmark-container:hover .kg-bookmark-title{color:var(--color-background-secondary)}.kg-bookmark-thumbnail{max-height:100%;min-width:33%;position:relative}.kg-bookmark-thumbnail img{border-radius:0 var(--radius) var(--radius) 0;height:100%;left:0;-o-object-fit:cover;object-fit:cover;position:absolute;top:0;width:100%}.kg-bookmark-metadata{align-items:center;display:flex;flex-wrap:wrap;font-size:1.5rem;font-weight:400;margin-top:1.4rem}.kg-bookmark-icon{height:2.2rem;margin-right:.8rem;width:2.2rem}.kg-bookmark-author:after{content:"•";margin:0 .6rem}.kg-bookmark-publisher{max-width:24rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.kg-gallery-container{display:flex;flex-direction:column;margin:0 auto;max-width:104rem;width:100%}.kg-gallery-row{display:flex;flex-direction:row;justify-content:center}.kg-gallery-image{align-items:center;background:#fff;border:1px solid #eee;border:var(--color-border);display:flex;flex:1 1}.kg-gallery-image img{background:#fff;display:block;height:auto;margin:0;max-width:none;width:100%}.kg-gallery-row:not(:first-of-type){margin:1rem 0}.kg-gallery-image:not(:first-of-type){margin:0 0 0 1rem}.kg-gallery-card+.kg-gallery-card,.kg-gallery-card+.kg-image-card.kg-width-wide,.kg-image-card.kg-width-wide+.kg-gallery-card,.kg-image-card.kg-width-wide+.kg-image-card.kg-width-wide{padding:2rem 0}.kg-gallery-card{padding:1rem 0 2rem}.kg-image-card{padding:1rem 0 2.5rem}.kg-card img,.kg-image{border:1px solid #eee;display:block;margin:0 auto}.kg-card img{border:1px solid var(--color-border)}code{color:var(--color-font-code);font-size:1.5rem;margin:0;padding:0;white-space:pre-line;word-break:break-all;word-spacing:0}code,code[class*=language-],pre[class*=language-]{font-family:var(--font-code)!important;font-weight:var(--code-font-weight)!important;letter-spacing:-.05rem;line-height:1.2!important}code[class*=language-],pre[class*=language-]{background:none!important;background-color:var(--color-background-code)!important;font-size:1.4rem!important;margin:0 0 2.5rem!important;text-shadow:none!important}pre[class*=language-]{border:1px solid var(--color-border);border-radius:0!important;overflow:scroll;overflow:auto!important;overflow-x:scroll;overflow-x:auto!important;overflow-y:hidden;padding-top:4.5rem!important}div.code-toolbar>.toolbar{background-color:var(--color-background-secondary)!important;border:1px solid var(--color-background-secondary);border-bottom-color:var(--color-border);display:flex;justify-content:space-between;opacity:1!important;padding:.5rem;right:0!important;top:0!important;transition:opacity .3s ease-in-out!important;width:100%}div.code-toolbar>.toolbar button,div.code-toolbar>.toolbar span{background-color:transparent!important;box-shadow:none!important;font-size:1.4rem!important;font-weight:var(--font-medium);padding:0!important}div.code-toolbar>.toolbar button{color:var(--color-font-code)!important}pre[class*=language-]::-webkit-scrollbar{height:1rem}pre[class*=language-]::-webkit-scrollbar-track{background-color:#293742!important;border-radius:var(--radius)}pre[class*=language-]::-webkit-scrollbar-thumb{background-color:#ff64dc;background-color:#ffffff80;border-radius:var(--radius)}.token.plain{color:#fff!important}.token.entity,.token.operator,.token.url,.token.variable{color:#d7deea!important}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#8dc891!important}.token.keyword{color:#c5a5c5!important}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#79b6f2!important}.token.punctuation{color:#8dc891!important}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999!important}.token.constant,.token.deleted,.token.symbol,.token.tag{color:#fc929e!important}.token.property{color:#5a9bcf!important}.token.boolean,.token.number{color:#ff8b50!important}.token.important,.token.regex{color:#fac863!important}.hide{display:none}.underline{border-bottom:1px solid var(--color-font)}.center{text-align:center}div.code-toolbar{position:relative}div.code-toolbar>.toolbar{opacity:0;position:absolute;right:.2em;top:.3em;transition:opacity .3s ease-in-out;z-index:10}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:none;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{background:#f5f2f0;background:hsla(0,0%,88%,.2);border-radius:.5em;box-shadow:0 2px 0 0 rgba(0,0,0,.2);color:#bbb;font-size:.8em;padding:0 .5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}code[class*=language-],pre[class*=language-]{word-wrap:normal;background:none;color:#f8f8f2;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:1em;-webkit-hyphens:none;hyphens:none;line-height:1.5;-o-tab-size:4;tab-size:4;text-align:left;text-shadow:0 1px rgba(0,0,0,.3);white-space:pre;word-break:normal;word-spacing:normal}pre[class*=language-]{border-radius:.3em;margin:.5em 0;overflow:auto;padding:1em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{border-radius:.3em;padding:.1em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
Tableau ShadowDB on Azure Database for PostgreSQL
December 18, 2020
Tableau ShadowDB on Azure Database for PostgreSQL Background
Numerous Report Developers requested admin access to the Tableau production server database for running daily report, but doing so would create security holes and performance issues.
To avoid granting privileged access to non-admins and reduce database loads on the production database, we built a copy of the transaction database on Azure Database for PostgreSQL that updates automatically on a schedule.
Criteria
Day-old data is OK.
The size of the production database backup file is about 14GB.
A Tableau replica server can not be used for this, because permissions need to be managed separately from the master Tableau server.
The update process must run automatically, take less than three hours, and cause the least amount of disruption possible. It also must be located where network latency will be the least (i.e. avoid Expressroute traffic).
Givens
The following are out of scope for this post, but need to be already in place:
Tableau server - most recent Windows server (cloud VM in my case).
Tableau server backup - saved to directory on Tableau server (non-OS drive).
Azure Database for PostgreSQL service.
Setup Tableau Server
Get Windows sign-in credentials and access the Tableau server through a Remote Desktop connection.
Download PostgreSQL v13 and run the install. Choose to install ONLY the pgAdmin4 and Command Line Tools.
Create Working Directories
Create a folder on the OS drive like c:\_tableau-shadow-database
for a PowerShell script.
Create a folder on the Data drive like e:\_tableau-shadow-database
for managing the updates.
Create PowerShell script
Create a c:\_tableau-shadow-database\tableau-shadow-database.ps1
file, and test each command in the script below until all commands work together.
The script performs the following tasks:
Start logging to a file
Unzip the backup to a working directory, and map where the pg_dump file is located (20 minutes).
Connect to the Azure Database for PSQL service, restore the backup to a temporary database (2 hrs).
Close connections to the old database, remove the old database, rename the temp database to the name of the working database, and set readonly user permissions on the temp database (1 min).
Close the log file.
Schedule Task (Windows)
Go to Start -> Settings -> Apps -> Find a setting -> "Task Scheduler" => Task Scheduler Library
Actions -> Create Task…
General
Name: Run Tableau ShadowDB script
Description: Runs Tableau Shadow Database PowerShell script
Security Options: Run whether user is logged on or not
Triggers -> New
Begin the task -> On a schedule, Weekly, Start: 12/4/2020 9:01PM, Recur every 1 week on Friday, Stop task if it runs longer than 1 day
Actions
Start a program -> Program/script: powershell, Add arguments -File C:_tableau-shadow-database\restore-tableau-db-to-shadow-db.ps1
Conditions
Settings
Allow task to be run on demand
Stop the task if it runs longer than 1 day
If the running task does not end when requested, force it to stop
DB Access Permissions
See https://aws.amazon.com/blogs/database/managing-postgresql-users-and-roles
See https://www.postgresql.org/docs/current/sql-createrole.html
Reporting Users
Define what the default "readonly" role has access to and then create a reporting user that assumes this role.
Grant connect on database
GRANT CONNECT ON DATABASE workgroup TO readonly;
Connect to [databasename] on local database cluster
\c workgroup
Grant Usage to schemas
GRANT USAGE ON SCHEMA public TO readonly;
Grant SELECT access to all current tables
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO readonly;
Grant SELECT access to all future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES to readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO readonly;
Create new user
CREATE USER reporting_user WITH LOGIN NOSUPERUSER INHERIT NOCREATEDB NOCREATEROLE NOREPLICATION PASSWORD '0nCeUp0n@Th^me';
GRANT readonly TO reporting_user;
Test user access by exiting current psql connection and logging in as reporting_user
"c:\Program Files\PostgreSQL\13\bin\psql" -h the-shadow-database.postgres.database.azure.com -p 5432 -U reporting_user@the-shadow-database -d postgres
App Users
Define a new role that has a bit more than "readonly" access to and then create an app user that assumes this role.