<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Praneet Loke</title>
    <description>The latest articles on DEV Community by Praneet Loke (@praneetloke).</description>
    <link>https://dev.to/praneetloke</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F384349%2Fba9e5280-07e0-4e12-a85f-1be9399431ec.jpeg</url>
      <title>DEV Community: Praneet Loke</title>
      <link>https://dev.to/praneetloke</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/praneetloke"/>
    <language>en</language>
    <item>
      <title>Running Folding@Home...in the cloud</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Tue, 19 Jan 2021 21:35:37 +0000</pubDate>
      <link>https://dev.to/praneetloke/running-folding-home-in-the-cloud-56nc</link>
      <guid>https://dev.to/praneetloke/running-folding-home-in-the-cloud-56nc</guid>
      <description>&lt;p&gt;You've probably read about Folding@Home in 2020 and its role in the drug discovery for treating COVID-19.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;To help tackle coronavirus, we want to understand how these viral proteins work and how we can design therapeutics to stop them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://foldingathome.org/diseases/infectious-diseases/covid-19/"&gt;https://foldingathome.org/diseases/infectious-diseases/covid-19/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All over the world people were running F@H to support their call to &lt;a href=""&gt;fight the virus with computing power&lt;/a&gt;. And &lt;a href="https://blogs.nvidia.com/blog/2020/04/01/foldingathome-exaflop-coronavirus/"&gt;the world responded&lt;/a&gt; to this call. There were many notable organizations (&lt;a href="https://home.cern/news/news/cern/cern-contributes-computers-combatting-covid-19"&gt;CERN&lt;/a&gt;, &lt;a href="https://stats.foldingathome.org/team/111065"&gt;EVGA&lt;/a&gt;, &lt;a href="https://stats.foldingathome.org/team/238068"&gt;AWS&lt;/a&gt;, &lt;a href="https://stats.foldingathome.org/team/259918"&gt;NVIDIA&lt;/a&gt; and &lt;a href="https://stats.foldingathome.org/teams-monthly"&gt;more&lt;/a&gt;!) running the Folding@Home client on their machines, thus donating spare cycles for a good cause. For this effort, the team behind F@H put out a special call. They were asking people with dedicated GPU hardware to run the client.&lt;/p&gt;

&lt;p&gt;Unfortunately, for those of us who don't keep up with the world of gaming PCs anymore or simply do not have access to high-end GPUs, all we could do is run the F@H client on our regular machines. It was (and still is!) a good way to help them in their efforts.&lt;/p&gt;

&lt;p&gt;For me, the solution was the public cloud providers. If I can't buy one, I'll rent one. In my case, I ran high-end instances on AWS and Azure for several months back in 2020. But there was no way I would be able to afford running those machines at their full price.&lt;/p&gt;

&lt;p&gt;Enter Spot Instances. The big 3 cloud providers all allow users to run workloads on machines that have unused capacity. Sometimes at a whopping 80% discount on the hourly pricing! This means one can run high end machines at a fraction of the hourly price depending on availability in a certain data center (region). I have been able to amass quite a few &lt;a href="https://stats.foldingathome.org/donor/66949740"&gt;points&lt;/a&gt; on F@H and climbed up the ranks quickly. From being nowhere on the leaderboards to somewhere in the top 30k (my rank has slipped quite a bit since I haven't run any work units in sometime) in a matter of few months. This is due to the way their system credits clients running high priority work units specifically designed to use GPUs.&lt;/p&gt;

&lt;p&gt;After running this infrastructure for several months last year, &lt;strong&gt;I am proud to announce that I am open-sourcing the very application that will allow &lt;em&gt;you&lt;/em&gt; to run Spot Instances on AWS or Azure as well.&lt;/strong&gt; It comes complete with the Pulumi app, as well as GitHub Actions workflows to automate the whole thing. All you need to do is, to configure the cloud credentials on GitHub and set some configuration for Pulumi.&lt;/p&gt;

&lt;p&gt;There is a lot to explain in the Pulumi app, but I hope to do another follow-up post on this topic. My hope is for you to get started on protein folding in the cloud in less than 10 minutes. I have tried to add as much details as I can in the README. But don't be shy if you have questions. Ask me! :)&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/praneetloke"&gt;
        praneetloke
      &lt;/a&gt; / &lt;a href="https://github.com/praneetloke/FoldingInTheCloud"&gt;
        FoldingInTheCloud
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Run Folding@Home...in the cloud.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I am open-sourcing this because F@H does important research. They grew in popularity in 2020 due to COVID-19. But they do a lot of &lt;a href="https://foldingathome.org/diseases/"&gt;other&lt;/a&gt; research too! You should definitely read more about them.&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>aws</category>
      <category>azure</category>
      <category>typescript</category>
    </item>
    <item>
      <title>A closer look at Stadia and gaming in the cloud</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Wed, 06 Jan 2021 03:43:40 +0000</pubDate>
      <link>https://dev.to/praneetloke/a-closer-look-at-stadia-and-gaming-in-the-cloud-cfn</link>
      <guid>https://dev.to/praneetloke/a-closer-look-at-stadia-and-gaming-in-the-cloud-cfn</guid>
      <description>&lt;p&gt;What a time we live in when one of the most anticipated games of the last decade plays better on Google Stadia than on dedicated gaming hardware in your living room! I am talking about consoles. Regardless of whether you are a gamer or not, it is likely that you've read about Cyberpunk 2077 and its performance on last gen consoles, which can still be found in hundreds of millions of households. Especially given that its been in development for quite literally a decade and comes from one of the most acclaimed developers CD Projekt Red (CDPR).&lt;/p&gt;

&lt;p&gt;CDPR is also the maker of The Witcher series of games. You know, the hit Netflix show of the same name. Both inspired by &lt;a href="https://en.wikipedia.org/wiki/The_Witcher"&gt;the books&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Nevertheless, as a gamer and a cloud technology enthusiast I was excited to take a look and experience this all for myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  A New Hope?
&lt;/h2&gt;

&lt;p&gt;When Google announced Stadia about 2 years ago, the gaming community was rather quick to judge it; myself included. That's because that sort of tech has been pushed upon the gaming community in the past. &lt;a href="https://en.wikipedia.org/wiki/OnLive"&gt;OnLive&lt;/a&gt; was a service that existed sometime ago. It seems that Stadia is breathing new life into this concept. I'd say their timing couldn't be better. I can't think of a better story for them to tell than the time when their platform outshined consoles. Admittedly, this is because of the way the game performs on traditional consoles, but also because some of Stadia's promises held their ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  OnLive vs. Stadia
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirements&lt;/th&gt;
&lt;th&gt;OnLive&lt;/th&gt;
&lt;th&gt;Google Stadia&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom controller&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;You don't need the Stadia controller if you only plan on playing on your computer. But if you want to have a decent gaming experience on your TV without using your laptop/desktop to cast it to your TV you will need their controller. That's fair. More on this later on.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom streaming device&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Although, in Google's defense, the Chromecast doubles as a Miracast device so it is more than just for Stadia.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Stadia's Pro subscription is completely optional. That is, if you want to play in 4k with 5.1 surround sound, you'd better pony up $10/month. But to take advantage of your Pro subscription, you'll need the Chromecast Ultra. Google's 4k streaming device.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All in all, you can see where I am going with this. The similarities are there in terms of how this is setup. However, the technology running in the data center is vastly different. For instance, Wikipedia says OnLive used custom video compression chips on their servers which they operated in their own data centers across the US.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stadia Hardware
&lt;/h2&gt;

&lt;p&gt;There is no need for speculation on what hardware Stadia might be using, since they have it right on their &lt;a href="https://stadia.dev/intl/en_us/about/"&gt;website&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At the time of this writing, it is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Upc3dI07--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/5k9bhzh7z98gzd6f9x1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Upc3dI07--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/5k9bhzh7z98gzd6f9x1l.png" alt="Stadia hardware specs"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For good measure, &lt;a href="https://stadia.dev/blog/google-partners-with-amd-for-custom-stadia-gpu/"&gt;Google partnered with AMD&lt;/a&gt; to design the GPUs for these machines. The consoles from both Microsoft and Sony have had custom AMD-designed GPUs too. The next-gen consoles have all AMD chipset too.&lt;/p&gt;

&lt;p&gt;Those specs are no joke either. They are the sort of specs Microsoft and Sony are touting as &lt;a href="https://www.theverge.com/2020/3/18/21185141/ps5-playstation-5-xbox-series-x-comparison-specs-features-release-date"&gt;next-gen hardware&lt;/a&gt;. More or less. But the benefit of Stadia is that hardware can be upgraded seamlessly whereas consoles cannot be. Or so they promise. How often these "transparent" hardware upgrades come about will surely meet the reality of economics, I say. Again, it stands to be seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stream
&lt;/h2&gt;

&lt;p&gt;Despite Stadia's perceived dubious launch, the technology is no piece of cake. It's pretty amazing cloud technology. I have been playing Cyberpunk 2077 on Stadia for about 2 weeks now and sometimes I forget that I am not playing the game "locally".&lt;/p&gt;

&lt;p&gt;Let me explain. Much like its video streaming counterparts like Netflix and Hulu, cloud gaming also has a video stream. But unlike video streaming, Stadia cannot buffer 5 mins of a game's stream on your Chromecast or computer or wherever it is you are playing. It's like watching live TV, but without any artificial delays other than the network latency itself. For the network latency to not affect the game too much, the video stream must be compressed and also adjust according to dips in network quality. It is in Google's best interest to really concentrate their efforts in ensuring the best gaming experience. The first sign of a poor gaming experience and they are done. The gaming market as big as it is, is also pretty unforgiving. The fact that a AAA game publisher like CDPR rushed to release -- a game that's been in development for about a decade -- in time for the holidays after a series of delays angered "gamers" on social media (to the point where &lt;a href="https://www.theverge.com/2020/10/28/21538525/cyberpunk-2077-cd-projekt-red-death-threats-game-delay"&gt;the devs were receiving death threats!&lt;/a&gt;), has got to say something about the kind of pressure this market puts on companies. I don't judge them and would like to give devs the benefit of the doubt. CDPR is certainly not the first (&lt;a href="https://www.bbc.com/news/technology-30226586"&gt;Ubisoft&lt;/a&gt;, &lt;a href="https://www.forbes.com/sites/erikkain/2019/05/30/why-anthem-failed-and-why-it-was-never-destined-to-succeed/?sh=99e060a2c906"&gt;EA&lt;/a&gt; are guilty of this too!) and won't be the last company to find themselves in trouble for releasing a buggy AAA game.&lt;/p&gt;

&lt;p&gt;Stadia's dedicated cloud gaming hardware renders the game using the dedicated GPU. I suspect these custom GPUs also have a dedicated hardware-based encoder of some sort. It wouldn't be too far fetched to think that because &lt;a href="https://developer.nvidia.com/nvidia-video-codec-sdk"&gt;Nvidia's latest generation of GPUs support hardware-based encoding&lt;/a&gt; and thus freeing up the CPU for other things. This ensures that you have a stutter free streaming. These streams also need to be compressed efficiently (probably using the most common video codec H.264 or who knows Google might have even built a custom video codec for speedy delivery!)&lt;/p&gt;

&lt;p&gt;With a video streaming to your Chromecast, it is now time to discuss the controller. The controller is its own technical marvel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Controller
&lt;/h2&gt;

&lt;p&gt;As per &lt;a href="https://dev.to/vuild/stadia-developer-tools-resources-3998"&gt;another article&lt;/a&gt;, Google has a &lt;a href="https://pdfaiw.uspto.gov/.aiw?Docid=20190068667&amp;amp;homeurl=http%3A%2F%2Fappft.uspto.gov%2Fnetacgi%2Fnph-Parser%3FSect1%3DPTO1%2526Sect2%3DHITOFF%2526d%3DPG01%2526p%3D1%2526u%3D%2Fnetahtml%2FPTO%2Fsrchnum.html%2526r%3D1%2526f%3DG%2526l%3D50%2526s1%3D20190068667.PGNR.%2526OS%3DDN%2F20190068667%2526RS%3DDN%2F20190068667&amp;amp;PageNum=&amp;amp;Rtype=&amp;amp;SectionNum=&amp;amp;idkey=299E14B741CB"&gt;patent&lt;/a&gt; for the controller. I haven't read through the patent myself and haven't confirmed the relevancy to the Google Stadia controller. I wonder how different this is from OnLive's controller, though, since its controller setup may have had to be similar to Stadia's.&lt;/p&gt;

&lt;p&gt;The controller needs to have an ultra-low latency connection with Google's servers for there not to be any perceived latency. Remember, what you see on your TV (or on your Chrome browser) is merely a video stream of a game that is being played elsewhere. Hopefully, at a data center closest to you. Your controller input needs to travel up to the Google's servers, rendered and the stream comes down to your TV. That's a lot of traveling if you ask me.&lt;/p&gt;

&lt;p&gt;Digital Foundry's &lt;a href="https://www.eurogamer.net/authors/175"&gt;Richard Leadbetter&lt;/a&gt; demonstrated measuring the latency between the controller's input and the game's stream updating according to the input.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/9uwunZ2a4gc"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting The Best Experience
&lt;/h2&gt;

&lt;p&gt;While I have tried to play Cyberpunk 2077 on Stadia using Wifi, the experience is just not good enough. Even while connected to the 5Ghz band of your router, in real world scenarios, you are likely to encounter some amount of interference or crowding. There is simply no replacement for a hard-wired connection here. To eliminate any possibility of a bandwidth reduction, you must use a wired connection and also ensure that you have nothing else in your network that is internet-heavy (like watching YT, Netflix, Hulu etc.) It is also the reason why Stadia &lt;em&gt;recommends&lt;/em&gt; playing using a hard-wired internet connection. Although I think they should be stronger with that recommendation, almost to the point where they require it, like OnLive did&lt;/p&gt;

&lt;p&gt;Perhaps with more devices certifying for Wifi 6, it may not be much of a problem in the near future since the standard specifically addresses maintaining QoS for multiple devices simultaneously. But there is nothing like a good ol' cable capable of handling Gigabit speeds. A nice Cat 6 cable will be more than enough in this situation. Beyond that, you are talking data center cabling which is not required and quite honestly you will never realize its potential in home use.&lt;/p&gt;

&lt;p&gt;Lastly, I recommend getting the Chromecast Ultra. Its power adapter comes with an ethernet port and offers up to 4k streaming for games played on Stadia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;The cost of all this of course cannot be simple. Let's try to calculate the cost.&lt;/p&gt;

&lt;p&gt;Since Stadia uses custom hardware, we can only draw approximations from similar hardware from their cloud services offering.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Montly Cost (USD)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nvidia Tesla T4 GPU&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;160 (approx.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2 instance type (accelerator-optimized CPU) vCPU (including memory)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;30 (approx.)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The above prices are monthly costs for a consumer using those resources on GCP. That's easily over the price of one next-gen console. I haven't even included the storage costs here and probably some other costs that I am not even aware of.&lt;/p&gt;

&lt;p&gt;Those machines are pushing out a lot of data with the 4k 60fps promises Google is making for Pro subscribers. The subscription costs just $120/year. They have got to be losing money here in order to gain gamers' trust. I doubt the free Stadia experience comes cheap either. There is still cost involved in operating those machines. And you only pay for the games in the free experience whereas in the Pro tier you pay for the games, get monthly free games (of Indie quality) and get 4k (whenever possible). That's a great value for $9.99 if you ask me.&lt;/p&gt;

&lt;p&gt;It is a herculean effort to have people ditch their consoles (or just "one highly anticipated game performing poorly on consoles" 😉) so I guess this is Google's way of getting more people to signup for the service to maybe try and offset the cost in the future. I can't imagine the Pro subscription staying at $9.99/month for too long into the future quite frankly. OnLive offered a plan for roughly $15/month. That was back in 2010. Everything's way more expensive now and Stadia is certainly not using 2010 hardware. These are high end specs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Alone
&lt;/h2&gt;

&lt;p&gt;Stadia is not alone in its foray into the &lt;a href="https://www.statista.com/topics/868/video-games/"&gt;$60bn USD&lt;/a&gt; gamingmarket. Amazon tried its hand and although, it met with failure, it appears it hasn't given up just yet. Amazon's approach was to start a gaming studio from the ground-up, though. And not to compete with the console market like Google is.&lt;/p&gt;

&lt;p&gt;But I wouldn't be surprised if Amazon enters changes direction or perhaps starts something parallel to its game development efforts. Seeing what Google is up to and the sudden success of Stadia is hard to ignore for a company like Amazon given that its cloud computing arm, AWS, has been partnering with Nvidia to create custom GPUs for all sorts of &lt;a href="https://www.nvidia.com/en-us/data-center/gpu-cloud-computing/amazon-web-services/"&gt;things&lt;/a&gt;. In fact, cloud gaming is one of the use cases for the AWS-specific G4 instances that sport an Nvidia T4 GPU. They are fully capable of what Stadia is doing along with RTX. They even have gaming drivers that you can install on Linux, Windows Server 2019, and Ubuntu.&lt;/p&gt;

&lt;p&gt;Then there's Nvidia's own GeForce Now service and the Nvidia Shield console and controller. GeForce Now is a cloud gaming service, but not in the same way that Stadia is. With GeForce Now you need to buy the game from a service like Valve's Steam and give GeForce Now access to it. Then you can use Nvidia's cloud prowess to play the game with the highest specs, though, there are some &lt;a href="https://nvidia.custhelp.com/app/answers/detail/a_id/4974/~/how-long-can-i-play-games-on-geforce-now%3F"&gt;limitations&lt;/a&gt;. I thought I read somewhere that you can't play the game whenever you would like and that you'll have to enter a queue during busy times. The one big reason why this could be attractive to gamers will experience Nvidia's famed RTX. A feature that is typically attributed to high-end gaming GPUs and most recently the next-gen consoles. RTX is one of the biggest selling points for the next-gen hardware.&lt;/p&gt;

&lt;p&gt;Speaking of Steam, Valve offers &lt;a href="https://store.steampowered.com/steamlink/about/"&gt;Steam Link&lt;/a&gt; for free. A service that allows you to play a game running on another computer. Sounds like something you have read before, yes? 🙂&lt;/p&gt;

&lt;p&gt;Finally, let's not forget Microsoft and Sony. Microsoft is already offering &lt;a href="https://en.wikipedia.org/wiki/Xbox_Cloud_Gaming"&gt;Project xCloud&lt;/a&gt; in the form of its Xbox Game Pass subscription. And what about Sony? Well, that's a &lt;a href="https://www.theverge.com/2019/12/5/20993828/sony-playstation-now-cloud-gaming-gaikai-onlive-google-stadia-25th-anniversary"&gt;different story&lt;/a&gt;. We'll have to wait for Sony's move on this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Since the early 2010s, cloud hardware has come a long way. I haven't even talked about the memory and the SSD that Stadia offers in its hardware stack right from its first generation. That in itself is a generational leap for the traditional living room consoles. Seriously, the next-gen consoles Xbox Series X/S and Sony's PS5 just now have built-in SSD along with GDDR6 memory. Cloud gaming certainly has some strict network requirements and its success is rooted in consumers having the best internet connection. Not to mention the proximity of these cloud gaming capable machines, housed in data centers, to the consumers. I don't believe it will replace traditional gaming experiences, be it through consoles or gaming PCs. If there is one thing 2020 has taught the world, it's that anything can happen.&lt;/p&gt;

&lt;p&gt;I am excited to see what this next decade brings for gamers. One thing's for certain, a new contender has entered the arena. Its name is Stadia.&lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>gaming</category>
      <category>stadia</category>
      <category>googlecloud</category>
    </item>
    <item>
      <title>I wrote a browser extension to protect your privacy</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Fri, 02 Oct 2020 01:05:11 +0000</pubDate>
      <link>https://dev.to/praneetloke/i-wrote-a-browser-extension-to-protect-your-privacy-1nje</link>
      <guid>https://dev.to/praneetloke/i-wrote-a-browser-extension-to-protect-your-privacy-1nje</guid>
      <description>&lt;p&gt;Ever wonder if you could simply mask your tab while you were sharing your screen during a meeting or because someone is by your desk? Well, maybe not the latter recently because you know...🤷‍♂️. The extension allows you to mask any tab or if you want to mask all open tabs in the current window, there is a shortcut key for that. Oh and you can customize the shortcut key bindings to your liking. You are most likely going to have to customize it since it seems that most shortcut combinations that I could think of were in conflict with something else 🤦‍♂️.&lt;/p&gt;

&lt;p&gt;The browser extension is available for Firefox, and Edge (Chromium) right now, with Chrome becoming available pretty soon! Download the extension for Firefox &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/privacy-shade/" rel="noopener noreferrer"&gt;here&lt;/a&gt; and Edge &lt;a href="https://microsoftedge.microsoft.com/addons/detail/shade/beejdgamkplgnpoabpkgpkdcbdhhnial" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you have any feedback or issues to report, please consider submitting an issue using one of the templates &lt;a href="https://github.com/praneetloke/shade-browser-extension/issues/new/choose" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's a gif showing you how it works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Feaqvc5gcb9gr6wai12dk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Feaqvc5gcb9gr6wai12dk.gif" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Roll your own Instagram gallery in 5 mins</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Fri, 28 Aug 2020 16:00:26 +0000</pubDate>
      <link>https://dev.to/praneetloke/roll-your-own-instagram-gallery-in-5-mins-3fjf</link>
      <guid>https://dev.to/praneetloke/roll-your-own-instagram-gallery-in-5-mins-3fjf</guid>
      <description>&lt;p&gt;A client had asked me about showing Instagram pictures from their own account on their website. A bit similar to Twitter's own &lt;a href="https://developer.twitter.com/en/docs/twitter-for-websites/timelines/overview"&gt;timeline widget&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;However, Instagram doesn't have a widget to display pictures in a feed. So I built one myself. This is a post about how you could do that for your website too. It took a bit of reverse-engineering but it all worked in the end and hopefully stays that way.🤞&lt;/p&gt;

&lt;p&gt;Although this post talks about a single Instagram account, you could modify what I show here to pull images from other accounts too. I make no guarantees!&lt;/p&gt;

&lt;h2&gt;
  
  
  The Facts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Instagram has a &lt;a href="https://developers.facebook.com/docs/instagram"&gt;developer API platform&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The method this post covers is only for publicly accessible accounts.&lt;/li&gt;
&lt;li&gt;If you need to show pictures from a private account, you must use the official Instagram API platform to register an app, get an API key, get it reviewed etc. The same applies if you want to use their platform to even show public images in a feed, unlike Twitter's timeline widget for public profiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Query Hash
&lt;/h2&gt;

&lt;p&gt;Navigate to the Instagram account you want to show pictures from and open the Developer Tools and click on the &lt;strong&gt;Network&lt;/strong&gt; tab. With the Network tab open, refresh the Instagram page you are viewing. Then click on the &lt;strong&gt;XHR&lt;/strong&gt; filter. Here's what it looks like for me in the latest Firefox browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XVnP83OY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/x1rs06ptml4oob63y5l1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XVnP83OY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/x1rs06ptml4oob63y5l1.png" alt="Firefox Developer Tools"&gt;&lt;/a&gt;&lt;/p&gt;
Network tab showing only XHRs



&lt;p&gt;Instagram uses GraphQL to fetch the posts, where each post contains URLs to different sizes of the picture, stats (likes, comments etc.), author and much more. We need the query hash from the request so we can make the same API call ourselves.&lt;/p&gt;

&lt;p&gt;You can find the query hash from the request made to the endpoint &lt;code&gt;https://www.instagram.com/graphql/query/&lt;/code&gt;. Once again you can filter the requests using the textbox to find the specific request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--FtP2VrxA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4qwzxz5o4oqp7znxg7si.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FtP2VrxA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4qwzxz5o4oqp7znxg7si.png" alt="Instagram GraphQL API"&gt;&lt;/a&gt;&lt;/p&gt;
Instagram GraphQL API request



&lt;p&gt;Here's the full URL from Developer Tools window:&lt;br&gt;
&lt;code&gt;https://www.instagram.com/graphql/query/?query_hash=d4d88dc1500312af6f937f7b804c68c3&amp;amp;variables={"user_id":"249074882","include_chaining":false,"include_reel":false,"include_suggested_users":false,"include_logged_out_extras":true,"include_highlight_reels":true,"include_live_status":true}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Save the value of the &lt;code&gt;query_hash&lt;/code&gt; query-parameter for later. We'll plug that into our own request to the same API endpoint.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Vue Component
&lt;/h2&gt;

&lt;p&gt;While the following is a Vue component, you could very easily do the same with any other front-end framework.&lt;/p&gt;

&lt;p&gt;Declare some constants for our API calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// These two URLs will be the same regardless of the account&lt;/span&gt;
&lt;span class="c1"&gt;// you want to show pictures from.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IG_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.instagram.com/graphql/query/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IG_POST_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.instagram.com/p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The `query_hash` value from before.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIMELINE_QUERY_HASH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;d4d88dc1500312af6f937f7b804c68c3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// The number of images to fetch per page.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMAGES_PER_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make the API call&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;249074882&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;first&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;IMAGES_PER_PAGE&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IG_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query_hash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TIMELINE_QUERY_HASH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;variables&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transform the data for display. Feel free to modify this according to what you want to show.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;transformData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edges&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;edges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;__typename&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GraphImage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thumbnails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thumbnail_resources&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thumbnails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config_width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imgProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thumbnails&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;IG_POST_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shortcode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it. With just those few lines, we have made an API call and retrieved a page of Instagram posts just like Instagram's own official site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting it all together
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"instagram mb-10"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"d-flex flex-row flex-md-row flex-column flex-sm-column justify-center align-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;v-card&lt;/span&gt;
        &lt;span class="na"&gt;v-for=&lt;/span&gt;&lt;span class="s"&gt;"(post, index) in posts"&lt;/span&gt;
        &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"index"&lt;/span&gt;
        &lt;span class="na"&gt;tile&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pa-2 ma-2"&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt;
          &lt;span class="na"&gt;:href=&lt;/span&gt;&lt;span class="s"&gt;"post.href"&lt;/span&gt;
          &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;
          &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"noopener"&lt;/span&gt;
          &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
            &lt;span class="na"&gt;:src=&lt;/span&gt;&lt;span class="s"&gt;"post.url"&lt;/span&gt;
            &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;
            &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"auto"&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/v-card&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IG_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.instagram.com/graphql/query/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IG_POST_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.instagram.com/p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TIMELINE_QUERY_HASH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;9dcf6e1a98bc7f6e92953d5a61027b98&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMAGES_PER_PAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Instagram&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="na"&gt;imgProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge_owner_to_timeline_media&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transformData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edge_owner_to_timeline_media&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20214540375&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;first&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;IMAGES_PER_PAGE&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IG_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query_hash&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TIMELINE_QUERY_HASH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;variables&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;

      &lt;span class="nx"&gt;transformData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edges&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;edges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;__typename&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GraphImage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thumbnails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;thumbnail_resources&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thumbnails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;config_width&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imgProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thumbnails&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;IG_POST_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shortcode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"scss"&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nc"&gt;.instagram&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="nf"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;640px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.instagram&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.ig-container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;.columns&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it for now. If you like this post and want me to write about something else, please let me know! Until next time, see ya! 👋&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Increasing Confidence in Infrastructure Changes</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Fri, 28 Aug 2020 05:33:10 +0000</pubDate>
      <link>https://dev.to/praneetloke/increasing-confidence-in-infrastructure-changes-3gh4</link>
      <guid>https://dev.to/praneetloke/increasing-confidence-in-infrastructure-changes-3gh4</guid>
      <description>&lt;p&gt;Pulumi is an infrastructure as code platform that lets you write cloud infrastructure using popular general-purpose languages (Go, JS, TS, Python, and .NET).&lt;/p&gt;

&lt;p&gt;Running &lt;a href="https://www.pulumi.com/docs/guides/continuous-delivery/gitlab-ci/"&gt;Pulumi in GitLab CI/CD&lt;/a&gt; is very easy and if you have ever wondered "is there a tool that tells me how my changes will affect my infrastructure", then that's exactly what running &lt;code&gt;pulumi preview&lt;/code&gt; will give you.&lt;/p&gt;

&lt;p&gt;Outputs of all Pulumi CLI commands are viewable in the &lt;a href="https://app.pulumi.com"&gt;Pulumi Console&lt;/a&gt; too. You could also view any Pulumi command executions in realtime on the Console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uimSBpqQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/guo58mv9pus57amsgqe9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uimSBpqQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/guo58mv9pus57amsgqe9.png" alt="Summary output as viewed in the Pulumi Console"&gt;&lt;/a&gt;&lt;/p&gt;
Summary output as viewed in the Pulumi Console



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--OgmUtgHq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/8m2q1zjrc7ux4ajl4kcx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--OgmUtgHq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/8m2q1zjrc7ux4ajl4kcx.png" alt="Types of logs"&gt;&lt;/a&gt;&lt;/p&gt;
Use the toggler to view different types of logs



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KpEhDlR---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/k7usjzldqzgyk1hqjwnc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KpEhDlR---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/k7usjzldqzgyk1hqjwnc.png" alt="Diff output as viewed in the Pulumi Console"&gt;&lt;/a&gt;&lt;/p&gt;
Diff output as viewed in the Pulumi Console



&lt;p&gt;There is one problem, though. How do you, as the reviewer, ensure that you see this summary during your review? You could check the Pulumi summary in the build pipeline logs, but it can be a bit cumbersome to have to navigate to the pipeline logs if you have to do that repeatedly. It is also likely that reviewers miss looking at the pipeline builds and thereby potentially missing some crucial unintended impacts to your infrastructure.&lt;/p&gt;

&lt;p&gt;Enter &lt;a href="https://www.pulumi.com/blog/gitlab-project-integration/"&gt;Pulumi integration&lt;/a&gt; for GitLab Merge Requests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SVTB-U0a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1ibeohf6j2z91dye2cfx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SVTB-U0a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1ibeohf6j2z91dye2cfx.png" alt="Pulumi integration for GitLab"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SNgnBaI3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/nc7c8s41uj5j75jqmdrx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SNgnBaI3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/nc7c8s41uj5j75jqmdrx.png" alt="Infrastructure summary as a comment"&gt;&lt;/a&gt;&lt;/p&gt;
Infrastructure summary in the comment



&lt;p&gt;So there you have it, folks! A summary of your infrastructure changes delivered right to your comments in the merge request, so you never miss a change again.&lt;/p&gt;

&lt;p&gt;Haven't started with Pulumi yet? What are you waiting for? Click &lt;a href="https://pulumi.com/start"&gt;here&lt;/a&gt; to get started.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>gitlab</category>
      <category>pulumi</category>
    </item>
    <item>
      <title>Unit Testing Angular Components That Use MatIconRegistry</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Fri, 12 Jun 2020 18:48:08 +0000</pubDate>
      <link>https://dev.to/praneetloke/unit-testing-angular-components-that-use-maticonregistry-a9h</link>
      <guid>https://dev.to/praneetloke/unit-testing-angular-components-that-use-maticonregistry-a9h</guid>
      <description>&lt;p&gt;If you use an Angular Material component (part of Angular Components) that relies on the &lt;code&gt;MatIconRegistry&lt;/code&gt;, chances are you have wondered how you could test it with mock data. I recently ran into this, and figured I would turn it into a post as a way to document it for myself. Who knows -- maybe one of you out there might find this helpful as well!&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MatIconRegistry&lt;/code&gt; is useful if you have a custom SVG icon set that you want to use within your app. This allows you to explicitly add the icons under a custom namespace using the &lt;code&gt;addSvgIcon(iconName: string, url: string)&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Let's say you use the &lt;code&gt;mat-icon&lt;/code&gt; component in &lt;em&gt;your&lt;/em&gt; component's template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;mat-icon&lt;/span&gt; &lt;span class="na"&gt;svgIcon=&lt;/span&gt;&lt;span class="s"&gt;"icon-name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/mat-icon&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...testing your component means you'll need to ensure that the the icon is served properly, at least with a fake icon.&lt;/p&gt;

&lt;p&gt;To do that, you'll need to first add the icon names that your component expects to find in the registry. Ideally, you'd do this in the &lt;code&gt;beforeEach&lt;/code&gt; setup function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClientTestingModule&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/common/http/testing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TestBed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/core/testing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MatIconRegistry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/material/icon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DomSanitizer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/platform-browser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TestingComponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeSvg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;svg&amp;gt;&amp;lt;path id="someId" name="someSvg"&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeIconPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/fake/icon.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;TestBed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configureTestingModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;HttpClientTestingModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;compileComponents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;MatIconRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DomSanitizer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;mir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MatIconRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sanitizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DomSanitizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The `MatIconRegistry` will make GET requests to fetch any SVG icons that are in the registry. More on this below...&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitizedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sanitizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bypassSecurityTrustResourceUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeIconPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Make sure that the icon name matches the icon name your component would be looking up.&lt;/span&gt;
    &lt;span class="nx"&gt;mir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addSvgIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;icon-name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sanitizedUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test my component&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="c1"&gt;// Create a test fixture of your component's instance and test your component.&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should do it, right? Nope. Your test would likely fail with a timeout error. But the reason may not be immediately clear. The answer lies within the functionality of the &lt;code&gt;MatIconRegistry&lt;/code&gt; and what happens when you use the attribute with &lt;code&gt;svgIcon&lt;/code&gt; with a &lt;code&gt;&amp;lt;mat-icon&amp;gt;&lt;/code&gt; component.&lt;/p&gt;

&lt;p&gt;Looking at the docs for &lt;a href="https://material.angular.io/components/icon/overview#svg-icons"&gt;&lt;code&gt;&amp;lt;mat-icon&amp;gt;&lt;/code&gt;&lt;/a&gt;, note how it says this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In order to guard against XSS vulnerabilities, any SVG URLs and HTML strings passed to the MatIconRegistry must be marked as trusted by using Angular's DomSanitizer service.&lt;/p&gt;

&lt;p&gt;MatIconRegistry fetches all remote SVG icons via Angular's HttpClient service. If you haven't included HttpClientModule from the @angular/common/http package in your NgModule imports, you will get an error at runtime.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's good. We did that in our &lt;code&gt;beforeEach&lt;/code&gt; call above. Note that for unit testing we use &lt;code&gt;HttpClientTestingModule&lt;/code&gt; and not &lt;code&gt;HttpClientModule&lt;/code&gt; because, well, it's a unit testing compatible HTTP client. One that doesn't actually hit a real endpoint. This means we need to control the response we send back to callers making HTTP requests based on the URL they request. Sort of like setting up a method spy and returning a mock value when the method is called. Only here we would be matching against the URL being requested and return an appropriate response.&lt;/p&gt;

&lt;p&gt;So let's update our snippet above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpClientTestingModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpTestingController&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/common/http/testing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TestBed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/core/testing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;MatIconRegistry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/material/icon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DomSanitizer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@angular/platform-browser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TestingComponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeSvg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;svg&amp;gt;&amp;lt;path id="someId" name="someSvg"&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fakeIconPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/fake/icon.svg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;TestBed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configureTestingModule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;HttpClientTestingModule&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;compileComponents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;httpTestingController&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TestBed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HttpTestingController&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;MatIconRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DomSanitizer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;mir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MatIconRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sanitizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DomSanitizer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The `MatIconRegistry` will make GET requests to fetch any SVG icons that are in the registry. More on this below...&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitizedUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sanitizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bypassSecurityTrustResourceUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeIconPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Make sure that the icon name matches the icon name your component would be looking up.&lt;/span&gt;
    &lt;span class="nx"&gt;mir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addSvgIcon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;icon-name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sanitizedUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test my component&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="c1"&gt;// Create a test fixture of your component's instance and test your component.&lt;/span&gt;
     &lt;span class="p"&gt;...&lt;/span&gt;
     &lt;span class="p"&gt;...&lt;/span&gt;
     &lt;span class="c1"&gt;// Use this to capture icon requests&lt;/span&gt;
     &lt;span class="c1"&gt;// and flush it manually so that the source observable will emit a value. Otherwise, async calls will timeout waiting for a response.&lt;/span&gt;
     &lt;span class="nx"&gt;httpTestingController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeIconPath&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fakeSvg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Happy testing!🙂&lt;/p&gt;

</description>
      <category>angular</category>
      <category>testing</category>
    </item>
    <item>
      <title>Coding Home Automation - Part 2</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Fri, 12 Jun 2020 03:55:53 +0000</pubDate>
      <link>https://dev.to/praneetloke/coding-home-automation-part-2-4phg</link>
      <guid>https://dev.to/praneetloke/coding-home-automation-part-2-4phg</guid>
      <description>&lt;p&gt;In the previous post, I walked-through a home automation recipe where I showed you how easy it is to wire-up existing automation platform with your own custom solution that runs in the cloud. Let's take a look at how to deploy that on Azure using code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Ready
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install the Pulumi CLI from &lt;a href="https://www.pulumi.com/docs/get-started/install/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=praneetloke_home_automation"&gt;https://www.pulumi.com/docs/get-started/install/&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The next step is to create an account on &lt;a href="https://app.pulumi.com/signup?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=praneetloke_home_automation"&gt;Pulumi Console&lt;/a&gt;, and I'll explain why you should signup for an account later in this post.&lt;/li&gt;
&lt;li&gt;Optionally, &lt;a href="https://www.pulumi.com/docs/intro/cloud-providers/azure/setup/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=praneetloke_home_automation#service-principal-authentication"&gt;create an Azure Service Principal&lt;/a&gt;, so you don't have to re-authenticate when your credentials expire.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project Creation
&lt;/h2&gt;

&lt;p&gt;Create an empty directory somewhere on your local disk. You can init a git repo as well if you'd like, but not necessary for using Pulumi for this project. If you are looking to use Pulumi at work, you should definitely use an SCM of some kind.&lt;/p&gt;

&lt;p&gt;Open your favorite terminal window depending on your OS, and &lt;code&gt;cd&lt;/code&gt; to your newly-created directory, then run &lt;code&gt;pulumi new&lt;/code&gt; (assuming &lt;code&gt;pulumi&lt;/code&gt; is on your &lt;code&gt;PATH&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This will show you a few templates, that you can use to create a project. You don't need to use any of them. In fact, you don't even have to run &lt;code&gt;pulumi new&lt;/code&gt;. You can do this all manually if you prefer that, but it's certainly an easy way to get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Imports And Config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/azure&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take a look at the imports at the top of the &lt;code&gt;index.ts&lt;/code&gt; file. All pulumi npm packages will be under &lt;code&gt;@pulumi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pulumi/pulumi&lt;/code&gt; is the core Pulumi SDK providing things like the configuration class, the programming model constructs for things components and custom resources. It also contains the types for recognizing inputs and outputs, and some helpers to work with both of those.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@pulumi/azure&lt;/code&gt; is the Azure-specific SDK for creating Azure resources.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;path&lt;/code&gt; is the standard NodeJS package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildFunctionsProject&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./projectBuilder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/praneetloke/GarageDoorMonitor/blob/master/infrastructure/projectBuilder.ts"&gt;&lt;code&gt;projectBuilder&lt;/code&gt;&lt;/a&gt; is a TypeScript file that exports just one function &lt;code&gt;buildFunctionsProject&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;namePrefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grge-mon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Config&lt;/code&gt; class is used to retrieve config key/values stored in the &lt;code&gt;Pulumi.&amp;lt;stack_name&amp;gt;.yaml&lt;/code&gt; file. Learn more &lt;a href="https://www.pulumi.com/docs/intro/concepts/config/"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;twilioAccountToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requireSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;twilioAccountToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a really cool part. You can add secrets to your config and retrieve them easily in your code. Pulumi will track the value of this variable as a secret. You add secrets to your stack config using &lt;code&gt;pulumi config --secret set &amp;lt;key&amp;gt; &amp;lt;value&amp;gt;&lt;/code&gt;. Learn more &lt;a href="https://www.pulumi.com/docs/intro/concepts/config/#configuring-secrets-encryption"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;buildFunctionsProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GarageDoorMonitor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As mentioned before, the &lt;code&gt;buildFunctionsProject&lt;/code&gt; is an exported function used from another file. This function builds our .NET Core Functions project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating The Infrastructure
&lt;/h2&gt;

&lt;p&gt;Alright. Let's create some Azure resources. 🏗️&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resourceGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResourceGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;namePrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-group`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we will need a resource group to put our resources into. This is an Azure construct and not a Pulumi-specific thing. If you have worked with Azure, everything you know about it applies even when you use a programming language in Pulumi.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure KeyVault
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;kv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keyvault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;KeyVault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;namePrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-vault`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;resourceGroupName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resourceGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;skuName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accessPolicies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// The current principal has to be granted permissions to Key Vault so that it can actually add and then remove&lt;/span&gt;
        &lt;span class="c1"&gt;// secrets to/from the Key Vault. Otherwise, 'pulumi up' and 'pulumi destroy' operations will fail.&lt;/span&gt;
        &lt;span class="c1"&gt;//&lt;/span&gt;
        &lt;span class="c1"&gt;// NOTE: This object ID value is NOT what you see in the Azure AD's App Registration screen.&lt;/span&gt;
        &lt;span class="c1"&gt;// Run `az ad sp show` from the Azure CLI to list the correct Object ID to use here.&lt;/span&gt;
        &lt;span class="na"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-SP-object-ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;secretPermissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;set&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We create the KeyVault resource using the &lt;code&gt;new&lt;/code&gt; operator. This sort of looks like the properties in the ARM template for creating a KeyVault, except, here you get the advantage of strongly-typed arguments. This makes it really easy to specify values without second-guessing what you need to provide to a property.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: A KeyVault resource in Azure uses Access Policies to restrict who can administer it. This also means that if you use a service principal (or your own personal account) to run the Pulumi app, it will need access to the KeyVault to add/remove secrets/keys/certificates. This is why we specify the object id of the account in the access policies initially.&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding a secret
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;twilioSecret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keyvault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;namePrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-twil`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;keyVaultId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;twilioAccountToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's add a secret to the KeyVault. Notice how we specified the KeyVault to which the secret should be added by simply referencing the variable &lt;code&gt;kv&lt;/code&gt; from the previous step. That's how you would normally pass values to anything that depends on a value from another object, right? But why am I calling this out like it's a big deal? Remember that this is no ordinary app. We are dealing with infrastructure resources here. These are not just some variables with values stored in memory.&lt;/p&gt;

&lt;p&gt;These variables represent &lt;em&gt;actual&lt;/em&gt; resources on Azure. This also means that when the KeyVault is still being created, a secret cannot be added to it. So how does Pulumi know when to extract the &lt;code&gt;id&lt;/code&gt; property from it? Well, the answer to that is resource ordering. Pulumi knows that the resource represented by &lt;code&gt;kv&lt;/code&gt; needs to finish creating &lt;em&gt;before&lt;/em&gt; a new &lt;code&gt;Secret&lt;/code&gt; resource is added to it. This is similar to how you would specify &lt;code&gt;dependsOn&lt;/code&gt; in an ARM template to tell ARM how to order your resources and to flow outputs of one resource's creation to another as an input. Instead, in Pulumi this happens automatically as you just go about writing regular TypeScript code.&lt;/p&gt;

&lt;p&gt;Let's add a secret to the KeyVault. Notice how we specified the KeyVault to which the secret should be added by simply referencing the variable &lt;code&gt;kv&lt;/code&gt; from the previous step. That's how you would normally pass values to anything that depends on a value from another object, right? But why am I calling this out like it's a big deal? Remember that this is no ordinary app. We are dealing with infrastructure resources here. These are not just some variables with values stored in memory.&lt;/p&gt;

&lt;p&gt;These variables represent actual resources on Azure. This also means that when the KeyVault is still being created, a secret cannot be added to it. So how does Pulumi know when to extract the &lt;code&gt;id&lt;/code&gt; property from it? Well, the answer to that is resource ordering. Pulumi knows that the resource represented by &lt;code&gt;kv&lt;/code&gt; needs to finish creating &lt;em&gt;before&lt;/em&gt; a new &lt;code&gt;Secret&lt;/code&gt; resource is added to it. This is similar to how you would specify &lt;code&gt;dependsOn&lt;/code&gt; in an ARM template to tell ARM how to order your resources and to flow outputs of one resource's creation to another as an input. Instead, in Pulumi this happens automatically as you just go about writing regular TypeScript code.&lt;/p&gt;

&lt;h4&gt;
  
  
  String interpolation with infrastructure resource outputs
&lt;/h4&gt;

&lt;p&gt;String interpolation in modern JavaScript (and TypeScript) is achieved by enclosing a string using the backtick character and using &lt;code&gt;${}&lt;/code&gt; to insert a variable. But here we are dealing with special resources. The variables used in the string format may not have been created (yet). This is why the Pulumi SDK provides a special interpolation function. In the TypeScript SDK it's &lt;code&gt;pulumi.interpolate&lt;/code&gt;. Use that with the standard JS interpolation syntax and Pulumi will recognize that the interpolation must be considered for resource availability before evaluation.&lt;/p&gt;

&lt;h4&gt;
  
  
  Creating an app insights dashboard is a piece of cake
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appInsights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appinsights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Insights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;namePrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-ai`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;applicationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;web&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resourceGroupName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resourceGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is not much to say here (and that's a good thing here!) The code is pretty self-explanatory.&lt;/p&gt;

&lt;h4&gt;
  
  
  Function App
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;durableFunctionApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appservice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ArchiveFunctionApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;namePrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-funcs`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resourceGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FileArchive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../GarageDoorMonitor/bin/Debug/netcoreapp3.1/publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;appSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;runtime&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dotnet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TwilioAccountToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`@Microsoft.KeyVault(SecretUri=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;twilioSecretUri&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;APPINSIGHTS_INSTRUMENTATIONKEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appInsights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instrumentationKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TimerDelayMinutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timerDelayMinutes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;httpsOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SystemAssigned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Now that the app is created, update the access policies of the keyvault and&lt;/span&gt;
&lt;span class="c1"&gt;// grant the principalId of the function app access to the vault.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;principalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;durableFunctionApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pulumi provides some higher-level helpers for well-known/popular resources such as Azure Functions to ease with the package/deployment. Normally you will have to package your code as a zip and deploy it to the Function App. You can do this using a built-in task extension in Azure DevOps, or you can manually zip up your functions and deploy them directly on Azure. Using Pulumi to do this is very easy. And because Pulumi tracks every resource, it will only trigger an update to the code package if you truly changed your functions code. Otherwise, nothing is changed.&lt;/p&gt;

&lt;h4&gt;
  
  
  KeyVault access
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Grant App Service access to KV secrets&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appAccessPolicy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keyvault&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AccessPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;namePrefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-app-policy`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="na"&gt;keyVaultId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="na"&gt;secretPermissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dependsOn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;durableFunctionApp&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our Function App needs access to the KeyVault to access the secret. So let's create a new access policy and attach it to the KeyVault.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outputs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webhookUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;durableFunctionApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a basic level, think of your Pulumi app as a thing that is creating several things, but there is typically some output that is of interest to you for your application code. For example, the URL for the API service, IP address of a loadbalancer, the hostname of a managed Cosmos DB instance etc. To create an output you simply need to &lt;code&gt;export&lt;/code&gt; it.&lt;/p&gt;

&lt;p&gt;You can retrieve outputs from your stack later if you would like using the Pulumi CLI by running &lt;code&gt;pulumi stack output &amp;lt;outputName&amp;gt;&lt;/code&gt; where &lt;code&gt;&amp;lt;outputName&amp;gt;&lt;/code&gt; is the name of the variable that you &lt;code&gt;export&lt;/code&gt;ed.&lt;/p&gt;

&lt;p&gt;Outputs play an important role in making your infrastructure modular. In this post we only used a single stack. In a more practical scenario, you are perhaps working with multiple teams and each of them may want to have their own stack. But you may have a dependency on the output of one of those stacks, you can use outputs from another stack in your own stack using a &lt;a href="https://www.pulumi.com/docs/intro/concepts/organizing-stacks-projects/#inter-stack-dependencies"&gt;&lt;code&gt;StackReference&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulumi Console And The Managed Backend
&lt;/h2&gt;

&lt;p&gt;Just like the &lt;a href="https://portal.azure.com"&gt;Azure Portal&lt;/a&gt;, Pulumi has a &lt;a href="https://app.pulumi.com?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=praneetloke_home_automation"&gt;Console&lt;/a&gt; UI. The Console gives you a detailed view of all the resources in each stack, their outputs, the timeline of events, activities etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bHbSPNP6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://codejournal.files.wordpress.com/2019/09/screenshot_2019-09-08-praneetloke-dev-resources-pulumi-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bHbSPNP6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://codejournal.files.wordpress.com/2019/09/screenshot_2019-09-08-praneetloke-dev-resources-pulumi-1.png" alt="Pulumi Console 1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hgn50yz5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://codejournal.files.wordpress.com/2019/09/screenshot_2019-09-08-praneetloke-garage-door-monitor-dev-updates-pulumi-1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hgn50yz5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://codejournal.files.wordpress.com/2019/09/screenshot_2019-09-08-praneetloke-garage-door-monitor-dev-updates-pulumi-1.png" alt="Pulumi Console 2"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pulumi tracks the state of your resources. It shows you diffs from the current state as you make changes to your infrastructure. At the beginning of this post I stated I would explain why you should sign-up for an account. To get the state tracking and diffs you don't need an account on Pulumi Console. However, if you want to keep that state safe, highly-available, and make sure concurrent updates are not performed on your infrastructure, which you will need when you are developing for production, you should use the Pulumi-managed backend, and let it take care of all of that for you. The alternative to this is, to manage all that on your own. Learn more about that &lt;a href="https://www.pulumi.com/docs/intro/concepts/state/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Closing Notes
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It is important to note that what you saw above is &lt;strong&gt;not&lt;/strong&gt; a Pulumi-flavor of TS. It is just TS. The same TS you would use to develop Angular apps or whatever else you use TS for these days. This also means everything that the language provides is available for you to use. There are no restrictions, other than what the Pulumi resource provider inflicts on your app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Review the Pulumi &lt;a href="https://www.pulumi.com/docs/intro/concepts/programming-model/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=praneetloke_home_automation"&gt;Programming Model&lt;/a&gt; page for some advanced concepts. Particularly, the &lt;a href="https://www.pulumi.com/docs/intro/concepts/programming-model/?utm_source=dev.to&amp;amp;utm_medium=social&amp;amp;utm_campaign=praneetloke_home_automation#components"&gt;Components&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For JS and TS-based Pulumi apps, the runtime is NodeJS. So each TS file is transpiled to JS and executed inside the Node runtime just like any other Node app. This also means that you can use just about any Node-compatible npm package, including the built-in Node packages as well. In fact, I used the &lt;code&gt;child_process&lt;/code&gt; package to execute the &lt;code&gt;dot net publish&lt;/code&gt; command.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you are planning to use Pulumi on Azure DevOps, then checkout &lt;a href="https://marketplace.visualstudio.com/items?itemName=pulumi.build-and-release-task"&gt;Pulumi's free Task Extension&lt;/a&gt; for build and release definitions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pulumi</category>
      <category>azure</category>
      <category>typescript</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Opinion: The New Full-Stack Dev</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Mon, 08 Jun 2020 01:51:37 +0000</pubDate>
      <link>https://dev.to/praneetloke/opinion-the-new-full-stack-dev-486b</link>
      <guid>https://dev.to/praneetloke/opinion-the-new-full-stack-dev-486b</guid>
      <description>&lt;p&gt;There was a time when calling oneself a full-stack dev was met with confused faces. It was a fairly new idea. No one really understood what it really meant and if it was real. It was sort of like gluten and people claiming gluten allergy.&lt;/p&gt;

&lt;p&gt;Years later, both couldn't be more real. In the early days of being a full-stack dev, it meant you could write services powering the UI (aka the backend), as well as some really cool jQuery selectors (like &lt;code&gt;$("#someDiv .items:nth-child(2)")&lt;/code&gt; -- I'll let you figure out what that means.), some HTML and some CSS. If you knew jQuery you were &lt;em&gt;the&lt;/em&gt; dev to go to at work.&lt;/p&gt;

&lt;p&gt;The full-stack developer movement couldn't have started without javascript templating, backbone.js, and later SASS. Pretty soon after those were introduced a lot more developers claimed to be full-stack developers, which was bolstered by the creation of Twitter's &lt;a href="https://getbootstrap.com/"&gt;Bootstrap&lt;/a&gt;. And suddenly, there was an explosion in the number of developers who could comfortably write both UI, and backend services. The gates to UI development were knocked-down pretty hard. There was probably a hot new frontend framework every week. From &lt;a href="http://backbonejs.org"&gt;BackboneJS&lt;/a&gt; (the OG of bringing Model/View/Collection paradigm to frontend development, in my opinion) to today's &lt;a href="https://reactjs.org"&gt;React&lt;/a&gt;/&lt;a href="https://angular.io"&gt;Angular&lt;/a&gt;/&lt;a href="https://vuejs.org"&gt;Vue&lt;/a&gt; -- today's big 3.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A new framework was announced on JS Weekly!? SWIIITCH!!&lt;br&gt;
-- Web Developer&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From there it was a natural progression for developers to try and tackle writing mobile apps with PhoneGap, which was later renamed Apache Cordova. Arguably one of the most contested ways to write a mobile app. It was nonetheless a way for a web developer to also try their hand at mobile development too. To this day, there are numerous frameworks that promise the "write-once-run-anywhere" type of semantics. &lt;a href="https://dotnet.microsoft.com/apps/xamarin"&gt;Xamarin&lt;/a&gt;, &lt;a href="https://flutter.dev/"&gt;Flutter&lt;/a&gt;, &lt;a href="https://facebook.github.io/react-native/"&gt;React Native&lt;/a&gt;, &lt;a href="https://www.nativescript.org/"&gt;NativeScript&lt;/a&gt;, &lt;a href="https://ionicframework.com/"&gt;Ionic Framework&lt;/a&gt; are all popular frameworks in that space. Regardless of what you and I believe the best way to build a mobile app to be, it was yet another skill a full-stack developer could add to their skillset.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Developments
&lt;/h3&gt;

&lt;p&gt;Back when cloud services were first introduced, almost no one cared how you got to "the cloud", so as long as you were "on the cloud". Scripting languages became uber-popular in the ops area. Knowledge of bash, and PowerShell were (and probably still are) indispensable. Tools like &lt;a href="https://www.chef.io/"&gt;Chef&lt;/a&gt;, &lt;a href="https://puppet.com/"&gt;Puppet&lt;/a&gt; invigorated the infrastructure space with their ideas. There are probably more tools that I have probably never even heard of.&lt;/p&gt;

&lt;p&gt;All this while every major cloud provider was cooking up their own template-based toolset to deploy services to their respective clouds. Surely all of the great minds that thought of the amazing ways to build software for the cloud could have seen the problem a mile away, right? Nope.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AWS has &lt;a href="https://aws.amazon.com/cloudformation/"&gt;CloudFormation&lt;/a&gt; templates, Azure has &lt;a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-authoring-templates"&gt;ARM templates&lt;/a&gt;, and Google has...uh, I don't even know and I don't want to. This is a clear and present problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a clear and present problem. It is what kept the infrastructure space away from the general reach of your average developer who just wants to deploy a simple service to the cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redefining Full-Stack Dev
&lt;/h3&gt;

&lt;p&gt;With HashiCorp's &lt;a href="https://www.terraform.io/"&gt;Terraform&lt;/a&gt;, this changed. Infrastructure "gurus" started to see the need for a system that would let them deploy repeatable, and predictable infrastructure to the cloud. This works really well if this is what you want. But if you don't want to learn a new DSL?&lt;/p&gt;

&lt;p&gt;If you noticed at the beginning of this post, every innovative piece of technology introduced in the developer ecosystem, involved taking something that was accessible to a niche of developers and turning it into a programmable thing. HTML became JS template, CSS was too brittle, so SASS was created which brought some general-purpose programming language paradigms to CSS (when was the last time you wrote CSS? I mean, raw CSS, not SCSS as you know it today.), mobile apps could be built frontend frameworks. At each stage, the adoption of the respective technology was through-the-roof.&lt;/p&gt;

&lt;p&gt;We have been going about trying to solve the cloud infrastructure tooling the wrong way all this time.&lt;/p&gt;

&lt;p&gt;Allowing developers to do what they do best -- write software, will certainly bring infrastructure to the masses. This is why tools like &lt;a href="https://www.pulumi.com"&gt;Pulumi&lt;/a&gt; will disrupt this space. The movement to bring infrastructure to the masses has begun. Pulumi brings the programmability, the "coding" aspect to infrastructure. For the first time, it really feels like I can truly &lt;em&gt;program&lt;/em&gt; the cloud.&lt;/p&gt;

&lt;p&gt;That is not to say the tools that have been created so far have no place in building infrastructure. Nope. Quite the opposite. I think there is a need for these tools. For instance, not everyone is using the cloud the way cloud providers want us to use them. Many of them are tied to legacy on-premise systems, leading to the so-called "Hybrid Environments". Whatever the setup, there is a place for each of those tools.&lt;/p&gt;

&lt;p&gt;And for those of us building something new for the cloud, there's Pulumi. So let me define the new full-stack dev...&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A developer who can work on all tiers of an application, including the infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What do you think?&lt;/p&gt;

&lt;p&gt;(Cover image: Photo by &lt;a href="https://unsplash.com/@mongrel67"&gt;mongrel67&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/ZygjR7kQ89Q"&gt;Unsplash&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>devops</category>
    </item>
    <item>
      <title>Coding Home Automation</title>
      <dc:creator>Praneet Loke</dc:creator>
      <pubDate>Sun, 07 Jun 2020 18:28:04 +0000</pubDate>
      <link>https://dev.to/praneetloke/coding-home-automation-i55</link>
      <guid>https://dev.to/praneetloke/coding-home-automation-i55</guid>
      <description>&lt;p&gt;Home automation is easier than ever with a plethora of IoT-connected devices enabling everyday appliances in your home to be internet-enabled. When you write a custom piece of integration for your IoT, often times deployment to the cloud becomes an afterthought, only to become a nightmare when you actually want to update it later, and you don't remember where/how you deployed it. With Pulumi, you don't have to worry about that anymore. You can develop your IoT integration app, as well as your program your infrastructure as an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Garage Door Opener
&lt;/h2&gt;

&lt;p&gt;Most people have an internet hub connected to their automatic garage door opener, which allows them to remotely monitor the garage door as well as open/close it using a mobile app. But what about when you forget to close it and it stays open? Neither the app nor the existing home automation recipes on the home automation website &lt;a href="https://ifttt.com" rel="noopener noreferrer"&gt;IFTTT&lt;/a&gt; have a way to remind you that you left it open. To solve this problem I tried &lt;em&gt;not&lt;/em&gt; to build something of my own, but instead try to use &lt;a href="https://zapier.com" rel="noopener noreferrer"&gt;Zapier&lt;/a&gt;, a task automation platform.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/cloudy-sky-software" rel="noopener noreferrer"&gt;
        cloudy-sky-software
      &lt;/a&gt; / &lt;a href="https://github.com/cloudy-sky-software/GarageDoorMonitor" rel="noopener noreferrer"&gt;
        GarageDoorMonitor
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Use IFTTT and Azure Durable Functions to monitor your garage door status.
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;
 
&lt;h2&gt;
  
  
  The First Attempt
&lt;/h2&gt;

&lt;p&gt;My first attempt involved using Zapier. It would have worked if there was a way to update a state while waiting for a timer to fire. I used IFTTT to connect the myQ service to fire a Webhook request each time the garage door opened or closed. The webhook receiver was a Zapier webhook "catch" action, which I then connected to a timer delay before sending me a text message via Twilio. It mostly worked, except, if I closed the garage door before the timer fires, there was no way for me to update the state and therefore, cancel sending the text message.&lt;/p&gt;

&lt;p&gt;Here's the "zap" I ended up creating:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fzap.png%3Fw%3D1024" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fzap.png%3Fw%3D1024" alt="Zap!"&gt;&lt;/a&gt;&lt;/p&gt;
A Zap on Zapier using built-in actions.


&lt;h2&gt;
  
  
  Durable Functions on Azure Functions
&lt;/h2&gt;

&lt;p&gt;Durable Functions is an extension to the already popular Azure Functions platform. This means, you can write functions with an external trigger (HTTP, Queue etc.) and have it trigger an orchestration. Each orchestration instance is automatically tracked by the platform. Checkout the &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-http-api#http-api-reference" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; to see what you can control about an orchestration instance.&lt;/p&gt;
&lt;h2&gt;
  
  
  Durable Function Types
&lt;/h2&gt;

&lt;p&gt;There are other durable function types, learn more about them &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-types-features-overview" rel="noopener noreferrer"&gt;here&lt;/a&gt;. The following are just the types used in this project.&lt;/p&gt;
&lt;h3&gt;
  
  
  Orchestration Functions
&lt;/h3&gt;

&lt;p&gt;Each function has a trigger type that identifies &lt;em&gt;how&lt;/em&gt; that function can be triggered. Orchestration functions are no different. Orchestration functions typically don't do any work other than, you guessed it, orchestrate other functions that do the work.&lt;/p&gt;
&lt;h3&gt;
  
  
  Activity Functions
&lt;/h3&gt;

&lt;p&gt;Activity functions are responsible for most of the work in an orchestration. You can make HTTP calls, call other activity functions etc.&lt;/p&gt;
&lt;h3&gt;
  
  
  Entity Functions&lt;/h3&gt;


&lt;p&gt;Entity functions allow you to represent your orchestration instance with a state. It is up to you on whether each orchestration instance has its own entity or if your state is a singleton. This is controlled by the way that entities are persisted. Each entity is made up of two components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An entity name: a name that identifies the type of the entity (for example, "Counter").&lt;/li&gt;
&lt;li&gt;An entity key: a string that uniquely identifies the entity among all other entities of the same name (for example, a GUID).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  IFTTT + Azure Durable Functions + Twilio + Pulumi
&lt;/h2&gt;

&lt;p&gt;This is the high-level view of the solution I finally ended up with.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fhigh-level-arch.png%3Fw%3D673" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fhigh-level-arch.png%3Fw%3D673" alt="Bird's-eye view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IFTTT receives signals from the garage door opener.&lt;/li&gt;
&lt;li&gt;IFTTT then calls the function app.&lt;/li&gt;
&lt;li&gt;The function app waits for couple of minutes and if the door still isn't closed by then, sends a text message using Twilio.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Function App
&lt;/h3&gt;

&lt;p&gt;The only external trigger in the function app is the HTTP trigger used in &lt;a href="https://github.com/praneetloke/GarageDoorMonitor/blob/master/GarageDoorMonitor/main.cs#L119" rel="noopener noreferrer"&gt;this&lt;/a&gt; function. An orchestration instance is created only through the orchestration client, injected into the HTTP-triggered function as a param.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Main"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;RunOrchestrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OrchestrationTrigger&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;IDurableOrchestrationContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"TimerDelayMinutes"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;delayTimeSpan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToInt32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// The following loop will execute for as long as the garage door remains open&lt;/span&gt;
    &lt;span class="c1"&gt;// up to max number of retries.&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentUtcDateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delayTimeSpan&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Setting timer to expire at &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLocalTime&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateTimer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Timer fired!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LockAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EntityId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Entity lock acquired."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CallEntityAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;EntityId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Current state is &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="c1"&gt;// If the door is closed already, then don't do anything.&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"closed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Looks like the door was already closed. Will skip sending text message."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallActivityAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SendTextMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LockingRulesViolationException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Failed to lock/call the entity."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Unexpected exception occurred."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Reached max retry count, but the garage door is still open. Will stop checking now."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Door is still open. Will schedule another timer to check again."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploying the Infrastructure using Pulumi
&lt;/h3&gt;

&lt;p&gt;We will use Pulumi to deploy our function app. The Pulumi app creates the function app along with the Key Vault containing the Twilio account token necessary for the API call to send a text message. For more information, see the README file in the &lt;a href="https://github.com/praneetloke/GarageDoorMonitor/tree/master/infrastructure" rel="noopener noreferrer"&gt;&lt;code&gt;infrastructure&lt;/code&gt;&lt;/a&gt; folder of the source repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fpulumi-app.png%3Fw%3D344" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fpulumi-app.png%3Fw%3D344" alt="Infrastructure"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the Pulumi app is deployed, you can get the URL for your function app, in order to complete the IFTTT Applet creation in the next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  IFTTT Applets
&lt;/h3&gt;

&lt;p&gt;IFTTT allows you to create custom applets, which is basically creating your own recipe of "this" and "that". To create a new applet, click your avatar in the top-right corner on &lt;a href="https://ifttt.com" rel="noopener noreferrer"&gt;https://ifttt.com&lt;/a&gt;, and click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fifttt-create-applet-1.png%3Fw%3D329" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fifttt-create-applet-1.png%3Fw%3D329" alt="IFTTT Applets"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on &lt;strong&gt;+ This&lt;/strong&gt; and choose the service called &lt;strong&gt;myQ&lt;/strong&gt;. Most of the garage door openers here in the USA are made by the &lt;a href="https://chamberlaingroup.com/" rel="noopener noreferrer"&gt;Chamberlain Group&lt;/a&gt; anyway and you are most likely using one of those. All of those openers work with the &lt;a href="https://www.myq.com/" rel="noopener noreferrer"&gt;myQ&lt;/a&gt; internet gateway. Your alternative would be to buy a myQ Smart Hub.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fifttt-myq.png%3Fw%3D1024" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fifttt-myq.png%3Fw%3D1024" alt="Applet"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;+ That&lt;/strong&gt; and search for &lt;strong&gt;Webhook&lt;/strong&gt; to select it. You will need the URL of the Function App that was deployed using Pulumi. You can get this URL by navigating to &lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;https://portal.azure.com&lt;/a&gt;, too. Since the infrastructure was deployed using Pulumi, we can easily fetch its output by running &lt;code&gt;pulumi stack output webhookUrl&lt;/code&gt; in the &lt;code&gt;infrastructure&lt;/code&gt; folder. We can now complete the Webhook action's configuration in IFTTT.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Since the function app is exposed to the internet, we don't want just about anyone to be able to call it. Instead, we will use the built-in function app &lt;a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook#authorization-keys" rel="noopener noreferrer"&gt;authorization keys&lt;/a&gt; to allow only IFTTT to invoke it. Any other caller without the function key will receive a &lt;strong&gt;401 Unauthorized&lt;/strong&gt; error due to &lt;a href="https://github.com/praneetloke/GarageDoorMonitor/blob/master/GarageDoorMonitor/main.cs#L120" rel="noopener noreferrer"&gt;this auth requirement&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fifttt-webhook.png%3Fw%3D833" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcodejournal.files.wordpress.com%2F2019%2F09%2Fifttt-webhook.png%3Fw%3D833" alt="Complete Applet"&gt;&lt;/a&gt;&lt;/p&gt;
Completing the IFTTT applet creation for webhook action.



&lt;h3&gt;
  
  
  Twilio
&lt;/h3&gt;

&lt;p&gt;In order to send a text message, create an account on &lt;a href="https://www.twilio.com" rel="noopener noreferrer"&gt;Twilio&lt;/a&gt; and purchase a &lt;a href="https://www.twilio.com/console/sms/dashboard" rel="noopener noreferrer"&gt;Programmable SMS&lt;/a&gt; number. Your account SID and token can be found on the dashboard page of the &lt;a href="https://www.twilio.com/console" rel="noopener noreferrer"&gt;Twilio Console&lt;/a&gt; or on the &lt;a href="https://www.twilio.com/console/project/settings" rel="noopener noreferrer"&gt;Settings&lt;/a&gt; page under the &lt;strong&gt;API Credentials&lt;/strong&gt; section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Notes
&lt;/h2&gt;

&lt;p&gt;A few things important things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://docs.microsoft.com/en-us/azure/key-vault/general/overview" rel="noopener noreferrer"&gt;KeyVault&lt;/a&gt; in the infrastructure is not necessary for a project like this, but it is very easy to create one with Pulumi. And with Azure's new Managed Identity, it is even easier to configure application access to secrets.&lt;/li&gt;
&lt;li&gt;To learn more about security best practices on Azure, read &lt;a href="https://www.pulumi.com/blog/7-ways-to-deal-with-application-secrets-in-azure/" rel="noopener noreferrer"&gt;this&lt;/a&gt; excellent post by &lt;a href="https://mikhail.io/" rel="noopener noreferrer"&gt;Mikhail Shilkov&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next post, we will take a closer look at the Pulumi app used to deploy the Azure Function App.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>azure</category>
      <category>twilio</category>
      <category>serverless</category>
    </item>
  </channel>
</rss>
