<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Facundo Gauna's Blog]]></title><description><![CDATA[Facundo's Blog on Kubernetes, DevOps, Azure, productivity, and career.]]></description><link>https://gaunacode.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1663073027255/IL_Z-7prV.png</url><title>Facundo Gauna&apos;s Blog</title><link>https://gaunacode.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 06 Sep 2026 08:09:37 GMT</lastBuildDate><atom:link href="https://gaunacode.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Here are my "lessons learned" after 5+ years of DevOps consulting]]></title><description><![CDATA[Have you wondered what it's like to be a consultant before? Are you running into challenges in growing a DevOps culture at your job? I will share it all, and it will be a long blog post. To be honest, I don't know why I'm doing this; I just feel like...]]></description><link>https://gaunacode.com/here-are-my-lessons-learned-after-5-years-of-devops-consulting</link><guid isPermaLink="true">https://gaunacode.com/here-are-my-lessons-learned-after-5-years-of-devops-consulting</guid><category><![CDATA[Devops]]></category><category><![CDATA[Career]]></category><category><![CDATA[consulting]]></category><category><![CDATA[enterprise]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Wed, 09 Nov 2022 13:56:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/g1TWbj5XYb4/upload/v1667854882456/ykBdpj0svN.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you wondered what it's like to be a consultant before? Are you running into challenges in growing a DevOps culture at your job? I will share it all, and it will be a <em>long</em> blog post. To be honest, I don't know why I'm doing this; I just feel like it needs to be done. </p>
<p>I have been a consultant for boutique, medium-sized, and large consultancies. Before consulting, I was a developer. All of my consulting experience has been in helping companies adopt DevOps practices. Because I am a consultant, I get exposure to many more companies than I would if I were just an employee at them. So, I hope that if you don't know much about consulting, you get a view into my world, and if you're an existing consultant, then maybe I can help you with a challenge you've had.</p>
<h2 id="heading-before-we-get-started">Before we get started</h2>
<p>I was inspired at my day job because lots of our recruits don't have previous consulting experience, so I've been reflecting on what it means to be a "consultant." I thought that others could benefit from the lessons I have learned over the years. I am also doing this for myself because it's helping me reflect.</p>
<p><mark>These opinions are my own</mark>, dear employer, don't fire me. 🙃</p>
<p>For context, I live and work remotely for clients in the United States. I know there's an international audience through this medium.</p>
<h3 id="heading-what-this-blog-post-is-not-about">What this blog post is not about</h3>
<p>In my view, some "consulting" companies are staff augmentation companies. In other words, the "client" is a company that needs people with certain skills. The "client" can't find people because they don't have a strong brand or recruiting capabilities, their payscales don't allow for competitive compensation, or the company likes to spool up or down the workforce with contractors, etc. When a consulting company does a "staff augmentation" project, the "client" does not expect strong <strong>advisory services</strong>. </p>
<p>I believe a "consultant" is an <mark>advisor and change agent</mark> looking to help the "client" achieve their goals. So, in this post, I will share my opinions about projects where I was strictly in that role - an advisor. </p>
<p>Lastly, I consider "DevOps" to include people, process, and technology aspects. So, <mark>this will not be a blog post around tips and tricks for creating pipelines or cranking out containers.</mark> <strong>Spoiler alert</strong>: Technology is the easy part. 😱</p>
<h3 id="heading-consultant-glossary">Consultant Glossary</h3>
<ul>
<li>Statement of Work (SOW) - The contract for a project between a consulting company and the client organization.</li>
<li>Project Plan - A plan on how to deliver the project. Sometimes very <a target="_blank" href="https://en.wikipedia.org/wiki/Gantt_chart">Ghantt chart</a> esque.</li>
<li>Client Organization - The company that hires a consulting company.</li>
<li>Client - The main point of contact or stakeholder that works for the client organization.</li>
</ul>
<h2 id="heading-1-do-the-hardest-thing-first">1. Do the hardest thing first</h2>
<p>Often, as engineers, we tend to want to put our heads down and crank out code or fix <em>technical</em> issues. But we're human beings, and as humans, we are highly social creatures. There is no "avoiding people." And people also create processes to get stuff done. </p>
<p>So, do the hard things first. You're probably dreading the hard things, and they will most likely be problems related to "people" or "processes." </p>
<p>So, for example, here's a list of things I watch out for.</p>
<ol>
<li><strong>Go to production</strong> - Deploy to production as early as possible. Just because you "deploy" to production doesn't mean you have to "release" it to your users. More on this later.</li>
<li><strong>Have that security review with Central IT</strong> - If you're part of a big traditional enterprise, you are likely required to have a "security review" with enterprise architects and security architects. Do it as early as possible, be ready for it, and sometimes you'll be surprised to find that they're more straightforward than you made them out to be in your head. However, you want to avoid the scenario where you wait until the big production deployment towards the end of the project to have that security review. Then you receive feedback that will fundamentally change your architecture. </li>
<li><strong>Have that review with your enterprise architects</strong> - Like the previous point, propose a review at the beginning of the project in addition to the final one. The earlier, the better, so that you avoid surprises.</li>
<li><strong>Get the "process" stuff out of the way</strong> - Have to integrate with systems you find dumb, but they're crucial to your project? Do it soon. Don't run a project on assumptions that things will work. Validate those assumptions.</li>
<li><strong>Start that documentation early</strong> - Often, I have to provide long recommendation documents. So, I "write drunk" and "edit sober." Meaning writing the first draft without editing. If you try to write and edit, you will get stuck in writer's block or perfectionism.</li>
</ol>
<p>Here's a strategy I borrowed from a client for greenfield apps - perform a <strong>walking skeleton</strong> production deployment early in the project. A walking skeleton is just a <em>basic</em> set of the infrastructure deployed to production with very little actual code. Also, a walking skeleton uses Infrastructure as Code and a CI/CD pipeline. The sheer nature of being in "production" will trigger all those processes you dread, and you can get them out of the way early. You will avoid nasty surprises and delays toward the end of the project. You will also gain a lot of practice and confidence deploying to production with the automation you established in the first place.</p>
<h2 id="heading-2-big-bang-deployment-is-a-bad-word">2. "Big bang deployment" is a bad word</h2>
<p>Remember the "big bang" theory? There was a <strong>HUGE</strong> explosion, and then there was the universe. Deployments to production can be that way, too - a <em>big bang</em> deployment of infrastructure and code, chaos, and hopefully a working application.</p>
<p>You <em>might</em> be surprised, but so <strong>many</strong> companies today still wait until the end of the project to release to production. They hope and pray that a <em>big bang</em> will work. Sometimes, they ignorantly think nothing wrong will happen because of their skill and experience. </p>
<p>A big bang deployment is an explosion of Work in Progress (WIP). WIP is unfinished work. The work is unfinished because it hasn't been shipped to production, and you're not learning from it yet. The more WIP you have in the system at one time, the more risk. </p>
<p><mark>Can you make a big bang less painful?</mark> Yes. I have had to suck it up and do big-bang deployments before. Here are some tactics that have helped me: </p>
<ul>
<li><strong>Infrastructure as Code at the beginning of the project</strong> - Less environment drift between non-prod, the better. It's going to alleviate surprises on the production side. But it won't stop them completely. If you start at the beginning of the project, you'll get a lot more repetitions with the process and have the training needed for the big day.</li>
<li><strong>CI/CD pipeline at the beginning of the project</strong> - You become reliant on a pipeline to perform the deployments of the code. Therefore, you're less likely to have surprises on the big day. </li>
<li><strong>Create a deployment plan and review it thoroughly</strong> - Create a simple document and list step-by-step all the things the team will do to perform the deployment. If you can, deploy a <em>whitespace</em> deployment in a non-production environment to validate the document. A whitespace deployment is just a deployment from scratch, so re-create the infrastructure and redeploy the code.</li>
<li><strong>Break it up into chunks</strong> - Production "release" is not the same as a production "deployment." A release is when you make it available to your customers. So, if you deploy to production, <strong>you don't have to make it available to your customers yet</strong>. For example, you could deploy the cloud infrastructure, go to sleep, deploy the code, go to sleep, run some automated functional tests to validate, go to sleep, then perform database migration and do the DNS cutover.</li>
</ul>
<p>Ideally, if I had a magic wand and I could get rid of Big Bang deployments, I would:</p>
<ol>
<li>Create the infrastructure via code at the beginning of the project.</li>
<li>Set up CI/CD pipelines for my infrastructure and the application code at the beginning of the project. Then deploy to production frequently throughout the development of the app. Feature flags can also be very helpful here.</li>
<li>Perform whitespace deployments occasionally throughout the project.</li>
</ol>
<p>The last thing on this topic, you might think that big-bang deployments only affect companies that are not using Agile. In my experience, just because a company is "using" Agile or Scrum or SAFe doesn't automatically mean they are free from big-bang deployments. I've worked with <strong>zero</strong> companies that identify as using Waterfall, and I have run into many, many, many big-bang deployments.</p>
<h2 id="heading-3-listen-to-the-client-and-bring-them-into-the-conversation-co-create-with-them-but-theres-a-catch">3. Listen to the client and <strong>bring them into the conversation</strong>. <strong>Co-create with them</strong>. But there's a catch.</h2>
<p>When designing a system, I like to bring the client into the design conversations. Maybe have "whiteboarding" sessions or just a series of meetings where we talk. At some point, I begin to form an opinion and have an idea of what I would like to recommend. So, I want to fail fast and validate my assumptions with the client. I also recognize that I might not have all the data points, so if possible, I create a simple draft document or PowerPoint stating <em>options</em> on a <em>part of the system</em> that could be designed. I present the options to a core client team with the goal of <em>learning more</em>. Often, they provide minor feedback or significant feedback. Sometimes the options get smashed together into a new option. Sometimes, we need to create an experiment to validate one of the options to address any concerns. The key is that the client is involved and therefore feels in control of the project's outcome.</p>
<p>In my observations, <strong>traditional consulting companies</strong> have different project delivery methods. They create a detailed project plan and weekly email reports and assign a project manager to the project. The plan has a clear "design/document/build" phase. Depending on the size of the project, there could be several months before anything is built!</p>
<p>What happens if there are gaps in the design document? Will the implementation go according to plan? Does the project need to halt because the design needs to be flushed out? I'm not sure, but it does sound risky, right? </p>
<p>If the project does get delayed, the client would not feel good about it. Even worse, if the design phase is dragged out until it's "right," it could mean <em>months</em> without actually building something or driving outcomes for the client. In my opinion, the client typically doesn't <em>care</em> about a recommendations document - it just doesn't drive the outcome for them at all. The document is a method for communicating design decisions and rationale.  </p>
<p>Overall, I think one huge problem with the traditional consulting approach: <em><strong>the client was not involved in the design process.</strong></em></p>
<p>How would you feel if you attended 10+ meetings talking about system design, and when you asked a question, the consultant said that the final answer would be in a recommendations document? Then, several weeks or months later, you receive over a hundred pages of the document. So, either:</p>
<ul>
<li>A. <strong>You have no time to read it</strong> - You trust that there are appropriate recommendations based on the points you have made. Maybe you skim the document and can't find the things you're looking for, so you make a point to come back to it but never do.  </li>
<li>B. <strong>You make the time to read it, and you're disappointed</strong> - The document doesn't have enough detail and rationale behind the decisions. Or maybe, there are contradictions between recommendations. Or, more frustratingly, the questions and points you have made during the discussions were never addressed. After you read more and more, you start to question, "this document is generic; it just states obvious things that we already knew." </li>
</ul>
<p>Alright, so what's the catch to co-creating and designing with your client?</p>
<p>You can't hide.</p>
<p>If you're designing with them in the open, you will get to a point where you have to say, "I don't know." Or "in my experience, that hasn't worked well," or "I haven't tried this solution yet."</p>
<p>So ultimately, the client will know how much you know and how much you don't know. It will <em>come out</em> if you don't know enough about the subject you're talking about.</p>
<p>I don't recommend this approach to someone that doesn't have a lot of experience in the topic. </p>
<h2 id="heading-4-know-when-to-step-back-know-when-to-step-in">4. Know when to <strong>step back</strong>. Know when to <strong>step in</strong>.</h2>
<p>Active listening is crucial. Stating opinions is important too. So, like any normal conversation, I think talking to a client should be an ebb and flow of listening and stating opinions. This "ebb and flow" is more 
important the more people there are in the "room." I believe that the "ebb and flow" quality often translates to the <em>customer experience</em>. The reason is that sometimes clients bring in consultancies because they cannot work well with a different team; therefore, if you manage to have two or more teams talking and working together, that's a tremendous value.</p>
<p>Here are two extremes that I am not fond of:</p>
<ul>
<li><strong>Too much talking</strong> - Talking so much to "control" the conversation and "impress" the client. Often, it just comes across like someone likes to hear themselves talk. Besides, if you do all the talking, there's very little listening, and if you want to surely fail a project, that's to not listen at all.</li>
<li><strong>Listening and punting</strong> -  Too much of one thing is bad, and the same is valid for listening. Punting is when opinions are reserved until a later point and not "showing your cards." Being very introverted, I struggle with this.</li>
</ul>
<h2 id="heading-5-the-client-doesnt-give-a-shit-about-the-sow-the-client-wants-results">5. The client doesn't give a shit about the SOW. <strong>The client wants results</strong>.</h2>
<p>In my experience, it's normal for clients not to read the contract. Maybe, the stakeholder read it but didn't understand the technical fine points. Or maybe the stakeholder is not part of the day-day delivery of the project and did not share with the main people you're working with. Sometimes, I believe that they expect excellent customer service, and they trust that the consulting company understands their needs, so they never get around to reading it. Whatever the reason is, the SOW doesn't matter to them. What matters to them is that they can reach the outcome that they're looking for. It is very possible that you do everything in the SOW within the timeline, and the client is unsatisfied.</p>
<p>If the SOW does not state <em>clearly</em> the goals or outcomes of the engagement, ask the client. Sometimes the client will be a little annoyed since they would have had these conversations already during the pre-sales process. Still, it's better that they are annoyed that the project starts with complete misalignment from the beginning. </p>
<p>I'll share with you some high-level outcomes of my past projects so that you get a sense of how high-level we're talking.</p>
<ul>
<li>"We acquired a company with an extensive software system, and their system is not up to our security standards. We want to redo everything using DevOps practices and Infrastructure as Code to replicate these practices for other systems in the future and cut the lead time to onboard new applications to the cloud."</li>
<li>"We want to modernize our monolithic application because we had a severe outage that lost us millions of dollars of revenue. If it happens again, it could be detrimental to the business. We feel this technical debt on the monolith is catching up to us; it's been growing for a decade."</li>
<li>"We want to find a new branching strategy because our teams struggle to deliver value consistently during their sprints. Every sprint, we also have crazy four-hour deployment meetings at 3 AM because usually, we have challenges deploying to production without an issue. We have other pains like code freezes against our QA environment, large deployment checklists, and long hours before the end of the sprint to try to get work done."</li>
<li>"We want to standardize our cloud infrastructure to cut down little decisions when we onboard applications to the cloud platform. By standardizing, we will be able to cut lead times to deploy new applications in the cloud and keep up with the demand of the enterprise so that we don't have to grow the cloud team."</li>
</ul>
<p>Notice that I did not say, "we want ten Kubernetes clusters, twenty pipelines, and lots of shit doing fancy things."</p>
<h2 id="heading-6-dont-hide-behind-the-project-manager-the-salesperson-or-the-engineering-director-be-vulnerable">6. <strong>Don’t hide</strong> behind the project manager, the salesperson, or the engineering director. Be vulnerable.</h2>
<p>In my career, I have witnessed engineers that prefer to focus on the "technical" things while they lean on other teammates to do the "people" stuff. The same goes for consulting, and I have witnessed consultants that want to rely on the project manager to talk to the client. Heck, I have done this too. But, in my experience, this is what ends up happening:</p>
<ol>
<li>Project manager asks what the team has done and delivered.</li>
<li>Engineers speak a bunch of technical <em>gobbly doo</em>. </li>
<li>Project manager has a project update with the stakeholders and presents all the technical <em>gobbly doo</em>, but because they don't understand it, they don't explain it well. </li>
<li>Client asks questions, and the project manager says, "I don't know," and asks the engineer during the meeting. </li>
</ol>
<p>I believe it's much more effective for consultants to learn the soft skills necessary to say the hard things and communicate risks to the client. I'll give you an example of hard things I have said before:</p>
<ul>
<li>"I am afraid that if we don't stand up this Kubernetes cluster soon, we won't have enough time to perform a load test with the application on it and, therefore, not be ready for the traffic spike in the high season."</li>
<li>"I am concerned that if we don't migrate this database to the cloud, then there's a risk that latency will be too large for the API. So, let's migrate it, but that will require us to complete that security review now."</li>
<li>"I know this project is really important to you. I hear what you're saying. Unfortunately, we won't be able to start XYZ until the week of A because we have to finish B and an upcoming vacation."</li>
</ul>
<p>I won't lie to you - it takes time and practice. If you're early on in your career, start small. You'll be much more comfortable asking questions when you're a senior team member practicing this skill for years.</p>
<h2 id="heading-7-working-under-safe-is-really-really-hard">7. Working under SAFe is really <em>really</em> hard</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667855603430/7lrLjSLRn.png" alt="image.png" /></p>
<p>I'll be honest with you. I haven't taken the time to learn or understand the principles of <a target="_blank" href="https://en.wikipedia.org/wiki/Scaled_agile_framework">SAFe</a>. SAFe is short for Scaled Agile Framework, which was put together to help dictate how to do agile in a large enterprise. I can't speak to the details of exactly <em>why</em> it doesn't fit well with DevOps. But I can tell you my experience. </p>
<p>When I hear that a client is using SAFe, I bear for a <em>long</em> and <em>slow</em> burn-out-inducing project. It could be that 99% of my clients that have implemented SAFe have implemented it incorrectly, so it could be that it's not SAFe's fault. Based on opinions from others more well-versed in this topic, I don't think that's the case.</p>
<p>When I engage with a client organization that's typically already using SAFe, we have to use that delivery model to deliver a project. So, here are my observations about trying to deliver a project under SAFe:</p>
<ul>
<li><strong>Work to do work</strong> - So much work just to do <em>work</em>. Not only are there the typical Scrum ceremonies, but there's also lots of other planning to ensure that other's team's planning is planned effectively. In the end, it's a <em>lot</em> of planning work to prepare to do the work.</li>
<li><strong>Super large meetings</strong> - To ensure that multiple teams plan effectively, they get shoved into a massive meeting. It could be a meeting with maybe ten or so teams or hundreds of teams. This large meeting is typically called "Program Increment" or "PI" for short. I have had clients with a PI planning event that's six hundred people large. Have you ever attended a six-hundred-person meeting? I have. Shoutout to Microsoft Teams for handling that massive scale.</li>
<li><strong>It's just disguised Scrumfall</strong> - Scrumfall is when a team uses Scrum in Waterfall fashion. If you're "iterating" for nine months before you deploy to production, it's probably Waterfall, not Agile. With SAFe, there's so much emphasis on planning and the illusion of iterations that it's hard to deploy or deliver anything to production for the first time under this model. </li>
</ul>
<p>Did you know that big tech companies don't use SAFe? Even just Scrum, for that matter?</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/GergelyOrosz/status/1442489057381134345?s=20&amp;t=PaZEObPk_p8DQ2jMsmrTuA">https://twitter.com/GergelyOrosz/status/1442489057381134345?s=20&amp;t=PaZEObPk_p8DQ2jMsmrTuA</a></div>
<p>Here is the <a target="_blank" href="https://blog.pragmaticengineer.com/project-management-at-big-tech/">fantastic article</a> mentioned in the tweet.</p>
<p>So, do you have to do what big tech companies do? Nope. </p>
<p>But this data point probably means that you don't <em>need</em> SAFe to be able to be successful, right?</p>
<p>Before trying to undo SAFe at your company, read my other points.</p>
<p>So, it's likely that you'll have to learn how to make the best out of it. Here's what worked well for me:</p>
<ul>
<li><strong>Build excitement before a big planning increment</strong> - In SAFe, you have huge planning events once per quarter. If you can't convince another team to add some DevOps work during a program increment, then it's likely not going to happen. Therefore, before the big planning event, raise awareness before the big planning starts so that they'll add the work you're hoping to see onto their program increment. Do a brown bag, a demo, etc.</li>
<li><strong>Re-adjust your pace to match the client's pace</strong> - There is only so fast you can move under this world. I have had to learn multiple times to let go and be ok with moving much slower than I would like.</li>
</ul>
<p><strong>Edit:</strong> Lastly, here's a <a target="_blank" href="https://docs.google.com/document/d/1EdkoKpURZREBOmArg4aopWTzOhvEPfCgTD-aLNMSTgg/edit#heading=h.51xpgdtmmgfr">great guide</a> from Agile thought leaders.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1668011021601/agkwSQqs9.png" alt="2022-11-09_08-23-09.png" /></p>
<h2 id="heading-8-sunk-cost-fallacy-is-a-strong-force-add-ego-and-you-are-screwed">8. <strong>Sunk cost fallacy is a strong force</strong>. Add <strong>ego</strong>, and you are <strong>screwed</strong>.</h2>
<p>The <a target="_blank" href="https://en.wikipedia.org/wiki/Sunk_cost">sunk cost fallacy</a> is when someone decides to keep going in a specific direction because there's been too much time, money, and effort put into the project. I can't tell you how often a client has chosen to continue doing something a certain way just because they've spent too much time on the implementation.</p>
<p>The ego is a self-protection mechanism that we all humans have. The ego jumps out to help our self-esteem when we feel threatened. Maybe, we feel incompetent if XYZ were true, or we feel like we lost control if XYZ were true, or our bosses won't like us if XYZ were true. When someone has an extraordinary amount of ego, especially when they choose to listen to information based on the status of the person giving the information, it's going to be hard. Add some sunk-cost fallacy, and it becomes a fast-moving training heading for a cliff.</p>
<p>Here are some examples: </p>
<ul>
<li>"I went to an <a target="_blank" href="https://en.wikipedia.org/wiki/Ivy_League">ivy-league school</a>, and I created this fantastic library. We're going to keep using this library."</li>
<li>"Microsoft uses Azure Service Fabric, so we'll keep using Azure Service Fabric."</li>
<li>"Jimmy worked at Amazing Tech Company, so he's probably right on this. We're going to listen to Jimmy."</li>
</ul>
<p>As a consultant, you can't control the client's decision. You have <em>zero</em> control. You only have the power of influence. If you run into someone with an extraordinary "ego," then the best thing you can try is to speak to his/her ego and provide information tied to status. In the end, if <strong>they don't value or respect your authority on the subject, they most likely won't listen.</strong> It might as well be an immovable object.</p>
<p>So, if I encounter a lot of ego <em>and</em> they're protecting something that cost them a lot of time and effort to implement, I pick a different problem to tackle. Usually, there are a lot of problems to solve, so this is not an issue. But if the problem they're protecting is crucial to the outcome for the client, then I'd have to lay it all out on the table and say the hard thing. In practice, I would probably escalate the issue and add other key personnel to the conversation to increase the stakes and the pressure.</p>
<h2 id="heading-9-pick-a-hill-to-die-on-dont-fight-all-the-battles">9. Pick <strong>a hill to die on</strong>. Don’t fight all the battles.</h2>
<p>If you picture a medieval times battlefield where armies fight close combat with swords, then the army on top of a hill usually has the advantage, and they try to defend the hill. The opposing army then fights uphill to defeat the enemy, but it will be more challenging. If you don't like the combative analogy, think of yourself as a love warrior trying to spread love 🙃. You will still only have enough energy to climb some of the hills.</p>
<p>When I start a project, I look for the "hills." The hills are challenging topics that will require a tremendous amount of <em>emotional</em> energy. Because of the energy, these initiatives will take, I pick one or two. Based on experience, I rule out some hills automatically because I know it probably won't help the project move forward.</p>
<p>Here are some of those "hills" that I always avoid:</p>
<ul>
<li>Trying to convince the client to use something other than JIRA.</li>
<li>Trying to convince my client not to use SAFe for my project.</li>
</ul>
<h2 id="heading-10-there-needs-to-be-trust-between-you-and-the-client-if-theres-no-trust-they-will-not-listen-in-the-beginning-they-probably-will-not-trust-you-get-some-quick-wins-to-show-value-and-you-will-earn-trust">10. <strong>There needs to be trust between you and the client</strong>. If there’s no trust, they will not listen. In the beginning, <strong>they probably will not trust you</strong>. Get <strong>some quick wins to show value</strong> and you will earn trust.</h2>
<p>When I make recommendations, I imagine having a "trust budget." Each big recommendation and big ask that will mean a lot of work for my client will consume from the "trust budget." Before I make these recommendations, I visualize how much of my trust budget I will consume. Often, I have a <em>zero</em> trust budget at the beginning of a project with a new client because I haven't delivered results yet.</p>
<p>So, if trust is earned via results, I quickly try to increase my trust budget so that I can make recommendations. Therefore, I am looking for some quick wins.</p>
<p>Quick wins are painful problems that are relatively easy to solve. When clients hear a low-effort solution to a nagging problem, they feel <em>relief</em> and <em>excitement.</em> And, if you're able to solve the silly problem along the way, <em>then</em> tell them afterward... they might make a statue of you outside their building.</p>
<p>Here are some examples of quick wins I have delivered:</p>
<ul>
<li>Document the onboarding steps you're going through and deliver them as an onboarding guide or checklist for future engineers. </li>
<li>Add to their READMEs as you're learning about the systems.</li>
<li>Tweaking ugly deployment scripts to make them cleaner.</li>
<li>Provide <strong>easy</strong> recommendations on slashing cloud costs.</li>
<li>Provide a demo of a potential solution to a nagging problem, ideally within the project's first week.</li>
<li>Find a security vulnerability and raise awareness about it. <em>Caveat</em>: this only works if the company values security and doesn't see security as more work to do. </li>
</ul>
<p>What if you can't find a quick win?</p>
<p>Then listen closely and ensure you'll solve the main problem <strong>extremely well</strong>. <mark>The key point is that when you deliver your recommendations, you want them to be heard.</mark> If you sense skepticism when talking to the client, that's probably an indication of a low "trust budget." </p>
<p>But skepticism is different than curiosity. Skepticism is "Do I trust this consultant that they can solve this problem? Have they done it before?". Curiosity is, "Okay, if this solution works, it would be amazing. How do I make this happen? What could go wrong?"</p>
<h2 id="heading-11-theres-no-good-and-bad-just-effective-or-ineffective">11. There’s <strong>no good</strong> and <strong>bad</strong>. Just effective or ineffective.</h2>
<p>Don't be dogmatic. Don't hide behind "best practices." Explain <em>why</em> something is better or more effective.</p>
<p>I <strong>don't like</strong> "best practices." It implies that everyone should make this master list of things, and if you're not, then it's bad, and you're failing. They can also be a crutch to consultants. </p>
<p>I'm much more interested in the essence of a "best practice." Why is this a good idea? Why should your client do this? How much effort will they need to place to get the benefit?</p>
<p>For example, "it is usually best practice to usually keep secrets out of source control <strong>because</strong> if you decide to give <code>read</code> access to a repository to a contractor, then they can see those secrets and use them to gain access to an environment that they shouldn't have access to."</p>
<p>So, when you understand why something is considered "best practice," you can discuss risks vs. benefits with your client. If you're having trouble explaining why something is considered best practice, you probably don't understand it well enough <em>yet</em>.</p>
<h2 id="heading-12-when-working-with-your-team-call-out-ownership-areas">12. When working with your team, call out <strong>ownership areas</strong>.</h2>
<p>Some of the best projects I have delivered can be attributed to this single decision. Think of your team like a team of Avengers. Every person brings something different to the team, and each person has strengths and weaknesses. So, play to everyone's strengths. </p>
<p>For example:</p>
<ul>
<li>Jimmy used to work as an ops guy, so he should probably own the monitoring recommendations.</li>
<li>Even though Anya is early in her career, she has a lot of drive. She can work on creating some demos that will take just as much time for someone senior to complete.</li>
<li>Carlos used to be a developer, so he should probably own the Infrastructure as Code (IaC) recommendations.</li>
<li>Sofia has the most experience, so she should probably design the Kubernetes cluster infrastructure since that's the most crucial part of this project.</li>
</ul>
<p>When everyone agrees on what areas they will own upfront, it is much easier for the team to self-organize and react together. In contrast, if a team optimizes for "busyness" or time, then Jimmy, who doesn't have as much coding experience, might miss some crucial recommendations.</p>
<h2 id="heading-13-sometimes-the-customer-experience-matters-more-than-the-quality-of-the-work-sometimes-its-more-about-how-the-client-felt-than-how-amazing-the-delivery-was">13. Sometimes, the <strong>customer experience</strong> matters more than the quality of the work. Sometimes, it’s more about how the client <em>felt</em> than how amazing the delivery was.</h2>
<p>Although I cannot find the source, I heard of a study that found that average people thought the "best" doctors were the ones that had better bedside manners, not the more skilled ones. If this is true, it could also apply to many other aspects of life. </p>
<p>Receiving medical care can be a very vulnerable moment. Engaging with a consultant can also be a vulnerable moment since companies don't hire consultants when things are going well. Sometimes, the project that a client is sponsoring could mean a promotion or maybe the last ditch effort before they leave the company.</p>
<p>So, what is the client experiencing?</p>
<p>Do they feel listened to?</p>
<p>Do they feel understood?</p>
<p>Do they feel like they can trust their judgment?</p>
<p>I learned this lesson through a unique project. My consulting company has three teams working for a client organization. My team was the highest performer, and the client liked it best, but it wasn't because of me. It was because of my teammate. </p>
<p>At the time, I believed that delivering amazing client results was doing lots of great <em>technical</em> work. I put my head down, and I worked furiously to drive results. On the other hand, my teammate was not as ambitious; however, he did stop and strike up conversations with my client. He found out they both had military experience and loved Asian cuisine. Later, we ended up going to dinner with our client at a hole-in-the-wall yet amazing Korean BBQ place, and it was amazing.</p>
<p>I remember thinking, "Wow, what am I? Chopped liver? My client loves my team because of my teammate. I don't think it has anything to do with me."</p>
<p>My teammate just genuinely cared about the person in front of him. My client felt listened to and probably felt like they could talk openly when we had to make tough decisions at work. With time, after more dinners and lunches, we got to hear more stories and context from our clients. The relationship was growing into a friendship, and he likely felt very understood.</p>
<p>Wouldn't it be better to go through a vulnerable moment with someone you can trust?</p>
<p>Building a relationship with a client is very important.</p>
<h2 id="heading-14-practice-how-you-ask-questions-the-tone-is-so-important-avoid-sounding-combative-youre-trying-to-reduce-the-chances-of-people-getting-defensive">14. <strong>Practice how you ask questions</strong>. The tone is so important. Avoid sounding combative. You’re trying to reduce the chances of people getting defensive.</h2>
<p>You can't control how people feel. You <strong>can</strong> control how you <em>say</em> things in a way that minimizes the chances of someone being triggered and raising their defenses. </p>
<p>For example, </p>
<blockquote>
<p>Consultant: Why are you doing it that way? That's not best-practice.</p>
</blockquote>
<p>vs. </p>
<blockquote>
<p>Consultant: Hmm. I am not tracking. Help me understand. What's the value of doing it this way one more time? I <em>fear</em> that we might not reach the project's goal with this approach.</p>
</blockquote>
<p>When I think about <em>how</em> to ask questions, I think of two things: </p>
<ul>
<li>I want my question to come from a place of <em>curiosity</em>.</li>
<li>I want to avoid the word <strong>"you."</strong></li>
</ul>
<p>To ask from a place of curiosity, I often have to take a deep breath, put myself in the client's shoes, be empathic for them, and assume that they are trying their best given their circumstances.</p>
<p>Also, by avoiding using the word "you" forces me to talk about <em>my experience</em>. No one can challenge how I feel or what I have experienced. It is <em>my truth</em>.</p>
<p>Don't get me wrong. I am still me, and I am still a person with flaws. I always have moments where I think to myself, "what the f<em>*</em>?." The difference is that when those feelings arise, I acknowledge them and take a moment to think about the person in front of me.</p>
<p><em>Side note:</em> I was fortunate to work with an organizational psychologist at a previous consultancy. She trained me and fellow consultants to avoid certain words so we could speak more collaboratively. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667856178374/mFNns2lPt.jpg" alt="acDZXmwK.jpg" />
We had this posted in our office so that we would remember to use the vocabulary.</p>
<h2 id="heading-15-when-people-lose-their-cool-and-start-yelling-you-can-be-the-voice-of-reason">15. When people lose their cool and start yelling, you can be the voice of reason.</h2>
<p>As a consultant, you probably don't get hired into totally functional companies. From time to time, you might be caught in the middle of an internal political battle where someone ultimately loses their temper. That's a moment where you can add a lot of value.</p>
<p>Here's a recount of something that happened to me:</p>
<p><strong>Person 1:</strong> We can't design the network this way. It leaves XYZ vulnerable. We'd need another ABC service and another firewall. </p>
<p><strong>Person 2:</strong> Ah ok. But this part of the design is ok, right?</p>
<p><strong>Person 1:</strong> No, it's not. We can't link these networks together because XYZ is vulnerable.</p>
<p><em>Person 2 gets frustrated because he wants to go live with the project.</em></p>
<p><strong>Person 2:</strong> We need to start doing things in a new way! We can't be doing data center crap in the cloud!</p>
<p><em>Yelling continues, then stops for a second.</em></p>
<p><strong>Me:</strong> Yeah, Person 1, I understand what you're saying. What if we get rid of the issue? Because this is a greenfield deployment, could we say that workloads that don't meet these criteria won't be able to be hosted here?  That way, we can ship and don't have to design for all possible use cases, especially legacy ones.</p>
<p>Honestly, I can't tell you if this conversation moved them closer to the goal. I can tell you from more than one account that sometimes groups bring you in because they feel pain points in "dealing with another team." They want someone to help them work with another team. So, if I didn't try to be a mediator, I would have had zero chance of moving the outcome forward. If they weren't working well together before they hired me, then they will probably not magically begin to collaborate, right?</p>
<p>The best thing you can do is try; if it doesn't work, it's not any worse than before.</p>
<h2 id="heading-16-you-dont-need-the-client-to-like-you-they-dont-need-to-be-your-friend-say-the-hard-things">16. You <strong>don’t need the client to like you</strong>. They don’t need to be your friend. <strong>Say the hard things</strong>.</h2>
<p>I have to tell myself this all the time. I want people to like me, and I enjoy working with people that are having a good time. But, sometimes, what the client needs is not a friend. Sometimes, they need to hear what is wrong or what could go wrong - directly and clearly.</p>
<p>"If we don't do this soon, I fear we'll not have time to go to production."</p>
<p>"I know the burndown chart says we're tracking to finish the project; I strongly feel that we will need more time than our board says."</p>
<p>"I know you have spent a lot of time on your solution. I want to present you with the alternatives so you know our stance and the surprises that might come up. I would hate for you to continue your current path without knowing the risks. Ultimately, it's your decision, and if you choose to continue on this path, you know the risks."</p>
<p>So don't avoid saying the hard thing because you are afraid they won't like you anymore. Also, remember, watch your tone! </p>
<h2 id="heading-17-as-a-consultant-you-have-no-control-its-all-influence">17. As a <strong>consultant</strong>, you have no control. It’s all influence.</h2>
<p>Early in my career, learning that I had no control was probably one of the biggest lessons that took me the longest to learn. I kept getting frustrated working with some clients because they were not listening fully to my advice. I had to learn that I have <mark>no control over what the client decides or does.</mark></p>
<p>I have no "skin in the game." I don't work for the company that they work for. I probably won't get fired if this project does not go well. So, it's only fair that if they hold the risk, then they decide, right?</p>
<p>Instead, the only tool you have is influence.</p>
<p>How do you influence?</p>
<p>Think of social media influencers like YouTubers. What do they do? </p>
<ul>
<li>They share information in easily digestible ways.</li>
<li>They tell you about other products with <em>honest reviews</em>.</li>
<li>They share their experience and what's worked for them.</li>
<li>They connect you with other great ideas and people.</li>
</ul>
<p>All those things apply, minus the YouTube channel. </p>
<h2 id="heading-18-do-not-word-vomit-use-top-down-communication">18. Do not word vomit. Use "top-down" communication.</h2>
<p>Have you ever sat through a painful presentation where an engineer is presenting to upper management, and management seems disinterested? Was it painful? Did it seem like it was dragging on?</p>
<p>McKinsey is a world-renowned management consulting company. McKinsey uses <strong>"top-down" communication</strong>. Instead of telling a long story and finally providing the key point at the end, present the key point first and then explain. </p>
<p>This blog post is an example of that kind of communication. The key points are delivered as headings. The points might not make sense unless you read the explanations like this one. But, you have the <em>option</em> to determine if the key point is something you're interested in, and <strong>I'm not holding your attention hostage.</strong></p>
<p>I think this blog post in <em>story form</em> would be much more difficult to read. You would have to read <em>everything</em> to get the twenty-eight lessons I am sharing.</p>
<p>As an engineer, you might cringue, but I find PowerPoints very useful. I find them useful because they're a tool that allow me to help break down complex ideas into small chunks and communicate them effectively.
For example:  </p>
<ul>
<li>When creating PowerPoints, I create a slide to share the main recommendations in the beginning of the presentation. </li>
<li>I try to keep six items on the slide at once. Items include images <em>and</em> words. When I do this, it typically leads to people paying more attention to what I am saying because they are not caught reading slides. It also forces me to explain the key point and focus on it. </li>
</ul>
<p>If you want to learn more about the McKinsey method of communication, here's a great video from an ex-McKinsey consultant.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=ppokRtCeuyI&amp;t=10s">https://www.youtube.com/watch?v=ppokRtCeuyI&amp;t=10s</a></div>
<h2 id="heading-19-who-are-you-talking-to-it-doesnt-want-more-work-and-developers-want-to-ship-more">19. Who are you talking to? IT doesn't want more work, and developers want to ship more.</h2>
<p>Unless you have decades of industry experience and managed not to become a manager, you probably don't have experience being a professional system administrator and developer. So, you likely only have experience in the Dev or the Ops in DevOps. If you're a junior DevOps engineer, you have a serious disadvantage. </p>
<p>Part of playing politics and reaching agreements is figuring out what different people <em>want</em> to achieve.
Very generally speaking:</p>
<ul>
<li><strong>Developers</strong> want to ship more value. Developers at large enterprises they're often frustrated with all the processes that prevent them from shipping and how little control they have over infrastructure, even in the cloud. I was a developer at a large enterprise, and I <em>felt</em> this pain point deeply. </li>
<li><strong>IT</strong> wants less work to do. They include system administrators, identity admins, network engineers, infrastructure engineers, cloud admins, etc. They're usually underwater with work and often on call when things go wrong. They're responsible for large infrastructure and don't want to be called at night. Making things standardized makes their job easier. Also, if they're extremely busy, they will sometimes opt for tried and true technology and avoid new trends. Lastly, they also really care about top-down governance because they don't want people they don't know to cause more work for them.</li>
<li><strong>Security</strong> - They feel pressure to keep the company safe from breaches. There are generally different specialties within security. For example, some security engineers focus on network security, some on monitoring and alerting, and some on application security. They know that they are often seen as the folks that halt projects.</li>
</ul>
<p>If you have a different perspective on each group of people care about, that's ok. However, let's leave out security for the following metaphor; otherwise, it is too complicated.</p>
<p>Imagine that your role as a DevOps enabler is to be a bridge for "work." At one end of the bridge, there's "development," and at the other end, "operations." You have made your customers happier when you ship one unit of "work" across the bridge. </p>
<p>So, to become an effective bridge, you must know a bit about "development" and "operations." If you started your career as a developer, then you probably have gaps in operations, as I did. If you started your IT career, you probably have development gaps. If you started your career as a junior DevOps engineer, you probably have gaps in both realms. The key here is to know enough about each role so that you can <em>speak their language.</em> Being able to speak about what someone cares about is a <em>great</em> way to build trust. The more you can speak in someone's language, the more they will think, "you are one of us." And in dysfunctional environments that need the most help, there's usually an "us vs. them" mentality. No pressure. The better bridge you can be, the better advisor you will be.</p>
<p>So truly, here are some of my standard tactics to build more trust:</p>
<ul>
<li>Go out of your way to add guardrails on behalf of IT. Guardrails are often policies that prevent people from doing insecure or unapproved things. Examples of guardrails are Azure Policies, HashiCorp Sentinel, and AWS Policies. After adding guardrails, you can then turn around and try to give developers more autonomy because there are guardrails that disallow them to do bad things.</li>
<li>Go out of your way to increase security before the security engineers say something. By increasing security preemptively, you can gain favor and help design a solution that is not a hindrance.</li>
<li>Show developers Kubernetes, Infrastructure as Code, and Application Performance Monitoring (APM) tools. They will be often be interested in doing little bit of "ops" work so that they can ship more.</li>
</ul>
<h2 id="heading-20-you-are-a-doctor">20. You are a doctor</h2>
<p>How would you interact with people if you thought of yourself as a doctor? </p>
<p>Would you try to find the root cause of a symptom instead of pushing pills first?</p>
<p>In this case, pills are tools. If you push tools all the time, you're not going to solve the root problems. Sometimes organizations require a lifestyle change, which means a process or culture change.</p>
<p>So, think about the care plan for the organization. It could start with a tool to alleviate some profound pain but then think about process improvements to solve the deeper issues.</p>
<h2 id="heading-21-you-have-a-hidden-force-acting-against-your-relationship-with-the-client-its-hourly-billing">21. You have a <strong>hidden force</strong> acting against your <strong>relationship with the client</strong>. It's hourly billing.</h2>
<p>Consulting companies are businesses. Most consulting companies charge their clients by the hour. There is usually a margin between what they pay the consultant and what they charge the client. It's that simple.</p>
<p>So, because of the billing method, a consulting company makes more money when:</p>
<ul>
<li>Projects take longer, even if they are delayed.</li>
<li>Solutions are more complex because they will take more time and/or people.</li>
<li>Client organizations cannot maintain a solution, so they must continue hiring the consulting organization.</li>
</ul>
<p>I'll be honest. I find these to be extremes. I wouldn't work for a consulting organization that's doing these practices on purpose to make more money. </p>
<p>And, humans can be exceptionally good at smelling intent. </p>
<p>Also, we humans can be very sensitive to perceived ill intent. Think used car salesman.</p>
<p>For example, one time, I was working with an excellent client. We had a great relationship. They had asked for a follow-up engagement and given me a list of items to help them with. However, it was in the middle of a heavy sales process change, and I needed approval from general managers and go through a "deal review." I was so focused on getting approval and fighting the spreadsheets that I lost sight of the hourly rate we were charging the client <em>before</em> the process changed. I was trying to get approval so we could start soon, but the new rate was considerably higher. We sent the proposal to the client; they expressed that they had <strong>sticker shock</strong> (the price was extremely high). The relationship ended.</p>
<p>I believe that they thought that because we did a good job and they were asking for more help, we were being bold and raising the rates because they needed us. In reality, I was scoping what they had asked for and made a mistake carrying the proposal through the heavy-handed sales process. </p>
<p>The lesson to be learned is to remember what your company is incentivized to do and consider how your client will feel. Don't overcomplicate solutions, be transparent about delays, communicate early and often, and be mindful that a client must maintain your solution, show urgency, etc.</p>
<p>I'll leave you with this: imagine you hired a good plumber to fix a leaky pipe and he was charging you $150/hour. How would you feel is he was taking his sweet time and moving slow and looking ineffective? If you had a low budget, I think that would give anyone a little anxiety. </p>
<h2 id="heading-22-virtual-desktops-over-laptops">22. Virtual desktops over laptops</h2>
<p>If you're a consultant, you might like this one. Request a virtual desktop instead of a laptop when starting a project that's less than a year. A virtual desktop allows you to easily switch between your company's and the client's communications without needing a fancy KVM. </p>
<p><em>Side note:</em> here's a product idea: a very fancy <em>KVM for consultants</em>. Before giving up and accepting virtual desktops, I vigorously searched the market for the perfect KVM that would be supported by MacOS and Windows so that I could keep my peripherals plugged in one place and switch between computers effortlessly. </p>
<p>Consultants often have to get access to the client's systems to perform the work. <em>Many</em> companies have security postures that don't accept using the consultant's workstation, hence, bringing their own device. Instead, <em>many</em> enterprises like to provide laptops to contractors so that they can connect to the network from a device they can trust and control. </p>
<p>Well, if you're a consultant, these devices can pile up. I have an ugly laptop that's been collecting dust next to me for six months <em>after</em> I finished the project. 😊</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/gaunacode/status/1182776148012482561?s=20&amp;t=MktaVzDdQLOUSCr1gMUfdQ">https://twitter.com/gaunacode/status/1182776148012482561?s=20&amp;t=MktaVzDdQLOUSCr1gMUfdQ</a></div>
<p>I was once doing a project with a client that also hired AWS Professional Services (AWS consulting). I overheard the AWS project manager tell the client that using virtual desktops instead of laptops was part of their contract. I found this interesting because it took me two months to get set up with a laptop. It took the AWS consultants <em>two weeks</em> to get set up. Then, I started asking my clients for virtual desktops for short/medium projects. </p>
<p>By using VDIs, I was relieved that I didn't have to agonize anymore about having all this clutter and physical devices everywhere. Also, starting the project with virtual desktops is often much faster than with laptops. </p>
<p>I hope this doesn't happen to you, but I once had to make a round-trip of <strong>seven hours</strong> to go to a location to get a proprietary <em>badge</em> and then drive another <strong>nine hours</strong> round trip to pick up a laptop. The badge unlocked the laptop. Then, another time, I had to drive an <strong>hour each way</strong>, sit outside a client's branch location at a Chinese fast food place, get on the client's Wi-Fi, then talk to the IT help desk for <strong>two hours</strong> to get my awesome laptop set-up. </p>
<h2 id="heading-23-juggling-multiple-emails-is-a-necessary-evil">23. Juggling multiple emails is a necessary evil</h2>
<p>Another tip for a new consultant. </p>
<p>On top of getting a new laptop from clients, we are often given an email address by the client. So on top of our actual work email addresses, we end up with all these other identities and inboxes for each client. </p>
<p>At the beginning of my consulting career, I tried hard to solve this problem. The reality is that many <em>enterprises</em> lock down any customizations to their emails so that you can't use external OAuth applications. Heck, I had one client that monitored their emails so closely that I was <strong>strongly</strong> warned that if I ever forwarded an internal email to an external account, an automated system would pick it up and fire me. I was also <em>strongly</em> advised not to print anything because I would be automatically fired.</p>
<p>You will have multiple emails and identities. Just accept it. It's the consulting tax. 😀</p>
<h2 id="heading-24-windows-is-sometimes-a-necessary-evil">24. Windows is sometimes a necessary evil</h2>
<p>Last tip for an existing consultant, especially one that hates Windows. 😄</p>
<p>To be clear, I like Windows, even though I'm in the DevOps field. I'm writing this blog post from a Windows 11 laptop. 🤗 </p>
<p>However, I wouldn't say I like Windows as a <em>typical enterprise</em> setting, and that's because of all the controls and bloatware that enterprises install. I bet everyone hates slow laptops and funky issues due to bloatware.</p>
<p>Just like you can ask for a virtual desktop, you can also ask for what operating systems they support. Rarely do enterprises support Linux for workstations. Sometimes, you might get lucky, and an enterprise supports MacOs. Most of the time, enterprises only support Windows.</p>
<h2 id="heading-25-kiss-is-relative-simplicity-is-clear-its-not-lazy">25. KISS is relative. Simplicity is clear; it's not lazy.</h2>
<p>Many engineers like to say Keep It Simply Stupid (KISS). I agree. But I think simplicity is relative. </p>
<p>What you might find simple, I might not find simple. What I could find simple, the client might find complicated. What the client might find simple, I might find an abomination.</p>
<p>I think simple takes work. I don't think arriving at the first solution is often the "simple" one. Doing the least amount of work can be "lazy." So, I'm going to do this... I'm going to give you a Steve Jobs quote. 😀</p>
<blockquote>
<p>“Simple can be harder than complex: You have to work hard to get your thinking clean to make it simple. But it's worth it in the end because once you get there, you can move mountains.” - Steve Jobs</p>
</blockquote>
<p>There have been times that I <strong>worked hard</strong> to make a solution "simple" for clients. Here are some of the attributes I'm looking for:</p>
<ul>
<li><strong>Simple is clear</strong> - When the client reviews the solution, does it have an "aha!" moment? Will it click? <em>Many</em> consulting solutions go to die because when the client looks at the solution, they find it too complex and don't have the time to reverse engineer it.</li>
<li><strong>Simple is maintainable</strong> - Assuming that the client will work to learn the foundations of technology, will they know enough to maintain and extend it? One time, I had a client demanding that we undo a CI/CD pipeline written in YAML because they wanted to use the old and comfortable GUI-based pipelines in Azure DevOps. I think this was lazy on the client's part because they wanted to do what they've always done instead of exploring an industry practice. But, if our client had learned the essentials of building a YAML-based pipeline, could they grab our pipeline and understand it enough to extend it? </li>
<li><strong>Simple is not a <a target="_blank" href="https://youtu.be/VpLDfkLBJ0Q">Rube-Goldberg machine</a></strong> - Is your solution starting to feel like a Rube-Goldberg machine? Maybe, it's simple on its face but complicated behind the scenes. For instance, I was reviewing an application that my client had built, three simple pages. It was a user sign-up, a login, and the main page. Behind the scenes, there were like twenty microservices! It took a lot of <strong>cognitive load</strong> to understand what was happening in the backend. Oof.</li>
</ul>
<h2 id="heading-26-avoid-designing-the-bike-shed">26. Avoid designing the bike shed</h2>
<p>I learned this one from a co-worker recently. I think it applies to a lot of organizations. </p>
<p>What is described in this <a target="_blank" href="https://en.wikipedia.org/wiki/Law_of_triviality">Wikipedia</a> article, and what I have experienced, is that often we can easily focus on trivial things. To "avoid designing the bike shed" means that if you're designing something important like a nuclear power plant, then don't place as much importance on designing the bike shed next to the power plant.</p>
<p>Recently, I was on a project to design a very important enterprise-scale landing zone for the cloud, meaning the cloud foundation. The client was interested in discussing naming conventions and spending time on them. Naming is hard. I get it. So, I'd rather use a naming convention that a reputable source created rather than waste precious mental energy creating my naming convention that I'll probably get wrong. </p>
<p>Be aware of this for yourself, your teammates, and your clients. You might be stuck discussing something that will not improve the nuclear power plant.</p>
<h2 id="heading-27-confidence-is-different-from-assertiveness-confidence-comes-from-practice">27. Confidence is different from assertiveness. Confidence comes from practice.</h2>
<p>Have you met someone that seems like they're <em>exuding</em> confidence? I'm not that person, and I've always envied that to some extent. </p>
<p>Instead, I'm naturally quite the opposite. I have a lot of self-doubts, and I double-guess myself. So, I've asked co-workers, "you sound so confident; how do you do it?."</p>
<p>Sometimes, some people may <em>sound</em> confident, but inside, they are still scared. Their physiological responses are ones that you might not see. For example, when I get nervous, you can tell. My ears turn red, I start to sweat a <em>lot</em>, and I start to stutter and think slower. So, some people might be nervous, yet they don't show those clear signs, and then you might make up a story in your mind that they're confident because they sound so assertive.</p>
<p>The true solution that I found that works for me is <strong>practice</strong>. The more I do something, the more confident I get. So, I accept it and give myself permission to be nervous when I'm not great at something <em>yet</em>. If I keep showing up and doing, I will eventually get better and be much more confident.</p>
<p>Here's a book that helped me and might help you: <a target="_blank" href="https://www.amazon.com/Confidence-Gap-Guide-Overcoming-Self-Doubt/dp/1590309235/ref=sr_1_1?crid=3JVQYK44Y5LTC&amp;keywords=The+Confidence+Gap+A+Guide+to+Overcoming+Fear+and+Self-Doubt&amp;qid=1666868619&amp;qu=eyJxc2MiOiIxLjM4IiwicXNhIjoiMS4xMyIsInFzcCI6IjEuMTkifQ%3D%3D&amp;sprefix=the+confidence+gap+a+guide+to+overcoming+fear+and+self-doubt%2Caps%2C105&amp;sr=8-1">The Confidence Gap: A Guide to Overcoming Fear and Self-Doubt</a>.</p>
<h2 id="heading-28-how-you-treat-others-is-a-reflection-of-how-you-treat-yourself">28. How you treat others is a reflection of how you treat yourself</h2>
<p>Do you remember a time that you worked with someone that was really critical? </p>
<p>Maybe they weren't criticizing you, but you were listening to how they criticizing someone else. I've come to know someone with this tendency, and when I hear what they think of themselves, it's <em>rough</em>. I wouldn't want to be in their brain. There's a lot of stress and hate, so if they're hard on others, they're likely even harsher on themselves.</p>
<p>Watch how you show up with others. If you're passing judgment, you're likely to pass a lot of judgment on yourself. Be kind to yourself. </p>
<p>Also, it works the other way around. When working with someone overly critical of you, try thinking about how much harder they probably are on themselves. If I hear harsh criticism from someone, I find it useful to think about their inner struggle. It helps me center myself and lower my defenses because I realize they're also struggling. </p>
<h2 id="heading-thank-you-for-your-attention">Thank you for your attention!</h2>
<p>I know there's <em>so much content</em> out there. I'm honored if you've read some or all of this article. Remember, I'm just a random dude from the internet, and some of my lessons might or might not help you. </p>
]]></content:encoded></item><item><title><![CDATA[Deploying Terraform at scale with GitHub Actions]]></title><description><![CDATA[Have you wondered what is the best workflow to use open-source Terraform at scale? In this blog post, I'll walk you through my favorite workflow using GitHub Actions. I'll deploy to Azure, but the same workflow can be adapted with other cloud provide...]]></description><link>https://gaunacode.com/deploying-terraform-at-scale-with-github-actions</link><guid isPermaLink="true">https://gaunacode.com/deploying-terraform-at-scale-with-github-actions</guid><category><![CDATA[Devops]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Tue, 23 Aug 2022 13:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1661200043827/cJiwm1gU-.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you wondered what is the best workflow to use open-source Terraform at scale? In this blog post, I'll walk you through my favorite workflow using GitHub Actions. I'll deploy to Azure, but the same workflow can be adapted with other cloud providers.</p>
<p><strong>TLDR;</strong> I wrote a Terraform composite action that you can just plug into your GitHub workflow to make this happen. Jump ahead to <strong>Putting it all together</strong> section.</p>
<h2 id="heading-background">Background</h2>
<p>Here's the thing with GitHub - it favors automation running from pull requests and issues. For example, with other systems, Azure DevOps and GitLab, engineers can create "stages" to be able to separate environments and promotion through environments. And while GitHub has "environments" and "stages," I don't find them as effective as the other competing platforms. Even if it did, I like the pull-request workflow because it addresses some of the challenges I experienced at scale.</p>
<p>At scale, I ran into the following challenges:</p>
<ul>
<li><strong>Accountability</strong> - For companies not interested in paying for Terraform Cloud, having other engineers consistently review the Terraform plans can be difficult. Making engineers go to a pipeline log to view the Terraform plan reduces the experience and increases the chance that someone will blindly approve without looking closely at the plan.</li>
<li><strong>Really large Terraform plans</strong> - While very much discouraged, sometimes they happen. Big bang Terraform deployments are sometimes unavoidable on the first promotion to an environment. </li>
</ul>
<p><strong>Note</strong>: This content was inspired by the <a target="_blank" href="https://github.com/hashicorp/setup-terraform">README of the setup-terraform</a> action and experiences in the real world.</p>
<h2 id="heading-what-guide-is-not">What guide is not</h2>
<ul>
<li>A beginner guide on Terraform </li>
<li>An introduction to how to create GitHub Workflows/Actions</li>
<li>A how-to on creating a simple pipeline that deploys to Azure</li>
</ul>
<h2 id="heading-the-process">The process</h2>
<p>This process uses a <a target="_blank" href="https://trunkbaseddevelopment.com/">trunk-based</a> branching strategy. Environments like "dev", "test", and "prod" are isolated via <strong>folders</strong> and there's a single long-lived branch, <code>main</code>.</p>
<pre><code class="lang-shell">.
├── production
│   └── main.tf
└── staging
    └── main.tf
</code></pre>
<p><mark>Why folders?</mark> I find it the most elegant, and it's a whole topic. It also works well with a pull-request workflow. If you wonder about the advantages of separating different environments via folders vs. branches vs. workspaces, then <a target="_blank" href="https://blog.gruntwork.io/how-to-manage-multiple-environments-with-terraform-32c7bc5d692">Yevgeniy Brikman</a> recently wrote an in-depth blog post series on this topic. </p>
<p>So, here's what the process would look like. </p>
<ol>
<li>When someone opens a pull request, a GitHub workflow will kick off and run <code>terraform plan.</code> </li>
<li>When someone merges the pull request against <code>main</code>, the same GitHub workflow will kick off and run both <code>terraform plan</code> and <code>terraform apply.</code></li>
<li>Depending on what files under which environment folder were changed, a different GitHub workflow will trigger. For instance, if someone changes the Terraform code inside the <code>staging</code> environment folder, it will trigger the staging GitHub workflow for the staging environment.</li>
</ol>
<p><strong>Note:</strong> Terragrunt will help you keep code <a target="_blank" href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself">DRY</a>, but it's certainly <em>not</em> a requirement to use Terragrunt for environmental folders.</p>
<h2 id="heading-writing-the-github-actions">Writing the GitHub Actions</h2>
<h3 id="heading-1-the-trigger">1. The trigger</h3>
<p>When someone changes a Terraform file in the environment folder, trigger the appropriate action. You'll need a GitHub workflow file <em>per environment</em>.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">'Terraform - Staging'</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
    <span class="hljs-attr">paths:</span> 
    <span class="hljs-bullet">-</span> <span class="hljs-string">staging/**</span>
  <span class="hljs-attr">pull_request:</span>
    <span class="hljs-attr">branches:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
    <span class="hljs-attr">paths:</span> 
    <span class="hljs-bullet">-</span> <span class="hljs-string">staging/**</span>
</code></pre>
<h3 id="heading-2-create-a-github-environment">2. Create a GitHub environment</h3>
<p>Under the settings of the repository you're using, create an environment like "Staging." Then, if you're deploying to Azure, you can <a target="_blank" href="https://docs.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli">create an Azure Service Principal</a> and set secrets like <code>CLIENT_ID</code> and <code>CLIENT_SECRET</code> for the new environment. By having secrets scoped to the environment, you can use separate service principals per environment. </p>
<p>If you are not deploying to Azure, you can create whatever secrets your Terraform provider needs to authenticate.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661199221057/AI86db4ZM.png" alt="2022-08-22_16-13-09.png" /></p>
<h3 id="heading-3-set-the-permissions-for-githubtoken">3. Set the permissions for <code>GITHUB_TOKEN</code></h3>
<p>The following sets the permissions for the <code>GITHUB_TOKEN</code> to be able to add comments to the pull requests. You have to add this to your GitHub workflow file, which can be at the same level as <code>on</code> and <code>jobs</code>. The good news is that this configuration avoids you having to create a GitHub PAT.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-attr">permissions:</span>
  <span class="hljs-attr">contents:</span> <span class="hljs-string">read</span>
  <span class="hljs-attr">pull-requests:</span> <span class="hljs-string">write</span>
</code></pre>
<h3 id="heading-4-set-up-terraform-action">4. Set-up Terraform Action</h3>
<p>GitHub-hosted runners don't have Terraform installed out of the box, and you'll want to install it on each workflow run. Even if you use self-hosted runners, you still might want to consider using this action so that each repository can fully control what version of Terraform they are using.</p>
<p>Also, in the following snippet, we're using the "Staging" environment we created.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-attr">jobs:</span> 
  <span class="hljs-attr">terraform:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Terraform</span>
    <span class="hljs-attr">environment:</span> <span class="hljs-string">staging</span> 
    <span class="hljs-attr">steps:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Setup</span> <span class="hljs-string">Terraform</span>
      <span class="hljs-attr">uses:</span> <span class="hljs-string">hashicorp/setup-terraform@v2</span>
      <span class="hljs-attr">with:</span>
        <span class="hljs-attr">terraform_version:</span> <span class="hljs-number">1.1</span><span class="hljs-number">.9</span>
        <span class="hljs-attr">terraform_wrapper:</span> <span class="hljs-literal">false</span>
</code></pre>
<p><strong>Note</strong>: <code>terraform_wrapper</code> is set to false. This flag means there won't be a wrapper that highjacks the STDOUT from the Terraform commands. We'll still get fancy to add the Terraform plan to the pull-request comments.</p>
<h3 id="heading-5-terraform-init">5. Terraform Init</h3>
<p>This step requires that you have a Terraform backend configured. Otherwise, your pipeline won't work correctly since you will dispose of the Terraform state on each run. Here's how to set up a <a target="_blank" href="https://www.terraform.io/language/settings/backends/azurerm">Terraform backend using Azure</a>.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Terraform</span> <span class="hljs-string">Init</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">init</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">terraform</span> <span class="hljs-string">init</span>
  <span class="hljs-attr">env:</span>
    <span class="hljs-attr">ARM_CLIENT_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_ID</span> <span class="hljs-string">}}</span> <span class="hljs-comment"># This doesn't have to be a secret. Client IDs are not sensitive.</span>
    <span class="hljs-attr">ARM_CLIENT_SECRET:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_SECRET</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">ARM_SUBSCRIPTION_ID:</span> <span class="hljs-string">"[Your Subscription ID]"</span>
    <span class="hljs-attr">ARM_TENANT_ID:</span> <span class="hljs-string">"[Your Tenant ID]"</span>
  <span class="hljs-attr">working-directory:</span> <span class="hljs-string">./staging</span> <span class="hljs-comment"># Use your environment folder</span>
  <span class="hljs-attr">shell:</span> <span class="hljs-string">bash</span>
</code></pre>
<h3 id="heading-6-terraform-plan">6. Terraform Plan</h3>
<p>We're getting closer to the good part. There are some important arguments that we provide to Terraform when we "plan":</p>
<ul>
<li><code>-input=false</code> - This flag tells Terraform to fail if there are some missing variable values that were not specified. Otherwise, Terraform will stop the pipeline because it will ask the user (that's not there) for a variable value.</li>
<li><code>-no-color</code> - This flag tells Terraform not to be cute and show colors. Otherwise, you'll see funky characters in the output stream of the GitHub actions.</li>
<li><code>-out tf.plan</code> - This tells Terraform to write the plan to a local file. It will be used by the <code>terraform apply</code> and <code>terraform show</code>.</li>
</ul>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Terraform</span> <span class="hljs-string">Plan</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">plan</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">|
    terraform plan -input=false -no-color -out tf.plan
</span>  <span class="hljs-attr">env:</span>
    <span class="hljs-attr">ARM_CLIENT_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_ID</span> <span class="hljs-string">}}</span> 
    <span class="hljs-attr">ARM_CLIENT_SECRET:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_SECRET</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">ARM_SUBSCRIPTION_ID:</span> <span class="hljs-string">"[Your Subscription ID]"</span>
    <span class="hljs-attr">ARM_TENANT_ID:</span> <span class="hljs-string">"[Your Tenant ID]"</span>
  <span class="hljs-attr">working-directory:</span> <span class="hljs-string">./staging</span> <span class="hljs-comment"># Use your environment folder</span>
  <span class="hljs-attr">shell:</span> <span class="hljs-string">bash</span>
</code></pre>
<h3 id="heading-7-terraform-show">7. Terraform Show</h3>
<p>The <code>show</code> command simply replays the information shown from <code>terraform plan</code>. During this action, we will pipe the output of <code>terraform show</code> to a local temporary file.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Terraform</span> <span class="hljs-string">Show</span>
  <span class="hljs-attr">id:</span> <span class="hljs-string">show</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">terraform</span> <span class="hljs-string">show</span> <span class="hljs-string">-no-color</span> <span class="hljs-string">tf.plan</span> <span class="hljs-number">2</span><span class="hljs-string">&gt;&amp;1</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">/tmp/plan.txt</span>
  <span class="hljs-attr">working-directory:</span> <span class="hljs-string">./staging</span> <span class="hljs-comment"># Use your environment folder</span>
  <span class="hljs-attr">env:</span>
    <span class="hljs-attr">ARM_CLIENT_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_ID</span> <span class="hljs-string">}}</span> 
    <span class="hljs-attr">ARM_CLIENT_SECRET:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_SECRET</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">ARM_SUBSCRIPTION_ID:</span> <span class="hljs-string">"[Your Subscription ID]"</span>
    <span class="hljs-attr">ARM_TENANT_ID:</span> <span class="hljs-string">"[Your Tenant ID]"</span>
  <span class="hljs-attr">shell:</span> <span class="hljs-string">bash</span>
</code></pre>
<h3 id="heading-8-write-the-plan-to-the-pull-request">8. Write the plan to the pull-request</h3>
<p>In this step, we use <code>actions/github-script@v6</code> to create the GitHub pull-request comment. The action runs open-ended scripts that you create in JavaScript.  The contents of the pull-request comment come from the temp file created in the earlier step. </p>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/github-script@v6</span>
  <span class="hljs-attr">if:</span> <span class="hljs-string">github.event_name</span> <span class="hljs-string">==</span> <span class="hljs-string">'pull_request'</span>
  <span class="hljs-attr">with:</span>
    <span class="hljs-attr">github-token:</span> <span class="hljs-string">${{</span> <span class="hljs-string">inputs.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">script:</span> <span class="hljs-string">|
      const fs = require("fs");
      const plan = fs.readFileSync("/tmp/plan.txt", "utf8");
      const maxGitHubBodyCharacters = 65536;
</span>
      <span class="hljs-string">function</span> <span class="hljs-string">chunkSubstr(str,</span> <span class="hljs-string">size)</span> {
        <span class="hljs-string">const</span> <span class="hljs-string">numChunks</span> <span class="hljs-string">=</span> <span class="hljs-string">Math.ceil(str.length</span> <span class="hljs-string">/</span> <span class="hljs-string">size)</span>
        <span class="hljs-string">const</span> <span class="hljs-string">chunks</span> <span class="hljs-string">=</span> <span class="hljs-string">new</span> <span class="hljs-string">Array(numChunks)</span>
        <span class="hljs-string">for</span> <span class="hljs-string">(let</span> <span class="hljs-string">i</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span>, <span class="hljs-string">o</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span><span class="hljs-string">;</span> <span class="hljs-string">i</span> <span class="hljs-string">&lt;</span> <span class="hljs-string">numChunks;</span> <span class="hljs-string">++i</span>, <span class="hljs-string">o</span> <span class="hljs-string">+=</span> <span class="hljs-string">size)</span> {
          <span class="hljs-string">chunks</span>[<span class="hljs-string">i</span>] <span class="hljs-string">=</span> <span class="hljs-string">str.substr(o</span>, <span class="hljs-string">size)</span>
        }
        <span class="hljs-string">return</span> <span class="hljs-string">chunks</span>
      }

      <span class="hljs-string">//</span> <span class="hljs-string">Split</span> <span class="hljs-string">the</span> <span class="hljs-string">Terraform</span> <span class="hljs-string">plan</span> <span class="hljs-string">into</span> <span class="hljs-string">chunks</span> <span class="hljs-string">if</span> <span class="hljs-string">it's</span> <span class="hljs-string">too</span> <span class="hljs-string">big</span> <span class="hljs-string">and</span> <span class="hljs-string">can't</span> <span class="hljs-string">fit</span> <span class="hljs-string">into</span> <span class="hljs-string">the</span> <span class="hljs-string">GitHub</span> <span class="hljs-string">Action</span>
      <span class="hljs-string">var</span> <span class="hljs-string">plans</span> <span class="hljs-string">=</span> <span class="hljs-string">chunkSubstr(plan,</span> <span class="hljs-string">maxGitHubBodyCharacters);</span> 
      <span class="hljs-string">for</span> <span class="hljs-string">(let</span> <span class="hljs-string">i</span> <span class="hljs-string">=</span> <span class="hljs-number">0</span><span class="hljs-string">;</span> <span class="hljs-string">i</span> <span class="hljs-string">&lt;</span> <span class="hljs-string">plans.length;</span> <span class="hljs-string">i++)</span> {
        <span class="hljs-string">const</span> <span class="hljs-string">output</span> <span class="hljs-string">=</span> <span class="hljs-string">`###</span> <span class="hljs-string">$<span class="hljs-template-variable">{{ inputs.plan-title }}</span></span> <span class="hljs-string">Part</span> <span class="hljs-comment"># ${i + 1}</span>
        <span class="hljs-comment">#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`</span>
        <span class="hljs-comment">#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`</span>
        <span class="hljs-comment">#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`</span>
        <span class="hljs-string">&lt;details&gt;&lt;summary&gt;Show</span> <span class="hljs-string">Plan&lt;/summary&gt;</span>
        <span class="hljs-string">\`\`\`\n</span>
        <span class="hljs-string">$</span>{<span class="hljs-string">plans</span>[<span class="hljs-string">i</span>]}
        <span class="hljs-string">\`\`\`</span>
        <span class="hljs-string">&lt;/details&gt;</span>
        <span class="hljs-string">*Pusher:</span> <span class="hljs-string">@$<span class="hljs-template-variable">{{ github.actor }}</span></span>, <span class="hljs-attr">Action:</span> <span class="hljs-string">\`$<span class="hljs-template-variable">{{ github.event_name }}</span>\`</span>, <span class="hljs-attr">Working Directory:</span> <span class="hljs-string">\`$<span class="hljs-template-variable">{{ inputs.working-directory }}</span>\`</span>, <span class="hljs-attr">Workflow:</span> <span class="hljs-string">\`$<span class="hljs-template-variable">{{ github.workflow }}</span>\`</span><span class="hljs-string">*`;</span>   

        <span class="hljs-string">await</span> <span class="hljs-string">github.rest.issues.createComment(</span>{
          <span class="hljs-attr">issue_number:</span> <span class="hljs-string">context.issue.number</span>,
          <span class="hljs-attr">owner:</span> <span class="hljs-string">context.repo.owner</span>,
          <span class="hljs-attr">repo:</span> <span class="hljs-string">context.repo.repo</span>,
          <span class="hljs-attr">body:</span> <span class="hljs-string">output</span>
        }<span class="hljs-string">)</span>
      }
</code></pre>
<p>Lastly, if you look closely, there are some magic variables like <code>maxGitHubBodyCharacters</code> and a helper function <code>chunkSubstr</code>. These are my learnings from working with Terraform, specially <em>really</em> large plans. There is a <a target="_blank" href="https://github.com/orgs/community/discussions/27190">known discussion</a> that reveals that there is a limit to how big a comment can be, and it's tied to the underlying GitHub comment schema in their database. So, the <code>maxGitHubBodyCharacters</code> is the limit of characters allowed by GitHub's database, and <code>chunkSubstr</code> function breaks any huge comments into smaller parts.</p>
<p>Here's what the actual pull-request comment would look like. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1661199139695/SGkLz4J1X.png" alt="2022-08-22_16-09-40.png" /></p>
<p>The line of code that says <code>&lt;details&gt;&lt;summary&gt;Show Plan&lt;/summary&gt;</code> is the toggle "Show Plan" in the pull-request comment.</p>
<h3 id="heading-9-terraform-apply">9. Terraform Apply</h3>
<p>In this step, we will "apply" the plan file created during <code>terraform plan</code>. Notice there is a conditional <code>if</code> operator, so the <code>apply</code> only happens when the GitHub workflow runs from the <code>main</code> branch. If you use a different branch name like <code>master</code>, you would have to adjust this GitHub action.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Terraform</span> <span class="hljs-string">Apply</span>
  <span class="hljs-attr">if:</span> <span class="hljs-string">github.ref</span> <span class="hljs-string">==</span> <span class="hljs-string">'refs/heads/main'</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">github.event_name</span> <span class="hljs-string">==</span> <span class="hljs-string">'push'</span>
  <span class="hljs-attr">run:</span> <span class="hljs-string">terraform</span> <span class="hljs-string">apply</span> <span class="hljs-string">-input=false</span> <span class="hljs-string">tf.plan</span>
  <span class="hljs-attr">env:</span>
    <span class="hljs-attr">ARM_CLIENT_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_ID</span> <span class="hljs-string">}}</span> 
    <span class="hljs-attr">ARM_CLIENT_SECRET:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_SECRET</span> <span class="hljs-string">}}</span>
    <span class="hljs-attr">ARM_SUBSCRIPTION_ID:</span> <span class="hljs-string">"[Your Subscription ID]"</span>
    <span class="hljs-attr">ARM_TENANT_ID:</span> <span class="hljs-string">"[Your Tenant ID]"</span>
  <span class="hljs-attr">working-directory:</span> <span class="hljs-string">./staging</span> <span class="hljs-comment"># Use your environment folder</span>
  <span class="hljs-attr">shell:</span> <span class="hljs-string">bash</span>
</code></pre>
<h2 id="heading-putting-it-all-together">Putting it all together</h2>
<p>Combining all these actions for each repository and environment can be overwhelming. So, I created a GitHub composite workflow that you can reuse. If you cannot use it because of organizational policies, you can copy the action source code.</p>
<p><a target="_blank" href="https://github.com/boxboat/terraform-azure-actions"><img src="https://gh-card.dev/repos/boxboat/terraform-azure-actions.svg?fullname=" alt="boxboat/terraform-azure-actions - GitHub" /></a></p>
<p>And, here's how you would use it. </p>
<pre><code class="lang-yaml"><span class="hljs-comment"># .github/workflows/staging.yaml</span>
<span class="hljs-attr">name:</span> <span class="hljs-string">'Terraform - Staging'</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
    <span class="hljs-attr">paths:</span> 
    <span class="hljs-bullet">-</span> <span class="hljs-string">staging/**</span>
  <span class="hljs-attr">pull_request:</span>
    <span class="hljs-attr">branches:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
    <span class="hljs-attr">paths:</span> 
    <span class="hljs-bullet">-</span> <span class="hljs-string">staging/**</span>

<span class="hljs-attr">permissions:</span>
  <span class="hljs-attr">contents:</span> <span class="hljs-string">read</span>
  <span class="hljs-attr">pull-requests:</span> <span class="hljs-string">write</span>

<span class="hljs-attr">jobs:</span> 
  <span class="hljs-attr">terraform:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">Terraform</span>
    <span class="hljs-attr">environment:</span> <span class="hljs-string">staging</span> 
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v3</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">boxboat/terraform-azure-actions@v1.0.0</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">client-id:</span> <span class="hljs-string">'&lt;&lt; service principal client id &gt;&gt;'</span>
          <span class="hljs-attr">client-secret:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CLIENT_SECRET</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">default-subscription-id:</span> <span class="hljs-string">'&lt;&lt; some subscription id &gt;&gt;'</span>
          <span class="hljs-attr">tenant-id:</span> <span class="hljs-string">'&lt;&lt; some aad tenant id &gt;&gt;'</span>
          <span class="hljs-attr">working-directory:</span> <span class="hljs-string">'./staging'</span>
          <span class="hljs-attr">plan-title:</span> <span class="hljs-string">'Staging :star:'</span>
          <span class="hljs-attr">GITHUB_TOKEN:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.GITHUB_TOKEN</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">terraform-version:</span> <span class="hljs-number">1.1</span><span class="hljs-number">.9</span>
</code></pre>
<p>That's it! 🎉</p>
<h2 id="heading-the-caveats">The Caveats</h2>
<p>Here are the caveats that you should be aware of. </p>
<ul>
<li><strong>Race conditions</strong> - There won't be anything preventing you from trying to deploy conflicting changes simultaneously. Let's say you have two pull requests against the same environment, then merge them one after the other. The GitHub actions will trigger twice at roughly the same time. In the best-case scenario, one of the pipelines fails to acquire the terraform lock on the backend provider. <mark> You'll have to be diligent about which pull request and the other in which you merge them.</mark> </li>
<li><strong>Large plans</strong> - As mentioned earlier in this post, large Terraform plans are never fun to work with. There is a limit on the size of the GitHub comment you can write to issues and pull requests. Therefore, to have a reliable workflow, <mark>your GitHub action should account for large Terraform plans and try to break up the plan into <em>multiple</em> comments.</mark> An alternative to the solution covered in this post is to trim the <code>terraform plan</code> when it gets past a specific size.</li>
<li><strong>Noise in pull-requests</strong> - While performing with a big bang deployment, we had to update pull-request multiple times. So, because each plan created about 20 comments, there was a lot of noise. A co-worker didn't like this, so he deleted old and irrelevant comments before each Git push. If you prefer to update the comment, check out the <a target="_blank" href="https://github.com/hashicorp/setup-terraform">README for the setup-terraform</a> action.</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I love Terraform. Some of my coworkers call it "terrorform" because of the quirky challenges at scale. If companies don't pay for a solution like Terraform Cloud, adopt <a target="_blank" href="https://www.runatlantis.io/">Atlantis</a>, or use other paid solutions, they will have to engineer solutions like the ones discussed in this post. Hopefully, you found this helpful. Feel free to leave a comment below if you have questions. You can also tweet me at <a target="_blank" href="https://twitter.com/gaunacode">@gaunacode</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Using Terragrunt to deploy to Azure]]></title><description><![CDATA[I finally had the chance to play with Terragrunt. Unfortunately, I didn't find much content for using Terragrunt for Azure deployments. So, here it is. The guide I wish I had.
What this guide is not

An introduction to Terraform

An introduction to T...]]></description><link>https://gaunacode.com/using-terragrunt-to-deploy-to-azure</link><guid isPermaLink="true">https://gaunacode.com/using-terragrunt-to-deploy-to-azure</guid><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[terragrunt]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Mon, 21 Feb 2022 20:29:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/oXYPuX99htM/upload/v1645388563059/Q0QH_S5Bl.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I finally had the chance to play with Terragrunt. Unfortunately, I didn't find much content for using Terragrunt for Azure deployments. So, here it is. The guide I wish I had.</p>
<h1 id="heading-what-this-guide-is-not">What this guide is not</h1>
<ul>
<li><p>An introduction to Terraform</p>
</li>
<li><p>An introduction to Terragrunt</p>
</li>
<li><p>An introduction to Azure</p>
</li>
</ul>
<p>Instead, this guide will cover how to set up an opinionated Terragrunt "live" repository so that you can deploy across multiple subscriptions, regions, resource groups, and application environments.</p>
<p>If you're looking for an overview of Terraform and how to use it with Azure, <a target="_blank" href="https://learn.hashicorp.com/collections/terraform/azure-get-started">there's a great lab by HashiCorp that's free</a>.</p>
<p>If you're looking for an intro to Terragrunt, the <a target="_blank" href="https://terragrunt.gruntwork.io/docs/getting-started/quick-start/">Terragrunt docs</a> is a great place to start.</p>
<p>📕 Also, Terragrunt makes it easier to implement some of the ideas called out in the <a target="_blank" href="https://www.amazon.com/Terraform-Running-Writing-Infrastructure-Code/dp/1492046906/ref=asc_df_1492046906/?tag=hyprod-20&amp;linkCode=df0&amp;hvadid=385609353592&amp;hvpos=&amp;hvnetw=g&amp;hvrand=3123620425578758399&amp;hvpone=&amp;hvptwo=&amp;hvqmt=&amp;hvdev=c&amp;hvdvcmdl=&amp;hvlocint=&amp;hvlocphy=9011814&amp;hvtargid=pla-556939758690&amp;psc=1&amp;tag=&amp;ref=&amp;adgrpid=77500930054&amp;hvpone=&amp;hvptwo=&amp;hvadid=385609353592&amp;hvpos=&amp;hvnetw=g&amp;hvrand=3123620425578758399&amp;hvqmt=&amp;hvdev=c&amp;hvdvcmdl=&amp;hvlocint=&amp;hvlocphy=9011814&amp;hvtargid=pla-556939758690">Terraform Up and Running</a> book.</p>
<h1 id="heading-the-demo">The demo</h1>
<p>Here's a repository I used to create a demo to deploy to Azure.</p>
<p><a target="_blank" href="https://github.com/fgauna12/terragrunt-azure-example"><img src="https://gh-card.dev/repos/fgauna12/terragrunt-azure-example.svg?fullname=" alt="fgauna12/terragrunt-azure-example - GitHub" /></a></p>
<p><a target="_blank" href="https://github.com/fgauna12/terragrunt-azure-example">Here's a backup link</a> in case the fancy card stops working 👆🏽.</p>
<p>The directory structure looks like this.</p>
<pre><code class="lang-plaintext"> .
├── terragrunt.hcl
└── vs-enterprise 
    ├── staging 
    │   ├── eastus
    │   │   ├── region.hcl
    │   │   └── spoke-vnet
    │   │       └── terragrunt.hcl
    │   ├── env.hcl
    │   ├── global
    │   │   ├── region.hcl
    │   │   └── resource_groups
    │   │       ├── terragrunt.hcl
    │   └── westus
    │       ├── region.hcl
    │       └── spoke-vnet
    │           └── terragrunt.hcl
    └── subscription.hcl
</code></pre>
<p>This example is heavily inspired by the <a target="_blank" href="https://github.com/gruntwork-io/terragrunt-infrastructure-live-example">AWS terragrunt demo</a>.</p>
<p>So, if you want to follow along, <em>clone the repository</em>, then I'll walk you through customizing to your scenario.</p>
<h1 id="heading-okay-about-the-file-structure">Okay, about the file structure</h1>
<p>For me, the folder/file structure is one of the most appealing aspects of Terragrunt. I like that someone has put thought into and shared their experience on promoting Terraform across multiple environments for many large-scale applications.</p>
<ul>
<li><p><code>terragrunt.hcl</code> - We'll talk about this last</p>
</li>
<li><p><code>vs-enterprise</code> - A folder for my Visual Studio Enterprise subscription</p>
</li>
<li><p><code>vs-enterprise/staging</code> - A folder for my <code>staging</code> environment</p>
</li>
<li><p><code>vs-enterprise/staging/eastus</code> - A folder for resources in the East US region</p>
</li>
</ul>
<p>Let's walk through all the folders.</p>
<h2 id="heading-first-the-subscriptions">First, the subscriptions 🔑</h2>
<p>Some organizations have separate subscriptions per workload and per environment. Some organizations just have a "non-production" and a "production subscription."</p>
<p>Therefore, you will want to create as many folders as you have subscriptions. As you'll see later, this will help you switch subscriptions by simply changing the directory from which you run <code>terragrunt apply</code>. I like it because it is more obvious to see which subscription I am deploying to.</p>
<p>So, if you have a "non-production" and "production," then you're going to have these folders in your repository.</p>
<pre><code class="lang-plaintext">.
├── azsub-non-production
└── azsub-production
</code></pre>
<p>If you have a more complex subscription strategy, like subscription per app and environment, then you're going to have more folders.</p>
<pre><code class="lang-plaintext">.
├── azsub-fabrikam-app-dev
├── azsub-fabrikam-app-prod
└── azsub-fabrikam-app-test
</code></pre>
<p><strong>Please note:</strong> I pretend to use a fancy naming convention for my subscription. If you have messy subscription names, I think it's ok to have messy subscription folders.</p>
<p>Lastly, you'll want to create a <code>subscription.hcl</code> file in each subscription folder. In here, add the following contents. Make sure to add the <code>subscription_id</code> to each subscription file.</p>
<pre><code class="lang-plaintext"># vs-enterprise/subscription.hcl
locals {
    subscription_id = "&lt;&lt; your subscription id &gt;&gt;"
}
</code></pre>
<h2 id="heading-next-the-environment">Next, the environment 📦</h2>
<p>Like always, it depends on how many environments your applications will have. You could have just "staging" or "production" environments, or you could have "dev", "test", "prod."</p>
<p>I create a <code>shared</code> or <code>management</code> folder for resources that don't necessarily correspond to an environment. For example, you might have a Log Analytics workspace that's shared across <em>all</em> non-prod environments.</p>
<p>In my example, I am using a single environment - <strong>staging</strong>.</p>
<p>But then, in each environment folder, create an <code>env.hcl</code> file. Make sure to change the <code>environment</code> for each <code>env.hcl</code> file.</p>
<pre><code class="lang-plaintext"># vs-enterprise/staging/env.hcl
locals {
  environment = "staging"
}
</code></pre>
<h2 id="heading-the-region">The region 🌎</h2>
<p>Depending on your architecture, create a folder for each region. I make a <code>global</code> folder for resources that don't necessarily correspond to a region.</p>
<p>Examples of global resources are resource groups, Azure Front Door, Azure DNS, Azure Traffic Manager, Log Analytics workspace, etc. If you're a person that doesn't like to have resources from different regions inside of the same resource group, then you don't have to consider it a "global" resource.</p>
<p>For my example, if I am deploying to <em>multiple</em> regions, then my folders could be:</p>
<ul>
<li><p>East - A spoke virtual network</p>
</li>
<li><p>West - Another spoke virtual network</p>
</li>
<li><p>Global - Create the resource groups necessary for the virtual networks. More on dependencies later.</p>
</li>
</ul>
<p>In each region folder, create a <code>region.hcl</code> file.</p>
<pre><code class="lang-plaintext"># vs-enterprise/staging/eastus/region.hcl
locals {
  location = "eastus"
}
</code></pre>
<p>But what about the <code>global</code> folder? You'll still want to set a region. Most Azure global resources still require that you pick a region, for example, resource groups and Azure WAN. So, pick a "default" region for all of these global resources.</p>
<h2 id="heading-finally-the-terragrunt-file">Finally, the Terragrunt file 📄</h2>
<p>You will have to break up your system architecture into smaller pieces. This is one of the strengths of Terraform/Terragrunt. So, find "seams" in your architecture where you break things up into "modules." This takes some experience and practice and time.</p>
<p>Once you have the modules, you create the Terragrunt configuration. So, in my example, I will be deploying a virtual network.</p>
<p>So, I created a directory called <code>spoke-vnet</code> under the East and West US regions. Then, I created a <code>Terragrunt.hcl</code> file.</p>
<pre><code class="lang-plaintext"># vs-enterprise/staging/eastus/spoke-vnet/terragrunt.hcl
terraform {
  source = "tfr:///Azure/vnet/azurerm//?version=2.6.0"
}

include {
  path = find_in_parent_folders()
}

dependencies {
  paths = ["../../global/resource_groups"]
}

dependency "resource_groups" {
  config_path = "../../global/resource_groups"

  mock_outputs = {
    vnet_resource_group_name = "rg-terragrunt-mock-001"
  }
  mock_outputs_merge_with_state = true
}

locals {
  env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  environment = local.env_vars.locals.environment

  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))
  location = local.region_vars.locals.location
}

inputs = {
  vnet_name           = "vnet-spoke-${local.environment}-${local.location}-001"
  resource_group_name = dependency.resource_groups.outputs.vnet_resource_group_name
  address_space       = ["10.0.0.0/16"]
  subnet_prefixes     = ["10.0.1.0/26", "10.0.2.0/24", "10.0.3.0/24", "10.0.4.0/24"]
  subnet_names        = ["AzureBastionSubnet", "Management", "Tools", "Workloads"]
  location            = local.location

  tags = {
    environment = local.environment
  }
}
</code></pre>
<p>Then, I created another <code>Terragrunt.hcl</code> file in my <code>westus</code> folder. The contents are mostly the same with the exception of the address spaces (<code>inputs.address_space</code> and <code>inputs.subnet_prefixes</code>).</p>
<p>A couple of important things to call out:</p>
<ul>
<li><p><strong>The source</strong> - Under the <code>terraform</code> block, there's the opportunity to configure the source of the Terraform module. In my example, I use a public Terraform module to create a virtual network.</p>
</li>
<li><p><strong>The include</strong> - We're saying to include the <strong>first</strong> <code>terragrunt.hcl</code> file in the parent directories. Because our other Terragrunt files like <code>subscription.hcl</code> don't follow the default name, they will be ignored with this statement. Later, we'll create a <code>terragrunt.hcl</code> file at the repository's root. 😊 When we create a <code>terragrunt.hcl</code> file in the root of the repository, then this <code>include</code> statement will discover it.</p>
</li>
<li><p><strong>The inputs</strong> - The inputs are the input variables passed into the module. Notice how I am using <code>locals</code> to build some inputs.</p>
</li>
<li><p><strong>The locals</strong> - Much like proper Terraform, you can have locals. These variables are truly local to the file. <mark>You won't be able to consume </mark> <code>locals</code> defined in the parent directories like the <code>subscription.hcl</code> or <code>env.hcl</code>. However, you can <em>force</em> this behavior by loading them explicitly:</p>
</li>
</ul>
<pre><code class="lang-plaintext">locals {
    env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
    environment = local.env_vars.locals.environment
}
</code></pre>
<ul>
<li><strong>The dependencies</strong> - Remember how we never created a resource group for our virtual network? Well, we want to use Terraform to create the resource group as well. <mark>The problem is that we can't write proper Terraform inside of a Terragrunt configuration.</mark> We <em>could</em> create a <code>resource_group.tf</code> next to the <code>terragrunt.hcl</code> file and the resource group would be created, but there's no way to refer to the attributes of that resource or do a <code>depends_on</code> from Terragrunt file to Terraform configuration. Instead, the annoying part is that we'll have to create a separate module for just the resource groups to develop an inter-module dependency. Notice that there are two kinds of blocks: <code>dependencies</code> and <code>dependency</code> blocks The <code>dependencies</code> block is only required to specify the order of dependencies for when you use <code>terragrunt run-all</code>. The <code>dependency</code> block declares the actual dependency and refers to outputs from the depending module. For example, to get the name of the resource group that hasn't been created, we refer to the resource group module by calling <code>dependency.resource_groups.outputs.vnet_resource_group_name</code>.</li>
</ul>
<h2 id="heading-alright-fine-lets-create-the-resource-group-module">Alright, fine, let's create the <code>resource-group</code> module. 😬</h2>
<p>Create <code>resource-groups</code> folder. I placed mine under the <code>global</code> region. Here, you will define the resource groups that you want to create and deploy your infrastructure into.</p>
<p><strong>Note:</strong> This pattern of creating a module <mark>also applied to me when I wanted to use </mark> <code>data</code> blocks since you can't use <code>data</code> blocks in your Terragrunt configuration. In this example, I am not using data elements.</p>
<pre><code class="lang-plaintext"># vs-enterprise/staging/global/resource_groups/resource_group.tf
resource "azurerm_resource_group" "vnet_resource_group" {
  name     = var.vnet_resource_group_name
  location = var.location
}
</code></pre>
<p>Then you need some variables.</p>
<pre><code class="lang-plaintext"># vs-enterprise/staging/global/resource_groups/variables.tf
variable "location" {
  type  = string
  description = "The Azure Region to use"
}

variable vnet_resource_group_name {
  type        = string
  description = "The name of the vnet resource group"
}
</code></pre>
<p>And, some outputs so that you can create inter-module dependencies.</p>
<pre><code class="lang-plaintext"># vs-enterprise/staging/global/resource_groups/outputs.tf
output vnet_resource_group_name {
  value = var.vnet_resource_group_name
  depends_on = [azurerm_resource_group.vnet_resource_group]
}
</code></pre>
<p>Lastly, a Terragrunt configuration file.</p>
<pre><code class="lang-plaintext"># vs-enterprise/staging/global/resource_groups/terragrunt.hcl
include {
  path = find_in_parent_folders()
}

inputs = {
  vnet_resource_group_name = "rg-example-vnet"
}
</code></pre>
<h1 id="heading-dude-what-about-the-backend-and-the-provider">Dude, what about the backend? And... the provider?</h1>
<p>Yes, I know. We haven't talked about where the Terraform state will be stored. Bad news. This part is a little complicated.</p>
<p>To define a Terraform backend, we <em>could</em> go into each <code>terragrunt.hcl</code> file for each component, environment, subscription, and region. But, that would mean copying and pasting code. Instead, <mark>we could define the backend configuration in one place.</mark></p>
<p>As promised, remember this from our <code>terragrunt.hcl</code> file?</p>
<pre><code class="lang-plaintext"># vs-enterprise/staging/eastus/spoke-vnet/terragrunt.hcl
include {
  path = find_in_parent_folders()
}
</code></pre>
<p>Well, Terragrunt will be looking for another <code>terragrunt.hcl</code> file in the parent directories. So let's define it at the <em>root</em> of your repository.</p>
<pre><code class="lang-plaintext"># terragrunt.hcl
locals {
  # Automatically load subscription variables
  subscription_vars = read_terragrunt_config(find_in_parent_folders("subscription.hcl"))

  # Automatically load region-level variables
  region_vars = read_terragrunt_config(find_in_parent_folders("region.hcl"))

  # Automatically load environment-level variables
  environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))

  location              = local.region_vars.locals.location
  environment       = local.environment_vars.locals.environment
  subscription_id   = local.subscription_vars.locals.subscription_id
}

# Generate Azure providers
generate "versions" {
  path      = "versions_override.tf"
  if_exists = "overwrite_terragrunt"
  contents  = &lt;&lt;EOF
    terraform {
      required_providers {
        azurerm = {
          source = "hashicorp/azurerm"
          version = "2.95.0"
        }
      }
    }

    provider "azurerm" {
        features {}
        subscription_id = "${local.subscription_id}"
    }
EOF
}

remote_state {
    backend = "azurerm"
    config = {
        subscription_id = "${local.subscription_id}"
        key = "${path_relative_to_include()}/terraform.tfstate"
        resource_group_name = "rg-terragrunt-example-001"
        storage_account_name = "stterragruntexample001"
        container_name = "environment-states"
    }
    generate = {
        path      = "backend.tf"
        if_exists = "overwrite_terragrunt"
    }
}

# Configure root level variables that all resources can inherit. This is especially helpful with multi-subscription configs
# where terraform_remote_state data sources are placed directly into the modules.
inputs = merge(
  local.subscription_vars.locals,
  local.region_vars.locals,
  local.environment_vars.locals
)
</code></pre>
<p>Did you shed a tear? 😢 Yes, it's ugly and it looks complicated. I'll walk you through it.</p>
<ul>
<li><p><strong>The locals</strong> - The key to understanding the <code>locals</code> is the context where this <code>terragrunt.hcl</code> file will be run from. It <strong>always</strong> runs from the context of the leaf folder (the module folder). So, when we deploy our <code>spoke-vnet</code> module with Terragrunt, it will load the parent <code>terragrunt.hcl</code> file. Then, it will try to load <em>subscription</em> locals from a Terragrunt config file called <code>subscription.hcl</code> located in one of the parent directories.</p>
</li>
<li><p><strong>The generate block</strong> - Essentially, you will generate a block to specify the provider for all the leaf Terraform modules. This means one place to update the provider version! Notice how you can use <code>local</code> references to select which subscription you want to deploy to. Depending on which folder the "leaf folder" is located in, it's going to use a different subscription.</p>
</li>
<li><p><strong>The remote state</strong> - Here we tell Terragrunt to generate backend configuration and place it in the <code>backend.tf</code> file for each leaf folder.</p>
</li>
<li><p><strong>Input magic</strong> - Remember how we were adding <code>locals</code> blocks in the <code>subscription.hcl</code> and other non-default Terragrunt files? Essentially, we will be reading all of these local variables and merging them as inputs. So, let's say that <em>all</em> of your Terraform modules had an input variable of <code>location</code> for the Azure region. We won't have to specify <code>location = eastus</code> a million times with this input magic.</p>
</li>
</ul>
<p>Almost there! 🙏🏾 We just need a few things on the Azure side.</p>
<h1 id="heading-wait-some-prep-work">Wait ✋🏽, some prep work</h1>
<p>We defined a storage account for our backend and we haven't created it yet.</p>
<p>First, create a resource group for the Terraform backend.</p>
<ul>
<li><p>Name: <code>rg-terragrunt-example-001</code></p>
</li>
<li><p>Location: <code>eastus</code></p>
</li>
</ul>
<p>Next, create the storage account that will hold the Terraform backend. You will also need a storage account container.</p>
<ul>
<li><p>Name: <code>stterragruntexample001</code></p>
</li>
<li><p>Location: <code>eastus</code></p>
</li>
<li><p>Redunancy: <code>LRS</code></p>
</li>
<li><p>Container Name: <code>environment-states</code></p>
</li>
</ul>
<p>Also, remember that our code is creating the resource group for the virtual network? Well, if we do a <code>terragrunt run-all plan</code>, it will fail since no resource groups exist yet. So, let's create a "mock" resource group.</p>
<ul>
<li><p>Name: <code>rg-terragrunt-mock-001</code></p>
</li>
<li><p>Location: <code>eastus</code></p>
</li>
</ul>
<p>In reality, this "mocking" resource group will only be used when new infrastructure is being created for the first time. This is unique to Azure because, in AWS, resource groups are not required to create resources. This is also unique to Terragrunt for Azure because we can't add raw Terraform code in a Terragrunt configuration file. I guess the only way to avoid this issue is to create resource groups from <em>within</em> the modules themselves. But, creating resource groups within modules doesn't sound effective either since we would have less control over the <em>tags</em> at the resource group level.</p>
<p>Lastly, in that storage account, create a private container called <code>environment-states</code>. This is where the Terraform states will go. Don't worry; environments will be separated through sub-folders.</p>
<h1 id="heading-ready-to-run-it">Ready to run it?</h1>
<p>This part is so cool. Set the current directory of your shell to the region folder. Then run <code>terragrunt run-all plan</code>.</p>
<p>Terragrunt will find all the <code>terragrunt.hcl</code> files and then run <code>terragrunt plan</code> from each folder <em>(It won't work from the root of the repository)</em>.</p>
<p>Oh my...</p>
<pre><code class="lang-plaintext">❯ terragrunt run-all plan
INFO[0000] The stack at /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise will be processed in the following order for command plan:
Group 1
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/global/resource_groups

Group 2
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/eastus/spoke-vnet
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/westus/spoke-vnet


Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_resource_group.vnet_resource_group will be created
  + resource "azurerm_resource_group" "vnet_resource_group" {
      + id       = (known after apply)
      + location = "eastus"
      + name     = "rg-example-vnet"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + vnet_resource_group_name = "rg-example-vnet"

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_subnet.subnet[0] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.1.0/26",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "AzureBastionSubnet"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-westus-001"
    }

  # azurerm_subnet.subnet[1] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.2.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Management"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-westus-001"
    }

  # azurerm_subnet.subnet[2] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.3.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Tools"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-westus-001"
    }

  # azurerm_subnet.subnet[3] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.4.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Workloads"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-westus-001"
    }

  # azurerm_virtual_network.vnet will be created
  + resource "azurerm_virtual_network" "vnet" {
      + address_space         = [
          + "10.0.0.0/16",
        ]
      + dns_servers           = []
      + guid                  = (known after apply)
      + id                    = (known after apply)
      + location              = "eastus"
      + name                  = "vnet-spoke-staging-westus-001"
      + resource_group_name   = "rg-terragrunt-mock-001"
      + subnet                = (known after apply)
      + tags                  = {
          + "environment" = "staging"
        }
      + vm_protection_enabled = false
    }

Plan: 5 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + vnet_address_space = [
      + "10.0.0.0/16",
    ]
  + vnet_id            = (known after apply)
  + vnet_location      = "eastus"
  + vnet_name          = "vnet-spoke-staging-westus-001"
  + vnet_subnets       = [
      + (known after apply),
      + (known after apply),
      + (known after apply),
      + (known after apply),
    ]

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_subnet.subnet[0] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.1.0/26",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "AzureBastionSubnet"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-eastus-001"
    }

  # azurerm_subnet.subnet[1] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.2.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Management"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-eastus-001"
    }

  # azurerm_subnet.subnet[2] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.3.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Tools"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-eastus-001"
    }

  # azurerm_subnet.subnet[3] will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefix                                 = (known after apply)
      + address_prefixes                               = [
          + "10.0.4.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "Workloads"
      + resource_group_name                            = "rg-terragrunt-mock-001"
      + virtual_network_name                           = "vnet-spoke-staging-eastus-001"
    }

  # azurerm_virtual_network.vnet will be created
  + resource "azurerm_virtual_network" "vnet" {
      + address_space         = [
          + "10.0.0.0/16",
        ]
      + dns_servers           = []
      + guid                  = (known after apply)
      + id                    = (known after apply)
      + location              = "eastus"
      + name                  = "vnet-spoke-staging-eastus-001"
      + resource_group_name   = "rg-terragrunt-mock-001"
      + subnet                = (known after apply)
      + tags                  = {
          + "environment" = "staging"
        }
      + vm_protection_enabled = false
    }

Plan: 5 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + vnet_address_space = [
      + "10.0.0.0/16",
    ]
  + vnet_id            = (known after apply)
  + vnet_location      = "eastus"
  + vnet_name          = "vnet-spoke-staging-eastus-001"
  + vnet_subnets       = [
      + (known after apply),
      + (known after apply),
      + (known after apply),
      + (known after apply),
    ]

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
</code></pre>
<h2 id="heading-but-wait-should-you-use-run-all-all-the-time">But, wait, should you use <code>run-all</code> all the time?</h2>
<p>No, the Terragrunt team comments that <a target="_blank" href="https://github.com/gruntwork-io/terragrunt/issues/720#issuecomment-497888756">they originally designed this capability to stand up an entire infrastructure stack from scratch</a>. Once the infrastructure is up, they recommend using <code>terragrunt plan</code> and <code>apply</code> at the individual modules.</p>
<h1 id="heading-alright-do-it-apply">Alright, do it. Apply. 🤯</h1>
<p>I love this part. Run <code>terragrunt run-all apply -auto-approve</code>.</p>
<pre><code class="lang-plaintext">❯ terragrunt run-all apply -auto-approve
INFO[0000] The stack at /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise will be processed in the following order for command apply:
Group 1
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/global/resource_groups

Group 2
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/eastus/spoke-vnet
- Module /Users/facundo/Repos/terragrunt-azure-example/vs-enterprise/staging/westus/spoke-vnet

Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n) y
azurerm_resource_group.vnet_resource_group: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply":

  # azurerm_resource_group.vnet_resource_group has changed
  ~ resource "azurerm_resource_group" "vnet_resource_group" {
        id       = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet"
        name     = "rg-example-vnet"
      + tags     = {}
        # (1 unchanged attribute hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the
relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

─────────────────────────────────────────────────────────────────────────────

No changes. Your infrastructure matches the configuration.

Your configuration already matches the changes detected above. If you'd like
to update the Terraform state to match, create and apply a refresh-only plan:
  terraform apply -refresh-only

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

vnet_resource_group_name = "rg-example-vnet"
azurerm_virtual_network.vnet: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001]
azurerm_virtual_network.vnet: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001]
azurerm_subnet.subnet[3]: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Workloads]
azurerm_subnet.subnet[2]: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Tools]
azurerm_subnet.subnet[0]: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/AzureBastionSubnet]
azurerm_subnet.subnet[1]: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Management]
azurerm_subnet.subnet[3]: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Workloads]
azurerm_subnet.subnet[2]: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Tools]
azurerm_subnet.subnet[0]: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/AzureBastionSubnet]
azurerm_subnet.subnet[1]: Refreshing state... [id=/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Management]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply":

  # azurerm_subnet.subnet[0] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/AzureBastionSubnet"
        name                                           = "AzureBastionSubnet"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[1] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Management"
        name                                           = "Management"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[2] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Tools"
        name                                           = "Tools"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[3] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Workloads"
        name                                           = "Workloads"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_virtual_network.vnet has changed
  ~ resource "azurerm_virtual_network" "vnet" {
        id                      = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001"
        name                    = "vnet-spoke-staging-westus-001"
      ~ subnet                  = [
          + {
              + address_prefix = "10.1.1.0/26"
              + id             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/AzureBastionSubnet"
              + name           = "AzureBastionSubnet"
              + security_group = ""
            },
          + {
              + address_prefix = "10.1.2.0/24"
              + id             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Management"
              + name           = "Management"
              + security_group = ""
            },
          + {
              + address_prefix = "10.1.3.0/24"
              + id             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Tools"
              + name           = "Tools"
              + security_group = ""
            },
          + {
              + address_prefix = "10.1.4.0/24"
              + id             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Workloads"
              + name           = "Workloads"
              + security_group = ""
            },
        ]
        tags                    = {
            "environment" = "staging"
        }
        # (7 unchanged attributes hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the
relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

─────────────────────────────────────────────────────────────────────────────

No changes. Your infrastructure matches the configuration.

Your configuration already matches the changes detected above. If you'd like
to update the Terraform state to match, create and apply a refresh-only plan:
  terraform apply -refresh-only

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

vnet_address_space = tolist([
  "10.1.0.0/16",
])
vnet_id = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001"
vnet_location = "eastus"
vnet_name = "vnet-spoke-staging-westus-001"
vnet_subnets = [
  "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/AzureBastionSubnet",
  "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Management",
  "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Tools",
  "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-westus-001/subnets/Workloads",
]

Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply":

  # azurerm_subnet.subnet[0] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/AzureBastionSubnet"
        name                                           = "AzureBastionSubnet"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[1] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Management"
        name                                           = "Management"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[2] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Tools"
        name                                           = "Tools"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_subnet.subnet[3] has changed
  ~ resource "azurerm_subnet" "subnet" {
        id                                             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Workloads"
        name                                           = "Workloads"
      + service_endpoint_policy_ids                    = []
      + service_endpoints                              = []
        # (6 unchanged attributes hidden)
    }

  # azurerm_virtual_network.vnet has changed
  ~ resource "azurerm_virtual_network" "vnet" {
        id                      = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001"
        name                    = "vnet-spoke-staging-eastus-001"
      ~ subnet                  = [
          + {
              + address_prefix = "10.0.1.0/26"
              + id             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/AzureBastionSubnet"
              + name           = "AzureBastionSubnet"
              + security_group = ""
            },
          + {
              + address_prefix = "10.0.2.0/24"
              + id             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Management"
              + name           = "Management"
              + security_group = ""
            },
          + {
              + address_prefix = "10.0.3.0/24"
              + id             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Tools"
              + name           = "Tools"
              + security_group = ""
            },
          + {
              + address_prefix = "10.0.4.0/24"
              + id             = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Workloads"
              + name           = "Workloads"
              + security_group = ""
            },
        ]
        tags                    = {
            "environment" = "staging"
        }
        # (7 unchanged attributes hidden)
    }


Unless you have made equivalent changes to your configuration, or ignored the
relevant attributes using ignore_changes, the following plan may include
actions to undo or respond to these changes.

─────────────────────────────────────────────────────────────────────────────

No changes. Your infrastructure matches the configuration.

Your configuration already matches the changes detected above. If you'd like
to update the Terraform state to match, create and apply a refresh-only plan:
  terraform apply -refresh-only

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

vnet_address_space = tolist([
  "10.0.0.0/16",
])
vnet_id = "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001"
vnet_location = "eastus"
vnet_name = "vnet-spoke-staging-eastus-001"
vnet_subnets = [
  "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/AzureBastionSubnet",
  "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Management",
  "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Tools",
  "/subscriptions/&lt;&lt; redacted subscription id &gt;&gt;/resourceGroups/rg-example-vnet/providers/Microsoft.Network/virtualNetworks/vnet-spoke-staging-eastus-001/subnets/Workloads",
]
</code></pre>
<p>That's it! All resources were created. You'll notice that the output looks like someone iterated through all the folders and ran <code>terraform apply</code>.</p>
<h1 id="heading-summary">Summary 🙏🏾</h1>
<p>As I was learning Terragrunt, I wanted to like it. I am starting to, and I think I will end up liking it more. There were some quirks that are unique when using Azure. For example, AWS regions are king and therefore the AWS Terraform Provider has to be configured with the region. In Azure, subscriptions are king and it's wise to configure the Azure Terraform provider with the subscription ID. Also, Azure resource groups are mandatory to deploy resources and you have to make some weird decisions around <em>when</em> you create your resource groups and whether you pass them as inputs to your infrastructure modules. Lastly, don't forget that you might have to make a <code>data</code> module to query Azure AD for some <em>Object IDs</em>.</p>
<p>If you followed, this guide, I hope you found it useful. Leave a comment below, especially if you have some tips and tricks too. I would love to hear from you. 😊</p>
]]></content:encoded></item><item><title><![CDATA[You don't need secrets to deploy to Azure from GitHub Actions 🤯]]></title><description><![CDATA[I've been using GitHub a lot more lately for CI/CD. I have a background with Azure DevOps, so seeing the differences and similarities is fun. Because GitHub doesn't have a concept like "Azure DevOps Service Connections," the primary way I have been u...]]></description><link>https://gaunacode.com/you-dont-need-secrets-to-deploy-to-azure-from-github-actions</link><guid isPermaLink="true">https://gaunacode.com/you-dont-need-secrets-to-deploy-to-azure-from-github-actions</guid><category><![CDATA[GitHub]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Fri, 04 Feb 2022 15:34:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/JrZ1yE1PjQ0/upload/v1643984704035/DOQ85n3iW.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been using GitHub a lot more lately for CI/CD. I have a background with Azure DevOps, so seeing the differences and similarities is fun. Because GitHub doesn't have a concept like "Azure DevOps Service Connections," the primary way I have been using to deploy to Azure is through service principals with good ol' <em>Client ID</em> and <em>Client Secret</em>.  But, there's a <strong>better way</strong> - a way where you don't have to keep a <em>Client Secret</em>. Instead, you establish a <em>trust</em> relationship between your GitHub repository and an Azure Active Directory (AAD) application.  </p>
<h2 id="heading-background">Background</h2>
<p>One of the most straightforward ways to deploy Azure is through a service principal.</p>
<pre><code class="lang-bash">az login --service-principal -u &lt;aad app registration client id&gt; -p &lt;&lt;aad app client secret&gt;&gt; --tenant &lt;&lt;aad tenant id&gt;&gt;
<span class="hljs-comment"># now you can invoke az commands</span>
</code></pre>
<p>The problem with this is:</p>
<ul>
<li>You have to store that secret in your CI/CD system safely. Sure, you could integrate with KeyVault, but your mileage may vary depending on your CI/CD system.</li>
<li>Typically, client secrets expire. When creating the client secret, you can choose varying expiration timeframes, but you have to keep track of when these secrets expire. It's annoying when deployments stop working because the service principal has passed. And, if you don't choose an expiration, you're opening yourself up for a security incident.</li>
</ul>
<p>Alternatively, you can authenticate with a service principal using a <em>certificate</em>. </p>
<pre><code class="lang-bash">az login --service-principal -u &lt;aad app registration client id&gt; -p &lt;&lt;certificate&gt;&gt; --tenant &lt;&lt;aad tenant id&gt;&gt;
<span class="hljs-comment"># now you can invoke az commands</span>
</code></pre>
<p>The problem with this is:</p>
<ul>
<li>You have to use self-hosted runners. And, you have to install the certificates on those runners. And, certificates do expire, ugh. </li>
</ul>
<h2 id="heading-azure-active-directory-and-github-oidc-integration">Azure Active Directory and GitHub OIDC Integration!</h2>
<p>There's documentation on both the GitHub and the Microsoft side. I found the documentation on the Microsoft side to be a little more useful. </p>
<p>I used <a target="_blank" href="https://docs.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust-github?tabs=azure-portal">this walkthrough as described by Microsoft</a>.</p>
<h3 id="heading-the-app-registration-with-federation">The app registration with federation</h3>
<p>I won't re-document everything. From Azure AD, you create an app registration like you would normally do. Then instead of setting up the <em>client secret</em>, you set up a <em>federated credential</em>.</p>
<blockquote>
<p>Sign in to the Azure portal. Go to App registrations and open the app you want to configure.</p>
<p>Go to Certificates and secrets. In the <strong>Federated credentials</strong> tab, select <strong>Add credential.</strong> The <strong>Add</strong> a credential blade opens.</p>
<p>In the <strong>Federated credential scenario</strong> drop-down box select <strong>GitHub actions </strong>deploying Azure resources.</p>
<p>Specify the <strong>Organization</strong> and <strong>Repository</strong> for your GitHub Actions workflow.</p>
<p>For <strong>Entity type</strong>, select <strong>Environment</strong>, Branch, Pull request, or Tag and specify the value. The values must exactly match the configuration in the GitHub workflow. For more info, read the examples.</p>
<p>Add a <strong>Name</strong> for the federated credential.</p>
<p>The <strong>Issuer</strong>, <strong>Audiences</strong>, and <strong>Subject</strong> identifier fields autopopulate based on the values you entered.</p>
<p>Click <strong>Add</strong> to configure the federated credential.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643983891383/GYOsm77db.png" alt="2022-02-04_09-11-10.png" /></p>
<p>Here's my parameters:</p>
<ul>
<li><strong>Organization</strong> - <code>super-duper-github-oidc-pipelines-demo</code></li>
<li><strong>Repository</strong> - <code>secretless-amazing-pipelines-to-azure</code></li>
<li><strong>Entity type</strong> - <code>Environment</code></li>
<li><strong>GitHub environment name</strong> - <code>Sandbox</code></li>
<li><strong>Name</strong> - <code>sandbox_subscription</code></li>
</ul>
<p>💡 What's neat about this step is that the person performing the registration does not need access to GitHub at all! Great for those enterprise scenarios where you have to do a screen share with an AAD admin.</p>
<p>Next, grant this new app registration <strong>Reader</strong> role to the Azure subscription that you'll be using. In reality, you will probably need a different role based on what you're trying to automate. But, this does mean that you need to have <code>Owner</code> role of the subscription or <code>User Access Administrator</code>. If you're neither of these things, you'll need someone else to help you. If you need help with this, <a target="_blank" href="https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=current">here's the docs</a>.</p>
<p>Now, to the GitHub side of things.</p>
<h3 id="heading-the-github-side-of-things">The GitHub side of things</h3>
<p>Under my repository, I configured an environment called Sandbox. To add a new environment you go to Repository <strong>Settings</strong>, <strong>Environments</strong>, then add the new environment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643981926417/eEfMbvhKw.png" alt="2022-02-04_08-37-34.png" /></p>
<p>Next, we'll create a GitHub action that leverages the OIDC capability. To verify that we can login to Azure, we'll add a step to list all the resource groups in the subscription.</p>
<p>So, start with a simple workflow.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643982041111/cCnJNHx3iU.png" alt="2022-02-04_08-40-33.png" /></p>
<p>Now replace the contents with the following pieces. </p>
<p>The next bits of the process has been derived from the <a target="_blank" href="https://github.com/marketplace/actions/azure-login#sample-workflow-that-uses-azure-login-action-using-oidc-to-run-az-cli-linux">GitHub Action for Azure Login README</a>. </p>
<p>First, you need to grant permissions to the action to read the OIDC ID Token from Azure AD. Add a permissions block at the workflow level (at the same indentation level as the name of the workflow).</p>
<pre><code class="lang-yaml"><span class="hljs-attr">permissions:</span>
  <span class="hljs-attr">id-token:</span> <span class="hljs-string">write</span>
  <span class="hljs-attr">contents:</span> <span class="hljs-string">read</span>
</code></pre>
<p>And, you'll want to create a simple job that deploys to the <code>Sandbox</code> environment. </p>
<pre><code class="lang-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">login:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">environment:</span> <span class="hljs-string">Sandbox</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
</code></pre>
<p>Next, you'll want a step your action to login to Azure. Notice how it doesn't use a secret. 🙃</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">'Az CLI login'</span>
   <span class="hljs-attr">uses:</span> <span class="hljs-string">azure/login@v1</span>
   <span class="hljs-attr">with:</span>
     <span class="hljs-attr">client-id:</span> <span class="hljs-string">&lt;&lt;your</span> <span class="hljs-string">client</span> <span class="hljs-string">id</span> <span class="hljs-string">for</span> <span class="hljs-string">the</span> <span class="hljs-string">app</span> <span class="hljs-string">registration</span> <span class="hljs-string">you</span> <span class="hljs-string">created</span> <span class="hljs-string">above&gt;&gt;</span>
     <span class="hljs-attr">tenant-id:</span> <span class="hljs-string">&lt;&lt;the</span> <span class="hljs-string">tenant</span> <span class="hljs-string">id</span> <span class="hljs-string">for</span> <span class="hljs-string">aad&gt;&gt;</span>
     <span class="hljs-attr">subscription-id:</span> <span class="hljs-string">&lt;&lt;the</span> <span class="hljs-string">subscription</span> <span class="hljs-string">id&gt;&gt;</span>
</code></pre>
<p>Then, we'll add a step to run a test command to verify that we can authenticate.</p>
<pre><code class="lang-yaml"><span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Test</span> <span class="hljs-string">Command</span> <span class="hljs-number">1</span>
   <span class="hljs-attr">shell:</span> <span class="hljs-string">bash</span>
   <span class="hljs-attr">run:</span> 
     <span class="hljs-string">az</span> <span class="hljs-string">group</span> <span class="hljs-string">list</span> <span class="hljs-string">-o</span> <span class="hljs-string">table</span>
</code></pre>
<h3 id="heading-putting-it-all-together">Putting it all together</h3>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Login</span> <span class="hljs-string">to</span> <span class="hljs-string">Azure</span>

<span class="hljs-attr">permissions:</span>
  <span class="hljs-attr">id-token:</span> <span class="hljs-string">write</span>
  <span class="hljs-attr">contents:</span> <span class="hljs-string">read</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span> [ <span class="hljs-string">main</span> ]

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">login:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>

    <span class="hljs-attr">environment:</span> <span class="hljs-string">Sandbox</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">'Az CLI login'</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">azure/login@v1</span>
        <span class="hljs-attr">with:</span>
          <span class="hljs-attr">client-id:</span> <span class="hljs-string">&lt;&lt;your</span> <span class="hljs-string">client</span> <span class="hljs-string">id</span> <span class="hljs-string">for</span> <span class="hljs-string">the</span> <span class="hljs-string">app</span> <span class="hljs-string">registration</span> <span class="hljs-string">you</span> <span class="hljs-string">created</span> <span class="hljs-string">above&gt;&gt;</span>
          <span class="hljs-attr">tenant-id:</span> <span class="hljs-string">&lt;&lt;the</span> <span class="hljs-string">tenant</span> <span class="hljs-string">id</span> <span class="hljs-string">for</span> <span class="hljs-string">aad&gt;&gt;</span>
          <span class="hljs-attr">subscription-id:</span> <span class="hljs-string">&lt;&lt;the</span> <span class="hljs-string">subscription</span> <span class="hljs-string">id&gt;&gt;</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Test</span> <span class="hljs-string">Command</span> <span class="hljs-number">1</span>
        <span class="hljs-attr">shell:</span> <span class="hljs-string">bash</span>
        <span class="hljs-attr">run:</span> 
          <span class="hljs-string">az</span> <span class="hljs-string">group</span> <span class="hljs-string">list</span> <span class="hljs-string">-o</span> <span class="hljs-string">table</span>
</code></pre>
<p>Then, notice how your mind melts with awe. 🤯</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643984596541/NqqsyxH1s.png" alt="2022-02-04_09-22-44.png" /></p>
]]></content:encoded></item><item><title><![CDATA[Install ArgoCD on an AKS cluster with NGINX]]></title><description><![CDATA[In this post, I’ll walk through the essentials of configuring an AKS cluster to use Argo CD with an NGINX Ingress Controller.
Pre-Requisites
You’ll need:

An AKS cluster. Here’s a Gist I use to create a simple cluster.
Helm 3
Azure CLI
A Custom DNS/A...]]></description><link>https://gaunacode.com/install-argocd-on-an-aks-cluster-with-nginx</link><guid isPermaLink="true">https://gaunacode.com/install-argocd-on-an-aks-cluster-with-nginx</guid><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[gitops]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Mon, 22 Nov 2021 20:45:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639575252/xH7g9vrza.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, I’ll walk through the essentials of configuring an AKS cluster to use Argo CD with an NGINX Ingress Controller.</p>
<h2 id="heading-pre-requisites">Pre-Requisites</h2>
<p>You’ll need:</p>
<ul>
<li>An AKS cluster. Here’s a <a target="_blank" href="https://gist.github.com/fgauna12/a87ee3c4ec0726a186dd32ad25c56daf">Gist</a> I use to create a simple cluster.</li>
<li>Helm 3</li>
<li>Azure CLI</li>
<li>A Custom DNS/Access to the provider (I will be using Azure DNS)</li>
</ul>
<h2 id="heading-nginx-ingress-controller-with-lets-encrypt">NGINX Ingress Controller with Let’s Encrypt</h2>
<p>I was inspired by <a target="_blank" href="https://docs.microsoft.com/en-us/azure/aks/ingress-tls">this</a> page on the Microsoft docs. We’re going to install the NGINX ingress controller with Let’s Encrypt.</p>
<p><strong>Note:</strong> To simplify things, I will use the NGINX image from Docker Hub. The Microsoft Docs walk you through importing the NGINX image into your own container registry, and… for good reason.</p>
<h3 id="heading-install-nginx-ingress-controller">Install NGINX Ingress Controller</h3>
<p>First, add the NGINX Helm Repository.</p>
<pre><code>helm repo <span class="hljs-keyword">add</span> ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo <span class="hljs-keyword">update</span>
</code></pre><p>Then install the Helm chart,</p>
<pre><code>helm install nginx<span class="hljs-operator">-</span>ingress ingress<span class="hljs-operator">-</span>nginx<span class="hljs-operator">/</span>ingress<span class="hljs-operator">-</span>nginx \
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>namespace ingress <span class="hljs-operator">-</span><span class="hljs-operator">-</span>create<span class="hljs-operator">-</span>namespace\
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>set controller.replicaCount=<span class="hljs-number">2</span> \
    <span class="hljs-operator">-</span><span class="hljs-operator">-</span>set controller.nodeSelector."kubernetes\.io/os<span class="hljs-string">"=linux \
    --set controller.admissionWebhooks.patch.nodeSelector."</span>kubernetes\.io/os<span class="hljs-string">"=linux \
    --set defaultBackend.nodeSelector."</span>kubernetes\.io/os<span class="hljs-string">"=linux</span>
</code></pre><h3 id="heading-install-cert-manager">Install Cert Manager</h3>
<p>Now, install the <code>cert-manager</code> Helm chart. <strong>Notice:</strong> you will have to add the Jetstack helm repository first.</p>
<pre><code>CERT_MANAGER_TAG=v1.3.1

<span class="hljs-comment"># Label the ingress-basic namespace to disable resource validation</span>
kubectl label namespace ingress cert-manager.io/disable-validation=<span class="hljs-literal">true</span>

<span class="hljs-comment"># Add the Jetstack Helm repository</span>
helm repo add jetstack https://charts.jetstack.io

<span class="hljs-comment"># Update your local Helm chart repository cache</span>
helm repo update

<span class="hljs-comment"># Install the cert-manager Helm chart</span>
helm install cert-manager jetstack/cert-manager \
  --namespace ingress \
  --version <span class="hljs-variable">$CERT_MANAGER_TAG</span> \
  --<span class="hljs-built_in">set</span> installCRDs=<span class="hljs-literal">true</span> \
  --<span class="hljs-built_in">set</span> nodeSelector.<span class="hljs-string">"kubernetes\.io/os"</span>=linux
</code></pre><p>And lastly, create a CA cluster issuer. This will tell <code>cert-manager</code> to issue certificates for certificate requests across the cluster.</p>
<pre><code><span class="hljs-string">cat</span> <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">EOF</span> <span class="hljs-string">|</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-bullet">-</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">cert-manager.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ClusterIssuer</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">letsencrypt</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">acme:</span>
    <span class="hljs-attr">server:</span> <span class="hljs-string">https://acme-v02.api.letsencrypt.org/directory</span>
    <span class="hljs-attr">email:</span> <span class="hljs-string">you@mydomain.com</span>
    <span class="hljs-attr">privateKeySecretRef:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">letsencrypt</span>
    <span class="hljs-attr">solvers:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">http01:</span>
        <span class="hljs-attr">ingress:</span>
          <span class="hljs-attr">class:</span> <span class="hljs-string">nginx</span>
          <span class="hljs-attr">podTemplate:</span>
            <span class="hljs-attr">spec:</span>
              <span class="hljs-attr">nodeSelector:</span>
                <span class="hljs-attr">"kubernetes.io/os":</span> <span class="hljs-string">linux</span>
<span class="hljs-string">EOF</span>
</code></pre><h2 id="heading-install-argo-cd">Install Argo CD</h2>
<p>Now, for the good part, let’s install Argo CD with a TLS ingress.</p>
<pre><code>kubectl create namespace argocd

kubectl apply -n argocd -f <span class="hljs-symbol">https:</span>/<span class="hljs-regexp">/raw.githubusercontent.com/argoproj</span><span class="hljs-regexp">/argo-cd/stable</span><span class="hljs-regexp">/manifests/install</span>.yaml
</code></pre><p>For example: <code>argocd.yourdomain.com</code> You can get the Public IP of the NGINX Ingress controller by:</p>
<pre><code>$ kubectl get svc <span class="hljs-operator">-</span>n ingress
NAME                                               TYPE           CLUSTER<span class="hljs-operator">-</span>IP   EXTERNAL
nginx<span class="hljs-operator">-</span>ingress<span class="hljs-operator">-</span>ingress<span class="hljs-operator">-</span>nginx<span class="hljs-operator">-</span>controller             LoadBalancer   <span class="hljs-number">10.1</span><span class="hljs-number">.0</span><span class="hljs-number">.88</span>    <span class="hljs-number">20.120</span><span class="hljs-number">.123</span><span class="hljs-number">.218</span>   <span class="hljs-number">80</span>:<span class="hljs-number">30101</span><span class="hljs-operator">/</span>TCP,<span class="hljs-number">443</span>:<span class="hljs-number">31968</span><span class="hljs-operator">/</span>TCP   5m13s         <span class="hljs-number">443</span><span class="hljs-operator">/</span>TCP                      5m13s
</code></pre><p>Next, you’re going to deploy the Ingress rules to be able to reach ArgoCD’s UI using HTTPS. Make sure you add an <code>A</code> record on your DNS to the ingress controller.</p>
<p><strong>Note:</strong> Change the code snippet below to include your custom domain.</p>
<pre><code><span class="hljs-string">cat</span> <span class="hljs-string">&lt;&lt;</span> <span class="hljs-string">EOF</span> <span class="hljs-string">|</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-bullet">-</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">argocd-server-ingress</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">argocd</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">cert-manager.io/cluster-issuer:</span> <span class="hljs-string">letsencrypt</span>
    <span class="hljs-attr">kubernetes.io/ingress.class:</span> <span class="hljs-string">nginx</span>
    <span class="hljs-attr">kubernetes.io/tls-acme:</span> <span class="hljs-string">"true"</span>
    <span class="hljs-attr">nginx.ingress.kubernetes.io/ssl-passthrough:</span> <span class="hljs-string">"true"</span>
    <span class="hljs-comment"># If you encounter a redirect loop or are getting a 307 response code</span>
    <span class="hljs-comment"># then you need to force the nginx ingress to connect to the backend using HTTPS.</span>
    <span class="hljs-comment">#</span>
    <span class="hljs-attr">nginx.ingress.kubernetes.io/backend-protocol:</span> <span class="hljs-string">"HTTPS"</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">argocd.mydomain.com</span>
    <span class="hljs-attr">http:</span>
      <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">pathType:</span> <span class="hljs-string">Prefix</span>
        <span class="hljs-attr">backend:</span>
          <span class="hljs-attr">service:</span>
            <span class="hljs-attr">name:</span> <span class="hljs-string">argocd-server</span>
            <span class="hljs-attr">port:</span>
              <span class="hljs-attr">name:</span> <span class="hljs-string">https</span>
  <span class="hljs-attr">tls:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">hosts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">argocd.mydomain.com</span>
    <span class="hljs-attr">secretName:</span> <span class="hljs-string">argocd-secret</span> <span class="hljs-comment"># do not change, this is provided by Argo CD</span>
<span class="hljs-string">EOF</span>
</code></pre><p>Open your browser with your custom address for Argo. For example: <code>https://argocd.mydomain.com</code>. <strong>Note:</strong> it can take about 5 minutes for the Let’s Encrypt certificate to be assigned.</p>
<p>Congrats 🎉</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639570311/REchFqwL3.png" alt="Argo Login Example" /></p>
<p>To login, get the initial password via a Kubernetes secret. The username is <code>admin</code></p>
<pre><code>kubectl <span class="hljs-operator">-</span>n argocd get secret argocd<span class="hljs-operator">-</span>initial<span class="hljs-operator">-</span>admin<span class="hljs-operator">-</span>secret <span class="hljs-operator">-</span>o jsonpath<span class="hljs-operator">=</span><span class="hljs-string">"{.data.password}"</span> <span class="hljs-operator">|</span> base64 <span class="hljs-operator">-</span>d
</code></pre><p>Login to Argo. Then change the default password.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639572949/cFtMlCItm.gif" alt="To change password, first select &quot;User Info&quot;, then &quot;Update Password&quot;." /></p>
<p>I hope that helped! 🎊</p>
]]></content:encoded></item><item><title><![CDATA[Deploying an ingress controller to an internal virtual network and fronted by an Azure Application Gateway with WAF]]></title><description><![CDATA[There are many ways to add a Web Application Firewall (WAF) in front of applications hosted on Azure Kubernetes Service (AKS). In this post, we’ll cover how to set-up an NGINX ingress controller on AKS, then create an Azure Application Gateway to fro...]]></description><link>https://gaunacode.com/deploying-an-ingress-controller-to-an-internal-virtual-network-and-fronted-by-an-azure-application-gateway-with-waf</link><guid isPermaLink="true">https://gaunacode.com/deploying-an-ingress-controller-to-an-internal-virtual-network-and-fronted-by-an-azure-application-gateway-with-waf</guid><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Wed, 24 Feb 2021 17:48:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639590019/MYH53wGKz.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are many ways to add a Web Application Firewall (WAF) in front of applications hosted on Azure Kubernetes Service (AKS). In this post, we’ll cover how to set-up an NGINX ingress controller on AKS, then create an Azure Application Gateway to front the traffic from a Public IP, terminate TLS, then forward traffic to the NGINX ingress controller listening on a Private IP, unencrypted.</p>
<h2 id="heading-pre-requisites">Pre-Requisites</h2>
<ul>
<li><p>An AKS cluster using Azure CNI as the network plugin</p>
</li>
<li><p>A virtual network with two subnets <code>asg</code> and <code>aks</code>. The <code>asg</code> subnet will hold the Application Gateway, and the <code>aks</code> subnet will hold the AKS cluster.</p>
</li>
<li><p>A resource group, in this case <code>rg-appg-ingress-test</code></p>
</li>
<li><p>Your AKS cluster's identity will need <strong>Network Contributor</strong> role scoped to the virtual network</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639578946/18uGudgJ5.png" alt /></p>
<h2 id="heading-deploying-nginx-ingress-controller-with-a-private-ip">Deploying NGINX ingress controller with a private IP</h2>
<p>During this section, we’ll borrow instructions from this <a target="_blank" href="https://docs.microsoft.com/en-us/azure/aks/ingress-internal-ip">MSFT docs page</a>.</p>
<p>First, we need a private IP address that the NGINX ingress controller will accept requests from. So, choose a private IP address and verify that it’s available. In this case, the IP address I chose is <code>10.0.0.100</code>.</p>
<pre><code class="lang-bash">az network vnet check-ip-address --name vnet-ingress-test -g rg-appg-ingress-test --ip-address 10.0.0.100
</code></pre>
<p>Create the following file named <code>internal-ingress.yaml</code> with the chosen IP address.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">controller:</span> 
  <span class="hljs-attr">service:</span> 
    <span class="hljs-attr">loadBalancerIP:</span> <span class="hljs-number">10.0</span><span class="hljs-number">.0</span><span class="hljs-number">.100</span> 
    <span class="hljs-attr">annotations:</span> 
      <span class="hljs-attr">service.beta.kubernetes.io/azure-load-balancer-internal:</span> <span class="hljs-string">"true"</span>
</code></pre>
<p>This will be used to configure the NGINX Helm chart to use the given IP address for the ingress controller.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create a namespace for your ingress resources </span>
kubectl create namespace ingress

<span class="hljs-comment"># Add the ingress-nginx repository </span>
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

<span class="hljs-comment"># Use Helm to deploy an NGINX ingress controller </span>
helm install nginx-ingress ingress-nginx/ingress-nginx \
  --namespace ingress \
  -f internal-ingress.yaml \
  --<span class="hljs-built_in">set</span> controller.replicaCount=2 \
  --<span class="hljs-built_in">set</span> controller.nodeSelector.<span class="hljs-string">"kubernetes.io/os"</span>=linux \
  --<span class="hljs-built_in">set</span> defaultBackend.nodeSelector.<span class="hljs-string">"kubernetes.io/os"</span>=linux \
  --<span class="hljs-built_in">set</span> controller.admissionWebhooks.patch.nodeSelector.<span class="hljs-string">"kubernetes.io/os"</span>=linux
</code></pre>
<p>It may take a few minutes for the IP address to be assigned.</p>
<pre><code class="lang-plaintext">$ kubectl --namespace ingress get services -o wide -w nginx-ingress-ingress-nginx-controller 
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR 
nginx-ingress-ingress-nginx-controller LoadBalancer 10.0.75.105 80:31026/TCP,443:30254/TCP 2m8s app.kubernetes.io/component=controller,app.kubernetes.io/instance=nginx-ingress,app.kubernetes.io/name=ingress-nginx
</code></pre>
<p>If it's stuck in <code>&lt;pending&gt;</code>, then likely, the Ingress controller can't reserve the static IP. Therefore, you're most likely missing the <strong>Network Contributor</strong> role on the AKS cluster's identity.</p>
<p>You can verify this by looking at the events for the service. <code>bash kubectl --namespace ingress get services -o wide -w nginx-ingress-ingress-nginx-controller</code></p>
<h2 id="heading-deploying-two-test-applications">Deploying two test applications</h2>
<p>Let’s create a namespace for the sample apps.</p>
<pre><code class="lang-bash">kubectl create ns ingress-test
</code></pre>
<p>Deploy the first sample application.</p>
<pre><code class="lang-bash">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: aks-helloworld
  namespace: ingress-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: aks-helloworld
  template:
    metadata:
      labels:
        app: aks-helloworld
    spec:
      containers:
      - name: aks-helloworld
        image: mcr.microsoft.com/azuredocs/aks-helloworld:v1
        ports:
        - containerPort: 80
        env:
        - name: TITLE
          value: <span class="hljs-string">"Welcome to Azure Kubernetes Service (AKS)"</span>
---
apiVersion: v1
kind: Service
metadata:
  name: aks-helloworld
  namespace: ingress-test
spec:
  <span class="hljs-built_in">type</span>: ClusterIP
  ports:
  - port: 80
  selector:
    app: aks-helloworld
EOF
</code></pre>
<p>Now, deploy the second test application in the same namespace.</p>
<pre><code class="lang-bash">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-demo
  namespace: ingress-test
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ingress-demo
  template:
    metadata:
      labels:
        app: ingress-demo
    spec:
      containers:
      - name: ingress-demo
        image: mcr.microsoft.com/azuredocs/aks-helloworld:v1
        ports:
        - containerPort: 80
        env:
        - name: TITLE
          value: <span class="hljs-string">"AKS Ingress Demo"</span>
---
apiVersion: v1
kind: Service
metadata:
  name: ingress-demo
  namespace: ingress-test
spec:
  <span class="hljs-built_in">type</span>: ClusterIP
  ports:
  - port: 80
  selector:
    app: ingress-demo
EOF
</code></pre>
<p>Okay, create the ingress definition to verify part of the ingress controller. This will create an ingress rule to map requests for that domain to the appropriate service in Kubernetes.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> DOMAIN_NAME=<span class="hljs-string">"mysub.mydomain.com"</span>
</code></pre>
<pre><code class="lang-bash">cat &lt;&lt;EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-world-ingress
  namespace: ingress-test
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: <span class="hljs-string">"false"</span>
    nginx.ingress.kubernetes.io/use-regex: <span class="hljs-string">"true"</span>
    nginx.ingress.kubernetes.io/rewrite-target: /<span class="hljs-variable">$1</span>
spec:
  ingressClassName: nginx
  defaultBackend:
    service:
      name: aks-helloworld
      port:
        number: 80
  rules:
  - host: <span class="hljs-variable">$DOMAIN_NAME</span>
    http:
      paths:
      - backend:
          service:
            name: aks-helloworld
            port:
              number: 80
        path: /hello-world-one
        pathType: Prefix
      - backend:
          service:
            name: ingress-demo
            port:
              number: 80
        path: /hello-world-two
        pathType: Prefix
EOF
</code></pre>
<p><strong>Notice:</strong> how there’s an annotation of <code>nginx.ingress.kubernetes.io/ssl-redirect: "false"</code>. This will ensure that if we ever assign a TLS certificate to the ingress definition, NGINX won't start re-routing http traffic to https. This is important for us since we are using an Application Gateway that will terminate SSL and we want this behavior to be enforced by the Application Gateway, not the ingress controller. Otherwise, it might mess up with the health probes from the Application Gateway to the Kubernetes cluster.</p>
<h2 id="heading-testing-the-ingress-controller-from-a-test-container">Testing the ingress controller from a test container</h2>
<p>Now, let’s launch a pod to validate the configuration.</p>
<pre><code class="lang-bash">kubectl run -it --rm aks-ingress-test --image=debian --namespace ingress-test
</code></pre>
<p>You will be inside the container at this point. Then install curl, we’re going to use <code>curl</code> against our ingress controller.</p>
<pre><code class="lang-bash">apt-get update &amp;&amp; apt-get install -y curl
</code></pre>
<p>Once <code>curl</code> is installed on the container, then let’s make sure that our ingress is working.</p>
<pre><code class="lang-bash">curl -L -H <span class="hljs-string">"Host: ingress.gaunacode.com"</span> http://10.0.0.100
</code></pre>
<p>Notice how we’re changing the <code>Host</code> header to ensure our ingress route is used. We haven’t created an external DNS record or configured the application gateway yet.</p>
<p>If everything works well, you should see the HTML with “Welcome to Kubernetes”. That’s one of our sample applications.</p>
<p>root@aks-ingress-test:/# curl -L -H "Host: ingress.gaunacode.com" http://10.0.0.100</p>
<p>&lt;form id="form" name="form" action="/"" method="post"&gt; Welcome to Azure Kubernetes Service (AKS)</p>
<p><img src="/static/acs.png" alt /></p>
<h2 id="heading-preparing-an-application-gateway">Preparing an Application Gateway</h2>
<p>If you don’t have one, here’s how to create a <code>Standard_v2</code> application gateway. It pre-configures some aspects like the backend. It uses the private IP of the ingress controller.</p>
<pre><code class="lang-bash">az network public-ip create -g rg-appg-ingress-test -l eastus -n pip-appg-ingress-test --sku Standard
</code></pre>
<pre><code class="lang-bash">az network application-gateway create --name azappg-appg-ingress-test -g rg-appg-ingress-test -l eastus --sku Standard_v2 --public-ip-address pip-appg-ingress-test --vnet-name vnet-ingress-test --subnet appg --servers 10.0.0.100
</code></pre>
<h2 id="heading-configuring-the-application-gateway-manually">Configuring the Application Gateway (Manually)</h2>
<p>Next, grab the public IP of the application gateway and create an external DNS record.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639580770/n69w7CYmB.png" alt="Where to find the Public IP on the App Gateway from the portal" /></p>
<p>In my case,</p>
<blockquote>
<p>[A record @ <a target="_blank" href="http://ingress.gaunacode.com">ingress.gaunacode.com</a>] -&gt; 20.72.158.202</p>
</blockquote>
<p>Modify the default HTTP settings and override the hostname. This will ensure that our ingress rule is used in the NGINX ingress controller.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639582403/MrnkNp2zz.png" alt="Creating an HTTP listener on the app gateway" /></p>
<p>Verify that ingress works. I’m my case, <code>http://ingress.gaunacode.com</code>. HTTPS should not work yet.</p>
<p>Create an HTTPS listener. I am using a trial certificate for the <code>ingress.gaunacode.com</code> domain.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639583776/ILjfo2vXe.html" alt="Creating an HTTPS listener on the app gateway" /></p>
<p>Next, create a “Rule” to tie the HTTPS listener to the backend.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639585101/u_0Z9XWUD.png" alt="Creating an App Gateway Rule for HTTPS - Part 1" /></p>
<p>Then the “Backend Targets”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639586755/eyaLk3mng.png" alt="App Gateway Rule 2" /></p>
<p>Once the “Rule” is created, then the App Gateway should accept traffic from the public IP, through the HTTP listener, tied to a “backend” using the Rule and <code>http</code> settings. The App Gateway creates a new connection to the NGINX ingress controller through a private static IP and overriding the “hostname” so that the Ingress rule kicks in.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639588638/aLg_CdL4i.png" alt="Browser showing it works" /></p>
<p>That’s it! Hope that helped.</p>
]]></content:encoded></item><item><title><![CDATA[Migrating apps from one AKS cluster to another using Velero]]></title><description><![CDATA[Assuming you already set-up Velero on your primary cluster, you can restore all the configuration and applications in a similar cluster using Velero. This can be really useful for scenarios where you want to test dangerous changes without impacting a...]]></description><link>https://gaunacode.com/migrating-apps-from-one-aks-cluster-to-another-using-velero</link><guid isPermaLink="true">https://gaunacode.com/migrating-apps-from-one-aks-cluster-to-another-using-velero</guid><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Fri, 29 Jan 2021 20:24:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639593148/UO97DvhX8U.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Assuming you already set-up Velero on your primary cluster, you can restore all the configuration and applications in a similar cluster using Velero. This can be really useful for scenarios where you want to test dangerous changes without impacting app dev teams using a cluster.</p>
<p>I recently had this scenario where I wanted to experiment with far fetched ideas on a cluster. However, I needed to use all the existing configurations. Because I was not using GitOps or extensive re-hydration CI/CD pipelines, I was able to use Velero to restore my Kubernetes configuration onto a new 1-node “sandbox” cluster.</p>
<h2 id="heading-install-velero-on-new-cluster">Install Velero on new cluster</h2>
<p>Assuming you already configured Velero on your main cluster, <a target="_blank" href="https://gaunacode.com/aks-best-practice-backing-up-aks-with-velero">like this</a>, then start by creating a namespace.</p>
<pre><code>kubectl <span class="hljs-keyword">create</span> ns velero
</code></pre><p>Then from your existing cluster, get the <code>velero-credentials</code> secret holding the connection information to your Azure storage account. For example,</p>
<pre><code># Against the main <span class="hljs-keyword">cluster</span> (<span class="hljs-keyword">cluster</span> #<span class="hljs-number">1</span>)
kubectl <span class="hljs-keyword">get</span> secret -o yaml velero-credentials -n velero &gt; velero-credentials.yaml
</code></pre><p>Then switch to the new cluster and place it back.</p>
<pre><code># Against the <span class="hljs-built_in">new</span> <span class="hljs-keyword">cluster</span> (<span class="hljs-keyword">cluster</span> #<span class="hljs-number">2</span>)
kubectl apply -f velero-credentials.yaml   
rm velero-credentials.yaml 
</code></pre><p>Then install Velero on the new cluster. I used the same Helm command I used on the main cluster (cluster #1) some while back.</p>
<pre><code><span class="hljs-comment"># Against new cluster (cluster #2)</span>
<span class="hljs-built_in">export</span> AZURE_SUBSCRIPTION_ID=<span class="hljs-string">""</span> <span class="hljs-comment"># to get current sub: az account show --query "id" -o tsv</span>
<span class="hljs-built_in">export</span> STORAGE_RESOURCE_GROUP=<span class="hljs-string">""</span> 
<span class="hljs-built_in">export</span> STORAGE_ACCOUNT=<span class="hljs-string">""</span>
<span class="hljs-built_in">export</span> STORAGE_CONTAINER_NAME=<span class="hljs-string">""</span>

helm install velero vmware-tanzu/velero --namespace velero --version 2.13.2 \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].image=velero/velero-plugin-for-microsoft-azure:v1.1.0"</span> \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].imagePullPolicy=IfNotPresent"</span> \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].volumeMounts[0].mountPath=/target"</span> \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].volumeMounts[0].name=plugins"</span> \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].name=velero-plugin-for-azure"</span> \
--<span class="hljs-built_in">set</span> credentials.existingSecret=<span class="hljs-string">'velero-credentials'</span> \
--<span class="hljs-built_in">set</span> configuration.provider=<span class="hljs-string">'azure'</span> \
--<span class="hljs-built_in">set</span> configuration.backupStorageLocation.bucket=<span class="hljs-variable">$STORAGE_CONTAINER_NAME</span> \
--<span class="hljs-built_in">set</span> configuration.backupStorageLocation.config.resourceGroup=<span class="hljs-variable">$STORAGE_RESOURCE_GROUP</span> \
--<span class="hljs-built_in">set</span> configuration.backupStorageLocation.config.storageAccount=<span class="hljs-variable">$STORAGE_ACCOUNT</span> \
--<span class="hljs-built_in">set</span> configuration.backupStorageLocation.config.subscriptionId=<span class="hljs-variable">$AZURE_SUBSCRIPTION_ID</span> \
--<span class="hljs-built_in">set</span> configuration.volumeSnapshotLocation.name=<span class="hljs-string">'azure-eastus'</span> \
--<span class="hljs-built_in">set</span> configuration.volumeSnapshotLocation.config.resourceGroup=<span class="hljs-variable">$STORAGE_RESOURCE_GROUP</span> \
--<span class="hljs-built_in">set</span> configuration.volumeSnapshotLocation.config.subscriptionId=<span class="hljs-variable">$AZURE_SUBSCRIPTION_ID</span>
</code></pre><p>This also assumes that you’re using Azure as the provider/backend for Velero.</p>
<h2 id="heading-modify-veleros-backend-to-read-only">Modify Velero’s backend to read-only</h2>
<p>Find the backup location configured. It’s probably <code>default</code>.</p>
<pre><code># Against <span class="hljs-built_in">new</span> <span class="hljs-keyword">cluster</span> (<span class="hljs-keyword">cluster</span> #<span class="hljs-number">2</span>)
$ kubectl -n velero <span class="hljs-keyword">get</span> backupstoragelocation.velero.io
<span class="hljs-type">NAME</span>      PROVIDER   BUCKET/PREFIX                          PHASE       LAST VALIDATED                  <span class="hljs-keyword">ACCESS</span> MODE
<span class="hljs-keyword">default</span>   azure      backups-azaks-blah-blahh-staging<span class="hljs-number">-001</span>   Available   <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-29</span> <span class="hljs-number">12</span>:<span class="hljs-number">57</span>:<span class="hljs-number">49</span> <span class="hljs-number">-0800</span> PST   ReadWrite
</code></pre><p>Now, unfortunately, there’s no CLI option on Velero 1.5.1 to edit a <code>backup-location</code>. So, let’s edit the CRD instead.</p>
<pre><code><span class="hljs-selector-tag">kubectl</span> <span class="hljs-selector-tag">-n</span> <span class="hljs-selector-tag">velero</span> <span class="hljs-selector-tag">edit</span> <span class="hljs-selector-tag">backupstoragelocation</span><span class="hljs-selector-class">.velero</span><span class="hljs-selector-class">.io</span> &lt;<span class="hljs-selector-tag">NAME</span>&gt;
</code></pre><p>Using Vim, add/set the <code>spec.accessMode</code> to <code>ReadOnly</code>.</p>
<p>Then, verify that the backup-location is <code>ReadOnly</code> instead of <code>ReadWrite</code>.</p>
<pre><code><span class="hljs-comment"># Against new cluster (cluster #2)</span>
$ velero get <span class="hljs-keyword">backup</span>-locations                                                                                                                                                              
<span class="hljs-keyword">NAME</span>      PROVIDER   <span class="hljs-keyword">BUCKET</span>/PREFIX                          PHASE       <span class="hljs-keyword">LAST</span> VALIDATED                  <span class="hljs-keyword">ACCESS</span> <span class="hljs-keyword">MODE</span>
<span class="hljs-keyword">default</span>   azure      backups-azaks-blah-blahh-staging<span class="hljs-number">-001</span>   Available   <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-29</span> <span class="hljs-number">12</span>:<span class="hljs-number">57</span>:<span class="hljs-number">49</span> <span class="hljs-number">-0800</span> PST   ReadOnly
</code></pre><h2 id="heading-try-a-restore">Try a restore</h2>
<p>Great! Now let’s try restoring from a <em>scheduled</em> backup. This is assuming that you have some scheduled backups on your main cluster (cluster #1). If not, you’ll have to skip this and create a manual backup.</p>
<pre><code><span class="hljs-comment"># Against new cluster (cluster #2)</span>
$ velero <span class="hljs-keyword">backup</span> <span class="hljs-keyword">get</span>                                                                                                                                                                        
<span class="hljs-keyword">NAME</span>                            <span class="hljs-keyword">STATUS</span>      <span class="hljs-keyword">ERRORS</span>   <span class="hljs-keyword">WARNINGS</span>   CREATED                         EXPIRES   <span class="hljs-keyword">STORAGE</span> LOCATION   SELECTOR
every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210129010007</span>   Completed   <span class="hljs-number">0</span>        <span class="hljs-number">2</span>          <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-28</span> <span class="hljs-number">17</span>:<span class="hljs-number">00</span>:<span class="hljs-number">07</span> <span class="hljs-number">-0800</span> PST   <span class="hljs-number">29</span>d       <span class="hljs-keyword">default</span>            &lt;<span class="hljs-keyword">none</span>&gt;
every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210128010006</span>   Completed   <span class="hljs-number">0</span>        <span class="hljs-number">2</span>          <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-27</span> <span class="hljs-number">17</span>:<span class="hljs-number">00</span>:<span class="hljs-number">06</span> <span class="hljs-number">-0800</span> PST   <span class="hljs-number">28</span>d       <span class="hljs-keyword">default</span>            &lt;<span class="hljs-keyword">none</span>&gt;
every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210127010006</span>   Completed   <span class="hljs-number">0</span>        <span class="hljs-number">2</span>          <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-26</span> <span class="hljs-number">17</span>:<span class="hljs-number">00</span>:<span class="hljs-number">06</span> <span class="hljs-number">-0800</span> PST   <span class="hljs-number">27</span>d       <span class="hljs-keyword">default</span>            &lt;<span class="hljs-keyword">none</span>&gt;
every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210126010006</span>   Completed   <span class="hljs-number">0</span>        <span class="hljs-number">2</span>          <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-25</span> <span class="hljs-number">17</span>:<span class="hljs-number">00</span>:<span class="hljs-number">06</span> <span class="hljs-number">-0800</span> PST   <span class="hljs-number">26</span>d       <span class="hljs-keyword">default</span>            &lt;<span class="hljs-keyword">none</span>&gt;
every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210125010005</span>   Completed   <span class="hljs-number">0</span>        <span class="hljs-number">2</span>          <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-24</span> <span class="hljs-number">17</span>:<span class="hljs-number">00</span>:<span class="hljs-number">05</span> <span class="hljs-number">-0800</span> PST   <span class="hljs-number">25</span>d       <span class="hljs-keyword">default</span>            &lt;<span class="hljs-keyword">none</span>&gt;
every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210124010005</span>   Completed   <span class="hljs-number">0</span>        <span class="hljs-number">2</span>          <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-23</span> <span class="hljs-number">17</span>:<span class="hljs-number">00</span>:<span class="hljs-number">05</span> <span class="hljs-number">-0800</span> PST   <span class="hljs-number">24</span>d       <span class="hljs-keyword">default</span>            &lt;<span class="hljs-keyword">none</span>&gt;
</code></pre><p>Pick a backup and restore it.</p>
<pre><code>velero <span class="hljs-keyword">restore</span> <span class="hljs-keyword">create</span> <span class="hljs-comment">--from-backup every-day-at-1-20210129010007</span>
</code></pre><p>Velero will also give you the command to check the progress. For example:</p>
<pre><code><span class="hljs-comment"># Against new cluster (cluster #2)</span>
$ velero <span class="hljs-keyword">restore</span> <span class="hljs-keyword">describe</span> every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210129010007</span><span class="hljs-number">-20210129130618</span>
<span class="hljs-keyword">Name</span>:         every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210129010007</span><span class="hljs-number">-20210129130618</span>
Namespace:    velero
Labels:       &lt;<span class="hljs-keyword">none</span>&gt;
Annotations:  &lt;<span class="hljs-keyword">none</span>&gt;

Phase:  InProgress

Started:    <span class="hljs-number">2021</span><span class="hljs-number">-01</span><span class="hljs-number">-29</span> <span class="hljs-number">13</span>:<span class="hljs-number">06</span>:<span class="hljs-number">19</span> <span class="hljs-number">-0800</span> PST
Completed:  &lt;n/a&gt;

<span class="hljs-keyword">Backup</span>:  every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-1</span><span class="hljs-number">-20210129010007</span>

Namespaces:
  Included:  <span class="hljs-keyword">all</span> namespaces <span class="hljs-keyword">found</span> <span class="hljs-keyword">in</span> the <span class="hljs-keyword">backup</span>
  Excluded:  &lt;<span class="hljs-keyword">none</span>&gt;

Resources:
  Included:        *
  Excluded:        nodes, <span class="hljs-keyword">events</span>, events.events.k8s.io, backups.velero.io, restores.velero.io, resticrepositories.velero.io
  Cluster-scoped:  <span class="hljs-keyword">auto</span>

Namespace mappings:  &lt;<span class="hljs-keyword">none</span>&gt;

Label selector:  &lt;<span class="hljs-keyword">none</span>&gt;

<span class="hljs-keyword">Restore</span> PVs:  <span class="hljs-keyword">auto</span>
</code></pre><p>It took only about a minute or two. For me, there were a few warnings and some errors. It tried to restore itself, therefore, there were some warnings related to that. But, most of the deployments did not work because of the container registry. In my case, I did not add the ACR integration with my new cluster so many of the images failed to pull down. Once I “attached” the ACR to AKS, the deployments self-healed.</p>
<h2 id="heading-there-might-be-unhealthy-deployments">There might be unhealthy deployments</h2>
<p>Expect some of the Kubernetes deployments to not work. In my case, the Rancher agent was not working because one with the same name and secret was already running in my main cluster (cluster #1). Therefore, Rancher server was denying my second agent.</p>
<p>Also, my NGINX ingress controller was not healthy. This was because when I configured it on my main cluster, I tied it to a public static IP on Azure. The new cluster’s identity did not have permissions to modify that static IP nor did I want it to.</p>
<p>That’s it. Thanks for reading.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Kubernetes cluster on Raspberry Pis with a little Azure]]></title><description><![CDATA[Recently I finished something I’ve been wanting to do for a long time: creating a Kubernetes cluster running on some Raspberry Pis. I mostly followed a recent post from Alex Ellis. So, this post will build on that and speak about my experience with t...]]></description><link>https://gaunacode.com/building-a-kubernetes-cluster-on-raspberry-pis-with-a-little-azure</link><guid isPermaLink="true">https://gaunacode.com/building-a-kubernetes-cluster-on-raspberry-pis-with-a-little-azure</guid><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Sat, 23 Jan 2021 12:55:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639606316/oIBiHfzQj.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I finished something I’ve been wanting to do for a long time: creating a Kubernetes cluster running on some Raspberry Pis. I mostly followed a recent post from <a target="_blank" href="https://blog.alexellis.io/self-hosting-kubernetes-on-your-raspberry-pi/">Alex Ellis</a>. So, this post will build on that and speak about my experience with the bill of materials, some kinks, and how to save some time.</p>
<h2 id="heading-overview">Overview</h2>
<p>I have a 4-node (1 master, 3 workers) Kubernetes cluster hosted on 4 Raspberry Pis 4 Model B with 4 GB of RAM. The nodes are running <a target="_blank" href="https://k3s.io/">k3s</a> - a lightweight distribution of Kubernetes for the edge. On my cluster, I am using the <a target="_blank" href="https://github.com/inlets/inlets">inlets</a> project to expose some test web applications to the public internet without requiring public static IPs from my ISP.</p>
<h2 id="heading-the-bill-of-materials">The bill of materials</h2>
<p>Here’s the list of things I bought. It all cost me <strong>$389.42.</strong></p>
<ul>
<li>1x <a target="_blank" href="https://www.amazon.com/gp/product/B08GG8814S/ref=ppx_yo_dt_b_asin_title_o01_s00?ie=UTF8&amp;psc=1">wall-mountable power strip</a></li>
<li>4x <a target="_blank" href="https://www.amazon.com/gp/product/B008F0YD46/ref=ppx_yo_dt_b_asin_title_o08_s00?ie=UTF8&amp;psc=1">patch ethernet cables</a></li>
<li>4x <a target="_blank" href="https://www.amazon.com/gp/product/B07TC2BK1X/ref=ppx_yo_dt_b_asin_title_o08_s00?ie=UTF8&amp;psc=1">Raspberry Pis Model B w/ 4 GB RAM</a></li>
<li>4x <a target="_blank" href="https://www.amazon.com/gp/product/B07TYQRXTK/ref=ppx_yo_dt_b_asin_title_o08_s00?ie=UTF8&amp;psc=1">power supplies for the Raspberry Pis</a></li>
<li>1x <a target="_blank" href="https://www.amazon.com/gp/product/B07CTG5N3V/ref=ppx_yo_dt_b_asin_title_o08_s01?ie=UTF8&amp;psc=1">Raspberry Pi cluster case</a></li>
<li>4x <a target="_blank" href="https://www.amazon.com/gp/product/B06XWN9Q99/ref=ppx_od_dt_b_asin_title_s00?ie=UTF8&amp;psc=1">Samsung SD cards</a></li>
</ul>
<p>What’s the power strip for? I ran out of plugs in my old power strip. This one has 12 outlets and I could also mount it <em>underneath</em> my desk to hide most of the cables.</p>
<p>What I <em>did</em> not buy? An ethernet cable to connect the Pis to my network. I already had some. Also, I did not buy a switch, I already had one too. If you need one, <a target="_blank" href="https://www.amazon.com/NETGEAR-5-Port-Gigabit-Ethernet-Unmanaged/dp/B07S98YLHM/ref=sr_1_3?dchild=1&amp;keywords=5+switch&amp;qid=1611407266&amp;s=electronics&amp;sr=1-3">something like this would work</a>.</p>
<p>Why not using a <a target="_blank" href="https://www.amazon.com/Anker-Charger-PowerPort-iPhone-Galaxy/dp/B00P936188/ref=sr_1_3?dchild=1&amp;keywords=usb+charging+hub&amp;qid=1611421817&amp;sr=8-3">USB charging hub</a>? Alex Ellis recommended using the power supply. He mentioned there are several accounts of people experiencing their charging hubs to be “browning out.”</p>
<h2 id="heading-the-assembly">The assembly</h2>
<p>It took me about 1.5 - 2 hours to put everything together. I stacked the Pis on the cluster case and tediously placed the fans. The fans are highly recommended and I configured them to be in <em>quiet</em> mode. They’re <em>really</em> quiet.</p>
<p>Then, I also spent significant time re-doing the wiring at my desk to have more outlets. This allowed me to have the cluster on my desk.</p>
<p>So plan accordingly and take your time.</p>
<h2 id="heading-flashing-the-sd-cards">Flashing the SD cards</h2>
<p>I flashed all the SD cards with <strong>Rasberry Pi OS Lite 32-bit</strong>. I used the <a target="_blank" href="https://www.raspberrypi.org/software/">Raspberry Pi Imager</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639596010/dD4K4xAHA.png" alt="The Raspberry Pi Imager Example" /><br /><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639597560/7nktukXd1.png" alt="Choosing the right image on the Raspberry Pi Imager" /></p>
<p>It was really easy. However, as each SD card was flashed, I would re-plug it. I would make the following two changes:</p>
<ul>
<li>Enabled SSH by creating an empty file in the <code>/boot</code> directory called <code>ssh</code>. <a target="_blank" href="https://www.raspberrypi.org/documentation/remote-access/ssh/">Read more here</a>.</li>
<li>I modified the <code>cmdline.txt</code> file in the <code>/boot</code> directory. I appended the <code>cgroup_memory=1 cgroup_enable=memory</code> commands . <a target="_blank" href="https://rancher.com/docs/k3s/latest/en/advanced/#enabling-legacy-iptables-on-raspbian-buster">This is a requirement for k3s to run on Raspberry Pis</a>.</li>
</ul>
<h2 id="heading-shaving-the-yak">Shaving the Yak</h2>
<p>After each SD card was flashed and some of the pre-requisites were taken care of, it was time to do more <em>even more</em> prep work.</p>
<p>First, I booted up the Pis and observed that the blinky lights looked healthy. I then used <code>nmap</code> to find the private IPs for these new Pis.</p>
<pre><code><span class="hljs-string">nmap</span> <span class="hljs-string">-sn</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.0</span><span class="hljs-string">/24</span>
</code></pre><p>If they were working fine, the default hostname started with <code>raspberry</code>.</p>
<p>Then, I would SSH into each Pi and change the default password. I would also change the default hostname in <code>/etc/hostname</code> and the matching entry in the hosts file at <code>/etc/hosts</code>. I would also enable <code>iptables</code> since <a target="_blank" href="https://rancher.com/docs/k3s/latest/en/advanced/#enabling-legacy-iptables-on-raspbian-buster">it’s another pre-requisite for k3s</a>.</p>
<pre><code>sudo iptables -F
sudo <span class="hljs-keyword">update</span>-alternatives <span class="hljs-comment">--set iptables /usr/sbin/iptables-legacy</span>
sudo <span class="hljs-keyword">update</span>-alternatives <span class="hljs-comment">--set ip6tables /usr/sbin/ip6tables-legacy</span>
sudo reboot
</code></pre><p>Once all the nodes rebooted, they had new hostnames and new DNS labels.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639599127/u6Ii5YdL1.png" alt="An example of the Pis with the private IPs" /></p>
<p>If you are OCD like me, you can make the <code>pi-master</code> DNS label/IP be the top pi of the stack, and each of the worker Pis can increment top to bottom. (i.e. from top to bottom of the stack: <code>pi-master</code>, <code>pi-worker-1</code>, <code>pi-worker-2</code>, and <code>pi-worker-3</code>).</p>
<p>Lastly, I would also copy my SSH key into each Pi via <code>ssh-copy-id</code>.</p>
<h2 id="heading-installing-k3s">Installing k3s</h2>
<p>I had limited success trying to install k3s through the official instructions. It was time-consuming and I had issues with the master node coming online. Unfortunately, I don’t recall the issues and I did not have enough time to look for the root cause of the issue (unhappy kids). I started over by re-flashing the SD cards and used the <a target="_blank" href="https://github.com/alexellis/k3sup">k3sup</a> project by Alex Ellis.</p>
<p>Once <code>k3sup</code> was installed, then it was really simple to create a k3s <em>server</em> node. A server node is the Kubernetes master.</p>
<pre><code><span class="hljs-built_in">export</span> MASTER=<span class="hljs-string">"[your private ip of the master pi]"</span>
k3sup install --ip <span class="hljs-variable">$MASTER</span> --user pi
</code></pre><p>After installation, I would ensure that the master was up and healthy by attempting a simple <code>kubectl</code> command. <code>k3sup</code> was also nice enough to place the Kubernetes config file into my working directory.</p>
<pre><code>kubectl <span class="hljs-keyword">get</span> nodes <span class="hljs-comment">--kubeconfig k3s.yaml</span>
</code></pre><p>After the master node was up, I moved on to the worker nodes.</p>
<pre><code><span class="hljs-string">k3sup</span> <span class="hljs-string">join</span> <span class="hljs-string">--ip</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.217</span> <span class="hljs-string">--server-ip</span> <span class="hljs-string">$MASTER</span> <span class="hljs-string">--user</span> <span class="hljs-string">pi</span>
<span class="hljs-string">k3sup</span> <span class="hljs-string">join</span> <span class="hljs-string">--ip</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.213</span> <span class="hljs-string">--server-ip</span> <span class="hljs-string">$MASTER</span> <span class="hljs-string">--user</span> <span class="hljs-string">pi</span>
<span class="hljs-string">k3sup</span> <span class="hljs-string">join</span> <span class="hljs-string">--ip</span> <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.221</span> <span class="hljs-string">--server-ip</span> <span class="hljs-string">$MASTER</span> <span class="hljs-string">--user</span> <span class="hljs-string">pi</span>
</code></pre><p>Then, verified that they registered properly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639600807/K0dxw0L0B.png" alt="Testing the new pi cluster" /></p>
<h2 id="heading-installing-inlets-for-ingress-with-a-little-azure">Installing inlets for ingress with a little Azure</h2>
<p>I wanted to deploy <em>something</em> on my new cluster. But, there were a few gotchas with this. Typically, in a cloud environment, you’ll probably use a <code>LoadBalancer</code> service to expose to public IPs for your ingress controller to allow traffic into the cluster to direct it to some applications. In a home lab set-up, unless you buy public static IPs from your ISP, then you have dynamic public IPs that change outside your control. So, if you point your custom domain to the public IPs of your home router, then your custom domain might not work in the future. This is especially true if you wanted to take your Pis into the office or a presentation for a Meetup.</p>
<p>So there’s another open-source project, <a target="_blank" href="https://github.com/inlets/inlets">inlets</a>, that essentially helps you create some cheap VMs on the cloud provider of choice, create/associate some public static IPs to those VMs, then install an agent on those VMs that talks to an operator running on your k8s cluster - effectively providing you with a public static IP for your cluster, securely.</p>
<p>To install <code>inlets</code>, the easiest way and the documented way is using <a target="_blank" href="https://github.com/alexellis/arkade">arkade</a>. To be honest, I don’t quite understand the value proposition of <code>arkade</code>. I haven’t found a strong use-case for it in the real world.</p>
<p>I had to log in to Azure and create an Azure service principal with the CLI. I saved the authentication info for the service principal into a temporary file outside of source control.</p>
<pre><code>az <span class="hljs-keyword">login</span>
az ad sp <span class="hljs-keyword">create</span>-<span class="hljs-keyword">for</span>-rbac <span class="hljs-comment">--sdk-auth &gt; /tmp/az_client_credentials.json</span>

SUBSCRIPTION_ID=$(az account <span class="hljs-keyword">show</span> | jq <span class="hljs-string">'.id'</span> -r)
</code></pre><p>Then, create a Kubernetes cluster in your Pi cluster. It will contain the contents of the service principal authentication file.</p>
<pre><code>kubectl <span class="hljs-keyword">create</span> secret generic inlets-<span class="hljs-keyword">access</span>-<span class="hljs-keyword">key</span> <span class="hljs-comment">--from-file=inlets-access-key=/tmp/az_client_credentials.json</span>
</code></pre><p>Then, I installed “inlets” through “arkade”.</p>
<pre><code>arkade <span class="hljs-keyword">install</span> inlets-<span class="hljs-keyword">operator</span> \
 <span class="hljs-comment">--provider azure \</span>
 <span class="hljs-comment">--region eastus \</span>
 <span class="hljs-comment">--subscription-id=$SUBSCRIPTION_ID</span>
</code></pre><p>Notice, how I used the Azure provider. The default <code>inlets</code> provider is Digital Ocean.</p>
<p>After a few minutes, I checked the status on the “traefik” ingress controller. Traefik comes installed by default with k3s.</p>
<pre><code>kubectl <span class="hljs-keyword">get</span> svc -n kube-<span class="hljs-keyword">system</span>
</code></pre><p>There’s a <code>traefik</code> service in the <code>kube-system</code> namespace. This <code>LoadBalancer</code> service is used by the ingress controller. If everything worked fine, <code>inlets</code> should have provided a public IP to this service. Make note of it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639602490/mA5xYSGwh.png" alt="Fidning Inlet public IP" /></p>
<h2 id="heading-deploying-the-test-application">Deploying the test application</h2>
<p>Once <code>inlets</code> was set-up, then I moved onto deploying a test application. One caveat with Raspberry Pi k8s clusters is the CPU architecture. Most Docker images support the default architectures of <code>amd64</code>. Raspberry Pis need the <code>arm64</code> architecture; therefore, Docker images have to be built to target that architecture.</p>
<p>I won’t get into how to do this, but if you want to build your own Docker image that runs on Pis, then I found <a target="_blank" href="https://www.docker.com/blog/multi-arch-images/">this blog post useful</a>.</p>
<p>If you want, you can use the test application I created. The image is public on Docker Hub. Start by creating a <code>test-application.yaml</code> with the following contents:</p>
<pre><code><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">realworld-deploy</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">realworld</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">realworld</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">realworld</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">realworld</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">fgauna12/realworld-react:latest</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">realworld</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">realworld-svc</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">ClusterIP</span>
  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
    <span class="hljs-attr">protocol:</span> <span class="hljs-string">TCP</span>
    <span class="hljs-attr">targetPort:</span> <span class="hljs-number">80</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">realworld</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">extensions/v1beta1</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">"realworld-ingress"</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">rules:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">pi.gaunacode.com</span>
      <span class="hljs-attr">http:</span>
        <span class="hljs-attr">paths:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">/</span>
            <span class="hljs-attr">backend:</span>
              <span class="hljs-attr">serviceName:</span> <span class="hljs-string">realworld-svc</span>
              <span class="hljs-attr">servicePort:</span> <span class="hljs-number">80</span>
</code></pre><p>You’ll need a custom domain. Use that instead of mine in the <code>Ingress.spec.rules[0].host</code> portion.</p>
<p>Next, I create an A record to point to the <code>traefik</code> service’s public IP.</p>
<pre><code>A <span class="hljs-type">record</span> <span class="hljs-keyword">on</span> pi.gaunacode.com -&gt; [Azure <span class="hljs-built_in">Public</span> IP created <span class="hljs-keyword">by</span> inlets <span class="hljs-keyword">and</span> used <span class="hljs-keyword">by</span> Traefik service]
</code></pre><p>Once you created the A record, you’re ready to deploy the test app.</p>
<pre><code><span class="hljs-selector-tag">kubectl</span> <span class="hljs-selector-tag">apply</span> <span class="hljs-selector-tag">-f</span> <span class="hljs-selector-tag">test-application</span><span class="hljs-selector-class">.yaml</span>
</code></pre><p>In a few minutes, I had my app running on my cluster and available on the internet! It will be addressable at the root domain <em>without</em> <code>https</code>. In my case, <code>http://pi.gaunacode.com</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639604362/1TG7e3nLf.png" alt="Example app screenshot" /></p>
<p>This example app is a dockerized version of <a target="_blank" href="https://github.com/gothinkster/realworld">this React front-end</a>. It’s simply a static website driven by data from an API hosted by someone else.</p>
<p>Hope that helped. Don’t hesitate to reach out if you need help. Happy hacking.</p>
]]></content:encoded></item><item><title><![CDATA[Creating an HTTPS ingress controller with your own TLS certificate and with public static IP on AKS]]></title><description><![CDATA[Microsoft already has good documentation on configuring an NGINX ingress controller with Let’s Encrypt or your own SSL certificate. There’s no guidance on creating an HTTPS ingress with your own certificate and using a public static IP for the ingres...]]></description><link>https://gaunacode.com/creating-an-https-ingress-controller-with-your-own-tls-certificate-and-with-public-static-ip-on-aks</link><guid isPermaLink="true">https://gaunacode.com/creating-an-https-ingress-controller-with-your-own-tls-certificate-and-with-public-static-ip-on-aks</guid><category><![CDATA[Azure]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Wed, 11 Nov 2020 12:09:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639609188/7F7ru2mUJ.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Microsoft already has good documentation on configuring an NGINX ingress controller with Let’s Encrypt or your own SSL certificate. There’s no guidance on creating an HTTPS ingress with your own certificate <em>and</em> using a public static IP for the ingress controller. This is important because many companies/enterprises already have certificates for applications and/or are not ready for Let’s Encrypt. Also, in some organizations requesting an A record on a DNS provider can take some time, so it’s more convenient to have static IPs for assurance that environments won’t break if AKS clusters have to be re-created.</p>
<p>In this post, I’ll guide you through acquiring a free certificate, then uploading it to Kubernetes, setting up the ingress controller with a static IP, configuring DNS, then defining the ingress route for a sample application, then verifying everything works.</p>
<h2 id="heading-pre-requisites">Pre-requisites</h2>
<ul>
<li>You have a domain or access to one.</li>
<li>You can verify the ownership of the domain or have an SSL certificate and its private key.</li>
</ul>
<h2 id="heading-first-the-certificate">First, the certificate.</h2>
<p>If you don’t have a certificate, you can either use a self-signed certificate or use a free and trial certificate from a certificate authority. If you already have a certificate, you can skip to the next section.</p>
<p>Here’s how you can get a free certificate from Comodo, a popular certificate authority.</p>
<p>Go to <a target="_blank" href="https://ssl.comodo.com/free-ssl-certificate">https://ssl.comodo.com/free-ssl-certificate</a>.</p>
<p>Choose the domain for your application. In my case, I will be using <code>https://my-app.gaunacode.com</code>.</p>
<p>Sign up for an account and fill out your information. You don’t have to provide your credit card.</p>
<p>View the certificate and choose <strong>“Setup ssl.comodo.com SSL Certificate.”</strong></p>
<p>You will be asked for a Certificate Signing Request (CSR).</p>
<p>Here’s how to create one with Open SSL.</p>
<pre><code><span class="hljs-comment"># Create the private key. You will need this later.</span>
openssl genrsa -out <span class="hljs-keyword">my</span>-app-gaunacode.key <span class="hljs-number">2048</span>

<span class="hljs-comment"># Create a CSR from the private key</span>
openssl req -new -key <span class="hljs-keyword">my</span>-app-gaunacode.key -out <span class="hljs-keyword">my</span>-app-gaunacode.csr

<span class="hljs-comment"># View the CSR</span>
cat <span class="hljs-keyword">my</span>-app-gaunacode.csr
</code></pre><p>Copy the CSR and paste it into the textbox from Comodo.</p>
<p>Then, Comodo will ask you to choose a method to verify the ownership. I chose email verification. If you try CNAME verification, you might have trouble with it as I did. With email verification, I had to <a target="_blank" href="https://optimwise.com/create-email-distribution-lists-with-google-apps/">set-up an email distro</a> at <code>admin@gaunacode.com</code> using my GSuite account.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639609188/7F7ru2mUJ.png" alt="Choosing domain verification" /></p>
<p>Then, I verified my domain and received an email from Sectigo (Comodo).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639610662/UIcxJ8rXu.png" alt="Example email from Comodo to verify email" /></p>
<p>Once I verified, I received my SSL certificate in an email.</p>
<h2 id="heading-setting-up-the-ingress">Setting up the ingress</h2>
<p>Now that you have a certificate, you can get to the fun part. First, create a public static IP using the Azure CLI. Create it on the same resource group where the node pools reside so that AKS has enough permissions to modify the resource. Alternatively, you can grant <strong>Network Contributor</strong> rights to the AKS service principal and create the public IP in any resource group of your choice.</p>
<pre><code>az aks <span class="hljs-keyword">show</span> <span class="hljs-comment">--resource-group my-resource-group --name my-cluster --query nodeResourceGroup -o tsv</span>

az network <span class="hljs-built_in">public</span>-ip <span class="hljs-keyword">create</span> <span class="hljs-comment">--resource-group MC_my_nodepool_resource_group --name my-ip-name --sku Standard --allocation-method static --query publicIp.ipAddress -o tsv</span>
</code></pre><p>The last command will give you the static public IP address. Copy it. In my case, it was <code>52.146.67.3</code>.</p>
<p>Now, create a namespace to place the NGINX ingress controller.</p>
<pre><code>kubectl <span class="hljs-keyword">create</span> ns ingress
</code></pre><p>Then, deploy NGINX using a Helm chart. If your cluster is RBAC enabled with Azure AD, then set <code>rbac.create=true</code>. Choose a name for the DNS label on the public static IP address. I chose the same name as the Azure public IP resource. Set this DNS label on the last parameter (<code>controller.service.annotations."service\.beta\.kubernetes\.io/azure-dns-label-name"</code>)</p>
<pre><code>helm repo add ingress-nginx https:<span class="hljs-comment">//kubernetes.github.io/ingress-nginx</span>

helm install nginx-ingress ingress-nginx/ingress-nginx \
    --<span class="hljs-built_in">set</span> rbac.create=<span class="hljs-literal">false</span> \
    --<span class="hljs-keyword">namespace</span> ingress \
    --<span class="hljs-built_in">set</span> controller.replicaCount=<span class="hljs-number">2</span> \
    --<span class="hljs-built_in">set</span> controller.nodeSelector.<span class="hljs-string">"beta\.kubernetes\.io/os"</span>=linux \
    --<span class="hljs-built_in">set</span> defaultBackend.nodeSelector.<span class="hljs-string">"beta\.kubernetes\.io/os"</span>=linux \
    --<span class="hljs-built_in">set</span> controller.service.loadBalancerIP=<span class="hljs-string">"[your public static ip address]"</span> \
    --<span class="hljs-built_in">set</span> controller.service.annotations.<span class="hljs-string">"service\.beta\.kubernetes\.io/azure-dns-label-name"</span>=<span class="hljs-string">"[dns label]"</span>
</code></pre><p>Once the ingress controller deploys, it shouldn’t take long because you can check the deployment.</p>
<pre><code>kubectl <span class="hljs-comment">--namespace ingress get services -o wide -w nginx-ingress-ingress-nginx-controller</span>
</code></pre><p>Verify that the <code>External IP</code> address should be the same as the public static IP.</p>
<pre><code><span class="hljs-attribute">NAME</span>                                     TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)                      AGE   SELECTOR
<span class="hljs-attribute">nginx</span>-ingress-ingress-nginx-controller   LoadBalancer   <span class="hljs-number">10.0.64.221</span>   <span class="hljs-number">52.146.67.3</span>   <span class="hljs-number">80</span>:<span class="hljs-number">31148</span>/TCP,<span class="hljs-number">443</span>:<span class="hljs-number">32641</span>/TCP   <span class="hljs-number">13</span>m   app.kubernetes.io/component=controller,app.kubernetes.io/instance=nginx-ingress,app.kubernetes.io/name=ingress-nginx
</code></pre><p>Create an A record from your domain to the public IP. For example, I am using Netlify DNS. Give it some time for the DNS change to propagate.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639612205/aNMRaYFQX.png" alt="Example of setting up A record" /></p>
<p>Next, let’s verify that ingress works with two sample applications.</p>
<p>Create a namespace for the sample app we will use.</p>
<pre><code>kubectl <span class="hljs-keyword">create</span> ns ingress-<span class="hljs-keyword">test</span>
</code></pre><p>Deploy the SSL certificate on the namespace as a Kubernetes secret. Notice how it’s deployed on the same namespace as the application to be deployed.</p>
<pre><code>kubectl <span class="hljs-keyword">create</span> secret tls aks-ingress-tls \
    <span class="hljs-comment">--namespace ingress-test \</span>
    <span class="hljs-comment">--key my-app-gaunacode.key \</span>
    <span class="hljs-comment">--cert my-app_gaunacode_com.crt</span>
</code></pre><p>Deploy the first sample application on the <code>ingress-test</code> namespace.</p>
<pre><code><span class="hljs-string">cat</span> <span class="hljs-string">&lt;&lt;EOF</span> <span class="hljs-string">|</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-bullet">-</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">aks-helloworld</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">ingress-test</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">aks-helloworld</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">aks-helloworld</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">aks-helloworld</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">mcr.microsoft.com/azuredocs/aks-helloworld:v1</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
        <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">TITLE</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">"Welcome to Azure Kubernetes Service (AKS)"</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">aks-helloworld</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">ingress-test</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">ClusterIP</span>
  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">aks-helloworld</span>
<span class="hljs-string">EOF</span>
</code></pre><p>Now, deploy the second sample application also on the same namespace.</p>
<pre><code><span class="hljs-string">cat</span> <span class="hljs-string">&lt;&lt;EOF</span> <span class="hljs-string">|</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-bullet">-</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Deployment</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">ingress-demo</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">ingress-test</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">1</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">app:</span> <span class="hljs-string">ingress-demo</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">app:</span> <span class="hljs-string">ingress-demo</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">ingress-demo</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">mcr.microsoft.com/azuredocs/aks-helloworld:v1</span>
        <span class="hljs-attr">ports:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">containerPort:</span> <span class="hljs-number">80</span>
        <span class="hljs-attr">env:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">TITLE</span>
          <span class="hljs-attr">value:</span> <span class="hljs-string">"AKS Ingress Demo"</span>
<span class="hljs-meta">---</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Service</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">ingress-demo</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">ingress-test</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">type:</span> <span class="hljs-string">ClusterIP</span>
  <span class="hljs-attr">ports:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">port:</span> <span class="hljs-number">80</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">ingress-demo</span>
<span class="hljs-string">EOF</span>
</code></pre><p>Lastly, deploy the ingress in the same namespace as the applications.</p>
<pre><code><span class="hljs-string">export</span> <span class="hljs-string">DOMAIN_NAME="my-app.gaunacode.com"</span>

<span class="hljs-string">cat</span> <span class="hljs-string">&lt;&lt;EOF</span> <span class="hljs-string">|</span> <span class="hljs-string">kubectl</span> <span class="hljs-string">apply</span> <span class="hljs-string">-f</span> <span class="hljs-bullet">-</span>
<span class="hljs-attr">apiVersion:</span> <span class="hljs-string">networking.k8s.io/v1beta1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">Ingress</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">hello-world-ingress</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">ingress-test</span>
  <span class="hljs-attr">annotations:</span>
    <span class="hljs-attr">kubernetes.io/ingress.class:</span> <span class="hljs-string">nginx</span>
    <span class="hljs-attr">nginx.ingress.kubernetes.io/use-regex:</span> <span class="hljs-string">"true"</span>
    <span class="hljs-attr">nginx.ingress.kubernetes.io/rewrite-target:</span> <span class="hljs-string">/$1</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">tls:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">hosts:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">$DOMAIN_NAME</span>
    <span class="hljs-attr">secretName:</span> <span class="hljs-string">aks-ingress-tls</span>
  <span class="hljs-attr">rules:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">host:</span> <span class="hljs-string">$DOMAIN_NAME</span>
    <span class="hljs-attr">http:</span>
      <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">backend:</span>
          <span class="hljs-attr">serviceName:</span> <span class="hljs-string">aks-helloworld</span>
          <span class="hljs-attr">servicePort:</span> <span class="hljs-number">80</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">/hello-world-one(/|$)(.*)</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">backend:</span>
          <span class="hljs-attr">serviceName:</span> <span class="hljs-string">ingress-demo</span>
          <span class="hljs-attr">servicePort:</span> <span class="hljs-number">80</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">/hello-world-two(/|$)(.*)</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">backend:</span>
          <span class="hljs-attr">serviceName:</span> <span class="hljs-string">aks-helloworld</span>
          <span class="hljs-attr">servicePort:</span> <span class="hljs-number">80</span>
        <span class="hljs-attr">path:</span> <span class="hljs-string">/(.*)</span>
<span class="hljs-string">EOF</span>
</code></pre><p>That’s it! Open the browser and navigate to the first app. The second application can be visited by adding <code>/hello-world-two</code> to the path.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639613725/m0LqbjhXN.png" alt="Screenshot of Sample App 1" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639615121/JiYvyAmfE.png" alt="Screenshot of Sample App 2" /></p>
]]></content:encoded></item><item><title><![CDATA[AKS Best Practice: Backing up AKS with Velero]]></title><description><![CDATA[Backing up AKS clusters is really important. Even if you’re using Infrastructure as Code and all deployments are automated, you will benefit from taking backups of AKS clusters. It’s also stated as a best-practice by Microsoft.
Why?
Mean Time To Reco...]]></description><link>https://gaunacode.com/aks-best-practice-backing-up-aks-with-velero</link><guid isPermaLink="true">https://gaunacode.com/aks-best-practice-backing-up-aks-with-velero</guid><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Sat, 03 Oct 2020 11:26:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639620921/4SRTvQwoTe.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Backing up AKS clusters is really important. Even if you’re using Infrastructure as Code and all deployments are automated, you will benefit from taking backups of AKS clusters. It’s also stated as a <a target="_blank" href="https://docs.microsoft.com/en-us/azure/aks/operator-best-practices-storage#secure-and-back-up-your-data">best-practice by Microsoft</a>.</p>
<h2 id="heading-why">Why?</h2>
<h4 id="heading-mean-time-to-recover-mttr">Mean Time To Recover (MTTR)</h4>
<p>You might have everything automated. From the cluster creation to the applications being deployed on the cluster. But, in my experience, this automation can take a long time. For example, it took me 45 minutes for all pipelines to deploy each of ~15 microservices to a cluster.</p>
<p>During a disaster, recreating new infrastructure and re-deploying all components can take time. Depending on the criticality of the incident and the importance of the app, <em>it can feel like an eternity</em>.</p>
<p>By using a tool like Velero to backup all Kubernetes resources, a cluster can be quickly restored to a certain state, lessening recovery time from ~45 minutes to ~15 minutes.</p>
<h4 id="heading-avoiding-data-loss">Avoiding Data Loss</h4>
<p>If you have <em>stateful</em> applications deployed on your clusters, you will likely use persistent volumes to store data. For example, running a database on Kubernetes. So, tools like Velero can also help backup the data in persistent volumes as well.</p>
<h2 id="heading-velero">Velero</h2>
<p>Velero is an open source tool that helps backup and restore Kubernetes resources. It also helps with migrating Kubernetes resources from one cluster to another. Also, it can help backup/restore data in persistent volumes.</p>
<h4 id="heading-how-it-will-work">How it will work</h4>
<p>Velero will have a pod running on the cluster, taking backups against an Azure Storage account (Although you can use other backends). You can take ad-hoc backups or create a schedule for them. But to do so, it has to be able to authenticate against the Azure API with enough permissions to be able to write to the storage account.</p>
<p>Velero has a CLI and a server side component to it. Either way, you’re going to have to install the CLI.</p>
<h4 id="heading-the-cli">The CLI</h4>
<p>Here are the <a target="_blank" href="https://velero.io/docs/v1.5/basic-install/">basic steps</a> on how to install the CLI on MacOs, Windows, or Linux.</p>
<h4 id="heading-the-server-side">The Server Side</h4>
<p>For the server side component, there’s two main methods of installation:</p>
<ul>
<li>The Velero CLI</li>
<li>A Helm Chart</li>
</ul>
<h2 id="heading-installing-velero">Installing Velero</h2>
<p>I prefer to use the Helm chart so that I can install Velero from a GitOps workflow. You can see the official instructions on the <a target="_blank" href="https://github.com/vmware-tanzu/helm-charts/tree/main/charts/velero">Helm chart’s readme</a> These instructions below will be a little different so that it’s easier to automate.</p>
<p>Velero uses an Azure Plugin to interact with Azure. To authenticate, use Service Principals for now. Usually, I prefer using Azure Active Directory Pod Identities but there’s an <a target="_blank" href="https://github.com/vmware-tanzu/velero-plugin-for-microsoft-azure/issues/61">open issue</a> with Managed Identities. It’s a project that allows pods to authenticate against Azure using Managed Identities. In other words, by using pod identities (managed identities), you won’t need an API secret for Velero to authenticate against Azure. Remember though, managed identities only work on Azure.</p>
<h4 id="heading-the-credentials">The Credentials</h4>
<p>Start by getting the resource group where the virtual machines for your AKS cluster. Velero will need permissions to this.</p>
<pre><code><span class="hljs-attr">AZURE_RESOURCE_GROUP</span>=$(az aks show -n mycluster -g myresourcegroup --query <span class="hljs-string">"nodeResourceGroup"</span> -o tsv)
</code></pre><p>Then, get the Azure subscription and Azure AD tenant information.</p>
<pre><code><span class="hljs-attr">AZURE_SUBSCRIPTION_ID</span>=$(az account list --query <span class="hljs-string">'[?isDefault].id'</span> -o tsv)
<span class="hljs-attr">AZURE_TENANT_ID</span>=$(az account list --query <span class="hljs-string">'[?isDefault].tenantId'</span> -o tsv)
</code></pre><p>Now create an Azure service principal for Velero to authenticate. I like to name the service principal with the name of the cluster so that I know <em>which Velero</em> identity belongs to which cluster.</p>
<p>Note: you can also limit the scope of the service principal to certain resource groups <code>--scopes</code> so that it doesn’t have access to the entire subscription.</p>
<pre><code><span class="hljs-attr">AZURE_CLIENT_SECRET</span>=$(az ad sp create-for-rbac --name <span class="hljs-string">"mycluster-velero"</span> --role <span class="hljs-string">"Contributor"</span> --query <span class="hljs-string">'password'</span> -o tsv)
</code></pre><p>Almost there… now, get the Client Id for the service principal just created.</p>
<pre><code><span class="hljs-attr">AZURE_CLIENT_ID</span>=$(az ad sp list --display-name <span class="hljs-string">"mycluster-velero"</span> --query <span class="hljs-string">'[0].appId'</span> -o tsv)
</code></pre><p>Create a file called <code>credentials-velero</code>. <strong>Do not commit this to source control</strong>.</p>
<pre><code>cat &lt;&lt; EOF  &gt; ./credentials-velero
AZURE_SUBSCRIPTION_ID=${AZURE_SUBSCRIPTION_ID}
AZURE_TENANT_ID=${AZURE_TENANT_ID}
AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
AZURE_RESOURCE_GROUP=${AZURE_RESOURCE_GROUP}
AZURE_CLOUD_NAME=AzurePublicCloud
EOF
</code></pre><p>Using <code>kubectl</code>, upload the credentials to a kubernetes secret in a new <code>velero</code> namespace.</p>
<pre><code>kubectl <span class="hljs-keyword">create</span> ns velero
kubectl <span class="hljs-keyword">create</span> secret generic velero-credentials -n velero <span class="hljs-comment">--from-literal="cloud=$(cat ./credentials-velero)"</span>
</code></pre><h4 id="heading-the-storage-account">The Storage Account</h4>
<p>First, create a storage account, this will store the backups.</p>
<pre><code>STORAGE_ACCOUNT=<span class="hljs-string">'stmyapptest001'</span>
STORAGE_RESOURCE_GROUP=<span class="hljs-string">'rg-myapp-test-001'</span>
az storage account create -n $STORAGE_ACCOUNT -g $STORAGE_RESOURCE_GROUP
</code></pre><p>Then, create the container to place the backups. I also like to name the container name after the cluster.</p>
<pre><code>STORAGE_CONTAINER_NAME=<span class="hljs-string">'backups-mycluster'</span>
az <span class="hljs-keyword">storage</span> container <span class="hljs-keyword">create</span> <span class="hljs-comment">--account-name $STORAGE_ACCOUNT -n $STORAGE_CONTAINER_NAME</span>
</code></pre><h4 id="heading-putting-it-all-together">Putting it all together</h4>
<p>Now, we install Velero using Helm and using Velero’s Azure plugin.</p>
<p>First, add the <a target="_blank" href="https://vmware-tanzu.github.io/helm-charts/">VMware Tanzu Helm repo</a>.</p>
<pre><code>helm repo <span class="hljs-keyword">add</span> vmware-tanzu https:<span class="hljs-comment">//vmware-tanzu.github.io/helm-charts</span>
</code></pre><p>Lastly, install the helm chart.</p>
<pre><code>helm install velero vmware-tanzu/velero --namespace velero --version 2.13.2 \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].image=velero/velero-plugin-for-microsoft-azure:v1.1.0"</span> \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].imagePullPolicy=IfNotPresent"</span> \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].volumeMounts[0].mountPath=/target"</span> \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].volumeMounts[0].name=plugins"</span> \
--<span class="hljs-built_in">set</span> <span class="hljs-string">"initContainers[0].name=velero-plugin-for-azure"</span> \
--<span class="hljs-built_in">set</span> credentials.existingSecret=<span class="hljs-string">'velero-credentials'</span> \
--<span class="hljs-built_in">set</span> configuration.provider=<span class="hljs-string">'azure'</span> \
--<span class="hljs-built_in">set</span> configuration.backupStorageLocation.bucket=<span class="hljs-variable">$STORAGE_CONTAINER_NAME</span> \
--<span class="hljs-built_in">set</span> configuration.backupStorageLocation.config.resourceGroup=<span class="hljs-variable">$STORAGE_RESOURCE_GROUP</span> \
--<span class="hljs-built_in">set</span> configuration.backupStorageLocation.config.storageAccount=<span class="hljs-variable">$STORAGE_ACCOUNT</span> \
--<span class="hljs-built_in">set</span> configuration.backupStorageLocation.config.subscriptionId=<span class="hljs-variable">$AZURE_SUBSCRIPTION_ID</span> \
--<span class="hljs-built_in">set</span> configuration.volumeSnapshotLocation.name=<span class="hljs-string">'azure-eastus'</span> \
--<span class="hljs-built_in">set</span> configuration.volumeSnapshotLocation.config.resourceGroup=<span class="hljs-variable">$STORAGE_RESOURCE_GROUP</span> \
--<span class="hljs-built_in">set</span> configuration.volumeSnapshotLocation.config.subscriptionId=<span class="hljs-variable">$AZURE_SUBSCRIPTION_ID</span>
</code></pre><p>You should see Velero’s pods come up.</p>
<pre><code>kubectl <span class="hljs-keyword">get</span> pods -n velero
</code></pre><h4 id="heading-testing-the-backup">Testing the backup</h4>
<p>Assuming you installed Velero’s CLI, you can simply create a test backup.</p>
<pre><code>velero <span class="hljs-keyword">backup</span> <span class="hljs-keyword">create</span> my-<span class="hljs-keyword">backup</span>

velero <span class="hljs-keyword">backup</span> <span class="hljs-keyword">logs</span> my-<span class="hljs-keyword">backup</span>
</code></pre><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639619323/DYK9-Zp5D.png" alt="Test backup screenshot with Velero against AKS cluster" /></p>
<p><strong>Note:</strong> you might run into <a target="_blank" href="https://github.com/vmware-tanzu/velero/issues/3160">this issue</a>. If so, then you have to exclude the admission webhook configuration when creating the backup.</p>
<pre><code>velero <span class="hljs-keyword">backup</span> <span class="hljs-keyword">create</span> my-<span class="hljs-keyword">backup</span> <span class="hljs-comment">--exclude-resources MutatingWebhookConfiguration.admissionregistration.k8s.io</span>
<span class="hljs-string">``</span>

<span class="hljs-keyword">If</span> it works, you can <span class="hljs-keyword">go</span> ahead <span class="hljs-keyword">and</span> <span class="hljs-keyword">create</span> a schedule.

<span class="hljs-comment">#### Setting up the schedule</span>

<span class="hljs-string">"Back up my cluster every day at 7 am"</span>

<span class="hljs-string">``</span><span class="hljs-string">`bash
velero schedule create every-day-at-7 --schedule "0 7 * * *"</span>
</code></pre><p><strong>Note</strong>: again, you might run into <a target="_blank" href="https://github.com/vmware-tanzu/velero/issues/3160">this issue</a> and if so then you’ll have to exclude the webhook admission configuration.</p>
<pre><code>velero schedule <span class="hljs-keyword">create</span> every-<span class="hljs-keyword">day</span>-<span class="hljs-keyword">at</span><span class="hljs-number">-7</span> <span class="hljs-comment">--schedule "0 7 * * *" --exclude-resources MutatingWebhookConfiguration.admissionregistration.k8s.io</span>
</code></pre><p>That’s it!</p>
]]></content:encoded></item><item><title><![CDATA[AKS Cost Savings: Stopping dev/test AKS clusters during off hours]]></title><description><![CDATA[Today, it’s possible to stop the virtual machine scale set (vmss) driving an AKS cluster. You can do this in many ways, including the Azure CLI. In this post, I’ll guide you through running an Azure CLI script to stop the vmss of an AKS cluster for d...]]></description><link>https://gaunacode.com/aks-cost-savings-stopping-devtest-aks-clusters-during-off-hours</link><guid isPermaLink="true">https://gaunacode.com/aks-cost-savings-stopping-devtest-aks-clusters-during-off-hours</guid><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Fri, 18 Sep 2020 19:03:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639623702/5U97Wcies.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today, it’s possible to stop the virtual machine scale set (vmss) driving an AKS cluster. You can do this in many ways, including the Azure CLI. In this post, I’ll guide you through running an Azure CLI script to stop the vmss of an AKS cluster for dev/test purposes. We’ll use Azure DevOps pipelines for the scheduling portion since Azure Automation Accounts do not support Azure CLI.</p>
<p>Based on rough calculations, this approach could save you roughly 46% on costs.</p>
<p>Edit: As suspected, Microsoft released a way to start/stop AKS clusters through the CLI, you no longer need to stop the AKS scale sets yourself.</p>
<h2 id="heading-option-a-preferred">Option A (Preferred)</h2>
<p>Microsoft released the preview feature as an Azure CLI extension. Follow the steps on <a target="_blank" href="https://docs.microsoft.com/en-us/azure/aks/start-stop-cluster">this Microsoft docs page</a> on how to install this extension. You can come back to this page to see how to schedule a script to start/stop a cluster during off hours using Azure Pipelines.</p>
<h2 id="heading-option-b-diy">Option B (DIY)</h2>
<h3 id="heading-stopping-the-cluster">Stopping the cluster</h3>
<p>Create a bash script called <code>aks-stop.sh</code>.</p>
<pre><code><span class="hljs-meta">#!/bin/bash</span>

CLUSTER_NAME=<span class="hljs-variable">$1</span>
RESOURCE_GROUP=<span class="hljs-variable">$2</span>

NODE_RESOURCE_GROUP=$(az aks show -g <span class="hljs-variable">$RESOURCE_GROUP</span> -n <span class="hljs-variable">$CLUSTER_NAME</span> --query <span class="hljs-string">"nodeResourceGroup"</span> -o tsv)

az vmss list -g <span class="hljs-variable">$NODE_RESOURCE_GROUP</span> --query <span class="hljs-string">"[].name"</span> -o tsv | <span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> -r scale_set
<span class="hljs-keyword">do</span> 
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Shutting down scaleset in the AKS resource group. Scale Set: <span class="hljs-variable">$scale_set</span>"</span>
    az vmss deallocate -g <span class="hljs-variable">$NODE_RESOURCE_GROUP</span> -n <span class="hljs-variable">$scale_set</span>
<span class="hljs-keyword">done</span>
</code></pre><p>To test it, login to Azure (<code>az login</code>) and the correct subscription (<code>az account set -s [subscription id]</code>.</p>
<p>Invoke the script.</p>
<pre><code>CLUSTER_NAME=<span class="hljs-string">"[your cluster name]"</span>
RESOURCE_GROUP=<span class="hljs-string">"[resource group name]"</span>

<span class="hljs-built_in">source</span> ./aks-stop.sh <span class="hljs-string">"<span class="hljs-variable">$CLUSTER_NAME</span>"</span> <span class="hljs-string">"<span class="hljs-variable">$RESOURCE_GROUP</span>"</span>
</code></pre><p>Once the script runs, a <code>kubectl</code> command will show that the nodes are not ready.</p>
<pre><code>$ kubectl <span class="hljs-keyword">get</span> nodes
<span class="hljs-type">NAME</span>                       STATUS     ROLES   AGE    <span class="hljs-keyword">VERSION</span>
aks-<span class="hljs-keyword">default</span><span class="hljs-number">-1</span>-vmss000000   NotReady   agent   <span class="hljs-number">5</span>d2h   v1<span class="hljs-number">.18</span><span class="hljs-number">.6</span>
aks-<span class="hljs-keyword">default</span><span class="hljs-number">-2</span>-vmss000001   NotReady   agent   <span class="hljs-number">5</span>d2h   v1<span class="hljs-number">.18</span><span class="hljs-number">.6</span>
aks-<span class="hljs-keyword">default</span><span class="hljs-number">-3</span>-vmss000002   NotReady   agent   <span class="hljs-number">5</span>d2h   v1<span class="hljs-number">.18</span><span class="hljs-number">.6</span>
</code></pre><h4 id="heading-what-does-it-do">What does it do?</h4>
<p>It finds the resource group where the underlying vmss are for the AKS cluster. Then it iterates through all the vmss in the resource group and deallocates them.</p>
<p>If you have more than one node pool, it will stop all the node pools.</p>
<h2 id="heading-start-the-vmss">Start the vmss</h2>
<p>Create a bash script called <code>aks-start.sh</code>.</p>
<pre><code><span class="hljs-meta">#!/bin/bash</span>

CLUSTER_NAME=<span class="hljs-variable">$1</span>
RESOURCE_GROUP=<span class="hljs-variable">$2</span>

NODE_RESOURCE_GROUP=$(az aks show -g <span class="hljs-variable">$RESOURCE_GROUP</span> -n <span class="hljs-variable">$CLUSTER_NAME</span> --query <span class="hljs-string">"nodeResourceGroup"</span> -o tsv)

az vmss list -g <span class="hljs-variable">$NODE_RESOURCE_GROUP</span> --query <span class="hljs-string">"[].name"</span> -o tsv | <span class="hljs-keyword">while</span> <span class="hljs-built_in">read</span> -r scale_set
<span class="hljs-keyword">do</span> 
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Starting scaleset in the AKS resource group. Scale Set: <span class="hljs-variable">$scale_set</span>"</span>
    az vmss start -g <span class="hljs-variable">$NODE_RESOURCE_GROUP</span> -n <span class="hljs-variable">$scale_set</span>
<span class="hljs-keyword">done</span>
</code></pre><p>Invoke the script.</p>
<pre><code>CLUSTER_NAME=<span class="hljs-string">"[your cluster name]"</span>
RESOURCE_GROUP=<span class="hljs-string">"[resource group name]"</span>

<span class="hljs-built_in">source</span> ./aks-start.sh <span class="hljs-string">"<span class="hljs-variable">$CLUSTER_NAME</span>"</span> <span class="hljs-string">"<span class="hljs-variable">$RESOURCE_GROUP</span>"</span>
</code></pre><p>Then you will see shortly that the nodes are ready.</p>
<pre><code>$ kubectl <span class="hljs-keyword">get</span> nodes
<span class="hljs-type">NAME</span>                       STATUS   ROLES   AGE    <span class="hljs-keyword">VERSION</span>
aks-<span class="hljs-keyword">default</span><span class="hljs-number">-1</span>-vmss000000   Ready    agent   <span class="hljs-number">5</span>d2h   v1<span class="hljs-number">.18</span><span class="hljs-number">.6</span>
aks-<span class="hljs-keyword">default</span><span class="hljs-number">-2</span>-vmss000001   Ready    agent   <span class="hljs-number">5</span>d2h   v1<span class="hljs-number">.18</span><span class="hljs-number">.6</span>
aks-<span class="hljs-keyword">default</span><span class="hljs-number">-3</span>-vmss000002   Ready    agent   <span class="hljs-number">5</span>d2h   v1<span class="hljs-number">.18</span><span class="hljs-number">.6</span>
</code></pre><h2 id="heading-scheduling-for-off-hours">Scheduling for off-hours</h2>
<p>Here’s how to “stop” your AKS cluster after-hours and then start it in the morning.</p>
<p>Azure Automation accounts unfortunately don’t support the Azure CLI. I find using Azure DevOps very good for this since YAML pipelines support cron-style scheduled triggers.</p>
<p>For example, this is how to trigger a pipeline starting at 6 pm EST. (Note: Azure Pipeline schedules have to be in UTC). For more information on the format, <a target="_blank" href="https://docs.microsoft.com/en-us/azure/devops/pipelines/process/scheduled-triggers?view=azure-devops&amp;tabs=yaml">here’s the detailed documentation</a>.</p>
<pre><code><span class="hljs-attribute">pr</span>: none 
<span class="hljs-attribute">trigger</span>: none
<span class="hljs-attribute">schedules</span>:
- <span class="hljs-attribute">cron</span>: <span class="hljs-string">"0 23 * * 1-5"</span>
  <span class="hljs-attribute">displayName</span>: <span class="hljs-string">"After-hours (11 pm UTC)"</span>
  <span class="hljs-attribute">always</span>: true
  <span class="hljs-attribute">branches</span>:
    <span class="hljs-attribute">include</span>:
    - master
</code></pre><p>Another example, to trigger pipelines at 7 am EST:</p>
<pre><code><span class="hljs-attribute">pr</span>: none 
<span class="hljs-attribute">trigger</span>: none
<span class="hljs-attribute">schedules</span>:
- <span class="hljs-attribute">cron</span>: <span class="hljs-string">"0 11 * * 1-5"</span>
  <span class="hljs-attribute">displayName</span>: <span class="hljs-string">"Mornings (11 am UTC)"</span>
  <span class="hljs-attribute">always</span>: true
  <span class="hljs-attribute">branches</span>:
    <span class="hljs-attribute">include</span>:
    - master
</code></pre><h3 id="heading-putting-it-together">Putting it together</h3>
<p>Here’s an example of a pipeline <strong>starting</strong> the cluster each morning at 7 am EST.</p>
<pre><code><span class="hljs-attribute">pr</span>: none 
<span class="hljs-attribute">trigger</span>: none
<span class="hljs-attribute">schedules</span>:
- <span class="hljs-attribute">cron</span>: <span class="hljs-string">"0 11 * * 1-5"</span>
  <span class="hljs-attribute">displayName</span>: <span class="hljs-string">"Mornings (11 am UTC)"</span>
  <span class="hljs-attribute">always</span>: true
  <span class="hljs-attribute">branches</span>:
    <span class="hljs-attribute">include</span>:
    - master

<span class="hljs-attribute">variables</span>:
  <span class="hljs-attribute">ClusterName</span>: [name of cluster]
  <span class="hljs-attribute">ResourceGroup</span>: [name of resource group]

<span class="hljs-attribute">steps</span>:
  - <span class="hljs-attribute">task</span>: AzureCLI<span class="hljs-variable">@2</span>
    <span class="hljs-attribute">inputs</span>:
      <span class="hljs-attribute">azureSubscription</span>: <span class="hljs-string">'[subscription service connection]'</span>
      <span class="hljs-attribute">scriptType</span>: <span class="hljs-string">'bash'</span>
      <span class="hljs-attribute">scriptLocation</span>: <span class="hljs-string">'scriptPath'</span>
      <span class="hljs-attribute">scriptPath</span>: <span class="hljs-string">'./aks-start.sh'</span>
      <span class="hljs-attribute">arguments</span>: <span class="hljs-string">'"$(ClusterName)" "$(ResourceGroup)"'</span>
</code></pre><p>Here’s an example of a pipeline <strong>stopping</strong> the cluster each morning at 7 pm EST.</p>
<pre><code><span class="hljs-attribute">pr</span>: none 
<span class="hljs-attribute">trigger</span>: none
<span class="hljs-attribute">schedules</span>:
- <span class="hljs-attribute">cron</span>: <span class="hljs-string">"0 23 * * 1-5"</span>
  <span class="hljs-attribute">displayName</span>: <span class="hljs-string">"After-hours (11 pm UTC)"</span>
  <span class="hljs-attribute">always</span>: true
  <span class="hljs-attribute">branches</span>:
    <span class="hljs-attribute">include</span>:
    - master

<span class="hljs-attribute">variables</span>:
  <span class="hljs-attribute">ClusterName</span>: [name of cluster]
  <span class="hljs-attribute">ResourceGroup</span>: [name of resource group]

<span class="hljs-attribute">steps</span>:
  - <span class="hljs-attribute">task</span>: AzureCLI<span class="hljs-variable">@2</span>
    <span class="hljs-attribute">inputs</span>:
      <span class="hljs-attribute">azureSubscription</span>: <span class="hljs-string">'[subscription service connection]'</span>
      <span class="hljs-attribute">scriptType</span>: <span class="hljs-string">'bash'</span>
      <span class="hljs-attribute">scriptLocation</span>: <span class="hljs-string">'scriptPath'</span>
      <span class="hljs-attribute">scriptPath</span>: <span class="hljs-string">'./aks-stop.sh'</span>
      <span class="hljs-attribute">arguments</span>: <span class="hljs-string">'"$(ClusterName)" "$(ResourceGroup)"'</span>
</code></pre>]]></content:encoded></item><item><title><![CDATA[AKS Cluster Security: Process Linux node updates/reboots using Kured]]></title><description><![CDATA[Microsoft does download and install security patches automatically and transparently on AKS worker nodes. But, by design it’s your responsibility to restart them. Once you do so, these patches will take effect. So it’s really important that you do so...]]></description><link>https://gaunacode.com/aks-cluster-security-process-linux-node-updatesreboots-using-kured</link><guid isPermaLink="true">https://gaunacode.com/aks-cluster-security-process-linux-node-updatesreboots-using-kured</guid><category><![CDATA[Azure]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Thu, 17 Sep 2020 19:33:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639628455/ynxnltoxf.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Microsoft does download and install security patches automatically and transparently on AKS worker nodes. But, by design it’s your responsibility to restart them. Once you do so, these patches will take effect. So it’s really important that you do so to keep clusters up-date. It’s considered <a target="_blank" href="https://docs.microsoft.com/en-us/azure/aks/operator-best-practices-cluster-security#process-linux-node-updates-and-reboots-using-kured">best-practice</a>.</p>
<p>There’s a project called Kured. It’s a daemonset that restarts nodes when it detects new updates. It can integrate with tools like Slack to notify you when a restart happens. You are also able to specify a schedule on when a restart is allowed.</p>
<h2 id="heading-installing-kured-through-helm">Installing Kured through Helm</h2>
<p>There’s a couple of ways to install <a target="_blank" href="https://github.com/weaveworks/kured">Kured</a>. My preferred way is through a <a target="_blank" href="https://github.com/weaveworks/kured/tree/master/charts/kured">Helm chart</a>.</p>
<pre><code>helm repo <span class="hljs-keyword">add</span> kured https://weaveworks.github.io/kured
helm install my-<span class="hljs-keyword">release</span> kured/kured
</code></pre><p>But, realistically you’re going to want to customize it by setting a schedule, alerts, or notifications.</p>
<h2 id="heading-setting-a-schedule">Setting a schedule</h2>
<p>You can configure Kured to only restart through certain windows. Useful to avoid downtime during peak hours.</p>
<pre><code>  /usr/bin/kured \
  <span class="hljs-comment">--reboot-days mon,tue,wed,thu,fri \</span>
  <span class="hljs-comment">--start-time 5pm \</span>
  <span class="hljs-comment">--end-time 12am \</span>
  <span class="hljs-comment">--time-zone America/New_York</span>
</code></pre><p>Through a Helm chart, this becomes:</p>
<pre><code>helm install kured kured/kured -n [some <span class="hljs-keyword">namespace</span> <span class="hljs-keyword">for</span> kured]\
--<span class="hljs-built_in">set</span> configuration.rebootDays=<span class="hljs-string">"{mon,tue,wed,thu,fri}"</span> \
--<span class="hljs-built_in">set</span> configuration.startTime=<span class="hljs-number">5</span>pm \
--<span class="hljs-built_in">set</span> configuration.endTime=<span class="hljs-number">11</span>:<span class="hljs-number">59</span>pm \
--<span class="hljs-built_in">set</span> configuration.timeZone=America/New_York
</code></pre><h2 id="heading-setting-up-slack-notifications">Setting up Slack notifications</h2>
<p>A good use of the Slack username is the name of the environment. In this example, I use <code>staging</code>. By the way, here’s some instructions on how to set-up <a target="_blank" href="https://api.slack.com/messaging/webhooks">Slack webhooks</a>. Unfortunately, there’s no integration with Microsoft Teams.</p>
<pre><code>helm install kured kured/kured -n [some <span class="hljs-keyword">namespace</span> <span class="hljs-keyword">for</span> kured]\
--<span class="hljs-built_in">set</span> configuration.slackChannel=environments \
--<span class="hljs-built_in">set</span> configuration.slackHookUrl=https:<span class="hljs-comment">//hooks.slack.com/services/blahblah \</span>
--<span class="hljs-built_in">set</span> configuration.slackUsername=staging
</code></pre><h2 id="heading-testing-kured">Testing Kured</h2>
<p>To test on AKS, you’re going to want to follow <a target="_blank" href="https://docs.microsoft.com/en-us/azure/aks/ssh">these instructions</a> to SSH into a worker node.</p>
<p>You’ll have to create a file on the node to indicate to Kured to restart the node. By default, daemon sets check for restarts once an hour. But, each pod on the daemon set offsets from the start time, so <em>you won’t necessarily see all restarts close one after another</em>.</p>
<pre><code>sudo touch /<span class="hljs-keyword">var</span>/run/reboot-<span class="hljs-keyword">required</span>
</code></pre><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639626960/SSGjMNN6v.png" alt="Kured slack notifications example" /></p>
<h2 id="heading-kured-gitops-flux">Kured + GitOps (Flux)</h2>
<p>When using GitOps, the Kubernetes cluster pulls deployments from a Git repository. The Git repository is the source of truth. Therefore, you can declaratively define what Helm charts should be installed in your cluster by committing a file like this one to your manifests repo:</p>
<pre><code><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">helm.fluxcd.io/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">HelmRelease</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">kured</span>
  <span class="hljs-attr">namespace:</span> <span class="hljs-string">kured</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">releaseName:</span> <span class="hljs-string">kured</span>
  <span class="hljs-attr">chart:</span>
    <span class="hljs-attr">repository:</span> <span class="hljs-string">https://weaveworks.github.io/kured</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">kured</span>
    <span class="hljs-attr">version:</span> <span class="hljs-number">2.2</span><span class="hljs-number">.0</span>
  <span class="hljs-attr">valuesFrom:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">secretKeyRef:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">slack-secrets</span>
      <span class="hljs-attr">namespace:</span> <span class="hljs-string">kured</span>
      <span class="hljs-attr">key:</span> <span class="hljs-string">values.yaml</span>
      <span class="hljs-attr">optional:</span> <span class="hljs-literal">false</span>
</code></pre><p>In addition, you can use <a target="_blank" href="https://github.com/bitnami-labs/sealed-secrets">sealed secrets</a> to also define information you don’t necessarily want committed to source control. Then, you can reference this secrets from Flux’s <code>HelmRelease</code> resource through a <code>secretKeyRef</code>.</p>
<h2 id="heading-what-about-alerting">What about alerting?</h2>
<p>In a future posts, I’ll explore how to integrate Kured with Prometheus.</p>
]]></content:encoded></item><item><title><![CDATA[Enabling copy/paste on an Ubuntu VM on VMware Workstation]]></title><description><![CDATA[This is a bit of a miscellaneous post. I’ve had some trouble making my copy/paste work on a Ubuntu VM and hopefully it will help someone.
Installing VMware tools
First, make sure you have VMware tools installed.
sudo service vmware-tools status
If it...]]></description><link>https://gaunacode.com/enabling-copypaste-on-an-ubuntu-vm-on-vmware-workstation</link><guid isPermaLink="true">https://gaunacode.com/enabling-copypaste-on-an-ubuntu-vm-on-vmware-workstation</guid><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Mon, 31 Aug 2020 13:52:03 GMT</pubDate><content:encoded><![CDATA[<p>This is a bit of a miscellaneous post. I’ve had some trouble making my copy/paste work on a Ubuntu VM and hopefully it will help someone.</p>
<h2 id="heading-installing-vmware-tools">Installing VMware tools</h2>
<p>First, make sure you have VMware tools installed.</p>
<pre><code><span class="hljs-attribute">sudo</span> service vmware-tools status
</code></pre><p>If it’s not installed, then first make sure you <strong>don’t</strong> have the open-vm-tools installed.</p>
<pre><code>sudo service <span class="hljs-keyword">open</span>-vm-tools status
</code></pre><p>If you do have it installed, uninstall it.</p>
<pre><code>sudo apt-<span class="hljs-keyword">get</span> remove <span class="hljs-keyword">open</span>-vm-tools
sudo apt-<span class="hljs-keyword">get</span> remove <span class="hljs-comment">--auto-remove open-vm-tools</span>
sudo apt-<span class="hljs-keyword">get</span> purge <span class="hljs-keyword">open</span>-vm-tools
sudo apt-<span class="hljs-keyword">get</span> purge <span class="hljs-comment">--auto-remove open-vm-tools</span>
sudo reboot
</code></pre><p>Then, after your computer reboots, you can install the VMware tools. If you didn't have open-vm-tools, you can skip to this step.</p>
<p>Click on Virtual Machine &gt; Install / Update / Reinstall VMware Tools. Extract the VMware Tools tar to the Desktop and follow the below commands.</p>
<pre><code>cd Desktop/vmware-tools-distrib
Run <span class="hljs-keyword">this</span> command to install VMware Tools:
sudo ./vmware-install.pl
</code></pre><p>Follow the prompts to install. I picked the defaults.</p>
<p>Reboot your computer and copy/paste should work with the default window manager.</p>
<h2 id="heading-i3">i3</h2>
<p>If you use i3 as a window manager, go to the i3 config at <code>~/.config/i3/config</code> and add the following line at the end. (This assumes you already had an i3 configuration file created).</p>
<pre><code><span class="hljs-keyword">exec</span> --<span class="hljs-keyword">no</span>-startup-id vmware-user
</code></pre><p>Log out and log back in.</p>
<p>That’s it! Goodluck!</p>
]]></content:encoded></item><item><title><![CDATA[Taking CKA/CKAD as a previous .NET developer]]></title><description><![CDATA[Last week, I got the great news that I passed the Certified Kubernetes Administrators (CKA) exam. A few months ago, I had passed the Certified Kubernetes Application Developer (CKAD) exam. I found both exams to be challenging, yet fun and rewarding. ...]]></description><link>https://gaunacode.com/taking-ckackad-as-a-previous-net-developer</link><guid isPermaLink="true">https://gaunacode.com/taking-ckackad-as-a-previous-net-developer</guid><category><![CDATA[Devops]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Tue, 18 Aug 2020 10:30:25 GMT</pubDate><content:encoded><![CDATA[<p>Last week, I got the great news that I passed the Certified Kubernetes Administrators (CKA) exam. A few months ago, I had passed the Certified Kubernetes Application Developer (CKAD) exam. I found both exams to be challenging, yet fun and rewarding. This post shares my experience and my take on how to get ready as a .NET developer.</p>
<p>I think prepping for the CKA or CKA without any past Linux experience will be hard - harder than most other types of developers/admins. So, be patient and diligent. I love Kubernetes and I think it’s the present and future. Yeah, I think it’s worth it.</p>
<h2 id="heading-background">Background</h2>
<p>I was once a traditional .NET developer working at an enterprise. I used Windows and Visual Studio Code to build solutions using C# and .NET. In that realm, I rarely used the command line and I lived in Visual Studio, <em>clicking around</em>. It wasn’t until Node and SPA frameworks like Angular that started to become very popular where I started to use the command line more.</p>
<h2 id="heading-the-pre-requisites">The Pre-Requisites</h2>
<p>It was invaluable to be comfortable with the <strong>command line</strong> and with Docker well before taking the exam. Years before taking the exams, I have been using Docker and PowerShell in production. I highly recommend <a target="_blank" href="https://app.pluralsight.com/library/courses/everyday-ps/table-of-contents">this course</a> to get you acquainted to using PowerShell in every day workflows.</p>
<p>What’s also useful is an understanding of <strong>Unix basics</strong>. Windows and Unix/Linux have very different methodologies. Having a context around Unix/Linux is extremely beneficial to understand - even if you just want to be a developer building app on Kubernetes. A friend of mine gave me <a target="_blank" href="https://www.amazon.com/Linux-Command-Line-2nd-Introduction/dp/1593279523/ref=sr_1_3?crid=1WSGF5Q0OO0V0&amp;dchild=1&amp;keywords=the+linux+command+line&amp;qid=1597757164&amp;sprefix=The+Linux+Command%2Caps%2C163&amp;sr=8-3">this book</a>, it’s a warm introduction into this world.</p>
<p>Lastly, having a strong understanding of Docker is fundamental. It will only be useful in building containerized apps. I recommend all of Nigel’s courses, including the Kubernetes ones:</p>
<ol>
<li><a target="_blank" href="https://app.pluralsight.com/library/courses/docker-getting-started/table-of-contents">Docker Getting Started</a></li>
<li><a target="_blank" href="https://app.pluralsight.com/library/courses/docker-deep-dive-update/table-of-contents">Docker Deep Dive</a></li>
<li><a target="_blank" href="https://app.pluralsight.com/library/courses/docker-kubernetes-big-picture/table-of-contents">Kubernetes Fundamentals</a></li>
</ol>
<h2 id="heading-the-ckad">The CKAD</h2>
<p>I think, the most important aspect of taking the exam is time-management. Questions are weighted (they show you the weight) and so it’s very risky to spend a lot of time on a questions that is not weighted high. I did not pay attention to this during the CKAD.</p>
<p>If you read other experiences and hear other opinions, most will say that the CKA is the hardest. In my experience, CKAD was the hardest. Both tests are hands-on, night and day difference from the multiple-choice/case-study questions from the Azure certifications.</p>
<p>The test was 2 hours long and I ran out of time. I did not get to finish two questions. I got an 84 on the exam.</p>
<p>Before the test, I spent a little less than a week preparing. My preparation was <a target="_blank" href="https://www.udemy.com/course/certified-kubernetes-administrator-with-practice-tests/">Mumshad’s course on Udemy</a>. The practice exams were amazing.</p>
<p>Practice practice practice. During the practice, I felt like I was sharpening my Vim skills than anything else. Vim is essential to the test because you don’t have other editors given during the test.</p>
<p>In the end, I think I had a harder time with the CKAD because I ran out of time and I had less practice than when taking the CKA.</p>
<h2 id="heading-the-cka">The CKA</h2>
<p>During the CKA, I finished an hour early and had time to go back to two questions I half-answered. In the end, I ended the exam half-hour early after having gone through all my answers. I think the difference was the amount of practice.</p>
<p>Leading up to the exam, I also used <a target="_blank" href="https://www.udemy.com/course/certified-kubernetes-application-developer/">Mumshad’s course on Udemy</a>. This time, I practiced and practiced and practiced. I ran through the lightening test 5 times and each of the practice tests two times. I also went through Kubernetes the Hard Way, slowed down to learn Vim shortcuts along the way, and installed a cluster using Kubeadm.</p>
<h2 id="heading-the-miscellaneous">The miscellaneous</h2>
<p>I received the CKAD test results within two hours. But, for the CKA, it was a full 48 hours. A couple of days after you get the certificate, you’re also awarded the pretty badges.</p>
<p>As part of taking the exams, I had to sign an agreement that I would not discuss the questions in the exam. But, during Mumshad’s courses, he explains a lot of useful tips when taking the exam like using <em>imperative</em> commands instead of bothering with creating YAML files to save time.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Both exams are very challenging and rewarding. Please note, as part of my day job, I used Docker and Kubernetes in production for a while before taking the exams. The problem I had with learning something organically (based on need), that I had knowledge gaps. Everyone has them. But, getting ready for the exams helped me find those gaps and fill them.</p>
<p>If you’re a traditional .NET developer and want to jump on the cloud-native/Kubernetes bandwagon, you have a lot of work to do. This is especially true if you don’t have Linux/scripting/Docker skills. Personally, I think it’s one of the best things you could do for your career. Take it slow.</p>
<p>Too many times have I seen teams adopt too much at once. Teams that were once stagnant with old tech <em>now all the suddenly</em> use Azure, Kubernetes, DevOps, microservices, OpenID connect, and .NET core. It doesn’t end well. You can only learn so much in a given time. You either cut corners or learn things superficially. Just because you deployed a cluster with <code>az aks create</code>, doesn’t mean you’re ready to run things in production. There’s a lot to it and you will run into gaping holes that you can’t address. <code>&lt;/soapbox&gt;</code></p>
<p>Reach out if you want help with your prep. Happy to help.</p>
]]></content:encoded></item><item><title><![CDATA[Terraform input variables using Azure DevOps]]></title><description><![CDATA[Pipeline variables in Azure DevOps YAML pipelines get mapped to environment variables in the pipeline agent. With Terraform, if there’s an environment variable that starts with TF_VAR_ it will get mapped to an input variable. Combining the two can me...]]></description><link>https://gaunacode.com/terraform-input-variables-using-azure-devops</link><guid isPermaLink="true">https://gaunacode.com/terraform-input-variables-using-azure-devops</guid><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Sat, 04 Apr 2020 03:52:49 GMT</pubDate><content:encoded><![CDATA[<p>Pipeline variables in Azure DevOps YAML pipelines get mapped to environment variables in the pipeline agent. With Terraform, if there’s an environment variable that starts with <code>TF_VAR_</code> it will get mapped to an input variable. Combining the two can mean more succinct pipelines.</p>
<h2 id="heading-azure-devops">Azure DevOps</h2>
<p>So, variables in Azure DevOps pipelines get mapped to environment variable. There’s one catch, it will always be uppercase.</p>
<pre><code><span class="hljs-attribute">variables</span>:
  <span class="hljs-attribute">someVariable</span>: <span class="hljs-string">'someValue'</span>
</code></pre><p>The variable <code>someVariable</code> becomes an environment variable named <code>SOMEVARIABLE</code>.</p>
<p>There is also another catch. Secrets won’t get mapped to environment variables for security reasons.</p>
<pre><code><span class="hljs-attribute">variables</span>:
  <span class="hljs-attribute">someVariable</span>: <span class="hljs-string">'$(SomeConnectionString)'</span>
</code></pre><p>Assuming that <code>SomeConnectionString</code> is a pipeline variable stored as a secret, it will not get mapped to the pipeline variable. Therefore, there will be no environment variable to use.</p>
<h2 id="heading-terraform">Terraform</h2>
<p>Let’s say you have the following input variables defined in Terraform.</p>
<pre><code>variable <span class="hljs-string">"app_name"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"location"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"environment"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}
</code></pre><p>When we seek to perform a <code>terraform plan</code> and then <code>terraform apply</code>, we can specify the values of the variables using the <code>-var</code> flag.</p>
<pre><code>terraform apply -<span class="hljs-keyword">var</span> <span class="hljs-string">"app_name=someapp"</span>
</code></pre><p>Also, we can specify its value by defining an environment variable that starts with <code>TF_VAR_</code>. This way, there’s no need to override with flags.</p>
<pre><code><span class="hljs-comment"># Assuming an environment variable TF_VAR_app_name is already defined</span>
<span class="hljs-attribute">terraform</span> apply
</code></pre><h2 id="heading-combining-the-two">Combining the two</h2>
<p>So, if we define Azure DevOps pipeline variables with a prefix of <code>TF_VAR_</code>, they will get mapped into environment variables that Terraform will pick them up.</p>
<pre><code><span class="hljs-attribute">variables</span>: 
  <span class="hljs-attribute">TF_VAR_APP_NAME</span>: <span class="hljs-string">'someapp'</span>
</code></pre><p>Then, in your Terraform configuration, you have to define the variables with capital letters.</p>
<p>Why? Because Azure DevOps will always transform pipeline variables to uppercase environment variables.</p>
<pre><code>variable <span class="hljs-string">"APP_NAME"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}
</code></pre><p>Except, this won’t work for Azure DevOps pipeline variables saved as secrets. For secrets, you will still have to pass them as variables. For example:</p>
<pre><code><span class="hljs-selector-tag">-</span> <span class="hljs-selector-tag">task</span>: <span class="hljs-selector-tag">TerraformTaskV1</span><span class="hljs-keyword">@0</span>
  <span class="hljs-attribute">displayName:</span> <span class="hljs-string">"Terraform Plan"</span>
  <span class="hljs-attribute">inputs:</span>
    <span class="hljs-attribute">provider:</span> <span class="hljs-string">'azurerm'</span>
    <span class="hljs-attribute">command:</span> <span class="hljs-string">'plan'</span>
    <span class="hljs-attribute">workingDirectory:</span> <span class="hljs-string">'$(System.DefaultWorkingDirectory)/iac/'</span>
    <span class="hljs-attribute">environmentServiceNameAzureRM:</span> $(AzureServiceConnection)
    <span class="hljs-attribute">commandOptions:</span> -input=false -var <span class="hljs-string">"VM_ADMIN_PASSWORD=$(TF_VAR_admin_password)"</span>
</code></pre><p><code>TF_VAR_admin_password</code> is a pipeline variable saved as a secret.</p>
]]></content:encoded></item><item><title><![CDATA[Install IIS on Azure VM using Terraform]]></title><description><![CDATA[Creating a blank VM on Azure is easy, especially from the portal. Installing software and enabling features on each new VM can be time consuming, not to mention error-prone. Similar to yesterday, I will show how to install IIS on a Windows VM using T...]]></description><link>https://gaunacode.com/install-iis-on-azure-vm-using-terraform</link><guid isPermaLink="true">https://gaunacode.com/install-iis-on-azure-vm-using-terraform</guid><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Thu, 02 Apr 2020 17:28:00 GMT</pubDate><content:encoded><![CDATA[<p>Creating a blank VM on Azure is easy, especially from the portal. Installing software and enabling features on each new VM can be time consuming, not to mention error-prone. Similar to <a target="_blank" href="https://gaunacode.com/provisioning-a-vm-with-an-azure-devops-deployment-group-agent-with-terraform">yesterday</a>, I will show how to install IIS on a Windows VM using Terraform.</p>
<p>In order to install IIS on a new Windows VM, we’ll use a simple powershell script. The command is:</p>
<pre><code><span class="hljs-keyword">Install</span>-WindowsFeature -<span class="hljs-keyword">Name</span> Web-<span class="hljs-keyword">Server</span> -IncludeAllSubFeature -IncludeManagementTools
</code></pre><p>This Powershell command installs IIS, all it’s sub features, and IIS Management tools.</p>
<p>We can execute this script from an Azure VM as it’s being provisioned using the <a target="_blank" href="https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-windows">virtual machine custom script extension</a>. As the VM is being provisioned, this script will be run and the state of the VM won’t show as “running” until the custom script finishes.</p>
<p>To invoke this custom script with Terraform, it’s quite simple.</p>
<pre><code>resource "azurerm_virtual_machine_extension" "vm_extension_install_iis" {
  <span class="hljs-type">name</span>                       = "vm_extension_install_iis"
  virtual_machine_id         = azurerm_windows_virtual_machine.vm.id
  publisher                  = "Microsoft.Compute"
  <span class="hljs-keyword">type</span>                       = "CustomScriptExtension"
  type_handler_version       = "1.8"
  auto_upgrade_minor_version = <span class="hljs-keyword">true</span>

  settings = &lt;&lt;SETTINGS
    {
        "commandToExecute": "powershell -ExecutionPolicy Unrestricted Install-WindowsFeature -Name Web-Server -IncludeAllSubFeature -IncludeManagementTools"
    }
SETTINGS
}
</code></pre><p>The only weird syntax is the <code>settings</code> object. It’s raw json, so you can provide the same parameters as specified on the <a target="_blank" href="https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-windows#extension-schema">custom script extension documentation</a>.</p>
<p>You can also provide <code>protectedSettings</code>. This is for more sensitive items like the storage account key. Why? You could choose to store an custom script <em>file</em> on a storage account. This would be especially useful if you want to do more than just enable IIS.</p>
<pre><code>resource "azurerm_virtual_machine_extension" "vm_extension_install_iis" {
  <span class="hljs-type">name</span>                       = "vm_extension_install_iis"
  virtual_machine_id         = azurerm_windows_virtual_machine.vm.id
  publisher                  = "Microsoft.Compute"
  <span class="hljs-keyword">type</span>                       = "CustomScriptExtension"
  type_handler_version       = "1.8"
  auto_upgrade_minor_version = <span class="hljs-keyword">true</span>

  settings = &lt;&lt;SETTINGS
    {
        "commandToExecute": "powershell -ExecutionPolicy Unrestricted Install-WindowsFeature -Name Web-Server -IncludeAllSubFeature -IncludeManagementTools"
    }
SETTINGS

protected_settings = &lt;&lt;PROTECTED_SETTINGS
    {
        "commandToExecute": "myExecutionCommand",
        "storageAccountName": "myStorageAccountName",
        "storageAccountKey": "myStorageAccountKey",
        "managedIdentity" : {}
    }
PROTECTED_SETTINGS
}
</code></pre><p>That’s it!</p>
<p>If you’re get errors with the custom script extension, there’s a set of <a target="_blank" href="https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/custom-script-windows#troubleshoot-and-support">troubleshooting tips</a> published by Microsoft.</p>
]]></content:encoded></item><item><title><![CDATA[Create a VM with a deployment group agent using Terraform]]></title><description><![CDATA[One of the advantages of PaaS to IaaS is how easy they are to deploy to. If you have to use IaaS, you’d still want to use Infrastructure as Code and CI/CD pipelines.
In this guide, I will show how to provision a VM on Azure with the Azure DevOps depl...]]></description><link>https://gaunacode.com/create-a-vm-with-a-deployment-group-agent-using-terraform</link><guid isPermaLink="true">https://gaunacode.com/create-a-vm-with-a-deployment-group-agent-using-terraform</guid><category><![CDATA[Azure]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Thu, 02 Apr 2020 12:37:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639643143/qqslaVzWW.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>One of the advantages of PaaS to IaaS is how easy they are to deploy to. If you have to use IaaS, you’d still want to use Infrastructure as Code and CI/CD pipelines.</p>
<p>In this guide, I will show how to provision a VM on Azure with the Azure DevOps deployment group agent pre-installed. When the VM comes up, it will also register against an Azure DevOps deployment group.</p>
<h2 id="heading-background">Background</h2>
<p><a target="_blank" href="https://docs.microsoft.com/en-us/azure/devops/pipelines/release/deployment-groups/?view=azure-devops">Deployment groups</a> allow you to deploy to on-prem servers or IaaS workloads on any cloud. It’s a group of machines that have Azure DevOps agents installed on them and they communicate outbound with Azure DevOps. You can define a release to a deployment group that will run a set of tasks. For example, you can deploy to IIS, copy files to the machine, deploy Windows Services, and other things.</p>
<h2 id="heading-set-up">Set-Up</h2>
<h3 id="heading-creating-the-deployment-group">Creating the Deployment Group</h3>
<p>First, define a deployment group to deploy to. Pick an Azure DevOps Team Project, and go Pipelines. From pipelines, select Deployment Groups.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639643143/qqslaVzWW.jpeg" alt /></p>
<p>Create a new deployment group. I called mine <code>test</code>.</p>
<h3 id="heading-creating-a-new-pat">Creating a new PAT</h3>
<p>Next, you’ll need a <a target="_blank" href="https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&amp;tabs=preview-page">Personal Access Token (PAT)</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639644757/MPAUosOxf.jpeg" alt /></p>
<p>Next, create the new token. Choose the expiration date that works for your and give it a descriptive name. Once you create it, you won't be able to retrieve the value. So, make sure you copy it or don’t close the browser tab until you finish this guide.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639646241/J-vYyOPn4.jpeg" alt /></p>
<h2 id="heading-terraform">Terraform</h2>
<p>Here’s some quick configuration on provisioning a simple Windows Server VM. I called my file <code>vm.tf</code>.</p>
<pre><code>resource "azurerm_virtual_network" "virtual_network" {
  <span class="hljs-type">name</span>                = var.vnet_name
  address_space       = ["10.0.0.0/16"]
  <span class="hljs-keyword">location</span>            = var.<span class="hljs-keyword">location</span>
  resource_group_name = azurerm_resource_group.resource_group.name
}

resource "azurerm_subnet" "main_subnet" {
  <span class="hljs-type">name</span>                 = "subnet1"
  resource_group_name  = azurerm_resource_group.resource_group.name
  virtual_network_name = azurerm_virtual_network.virtual_network.name
  address_prefix       = "10.0.2.0/24"
}

resource "azurerm_network_interface" "nic" {
  <span class="hljs-type">name</span>                = "nsg-${var.vnet_name}"
  <span class="hljs-keyword">location</span>            = var.<span class="hljs-keyword">location</span>
  resource_group_name = azurerm_resource_group.resource_group.name

  ip_configuration {
    <span class="hljs-type">name</span>                          = "internal"
    subnet_id                     = azurerm_subnet.main_subnet.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_windows_virtual_machine" "vm" {
  <span class="hljs-type">name</span>                = var.vm_name
  resource_group_name = azurerm_resource_group.resource_group.name
  <span class="hljs-keyword">location</span>            = var.<span class="hljs-keyword">location</span>
  size                = "Standard_F2"
  admin_username      = var.vm_admin_username
  admin_password      = var.vm_admin_password
  network_interface_ids = [
    azurerm_network_interface.nic.id,
  ]

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2016-Datacenter"
    version   = "latest"
  }
}
</code></pre><p>Here’s the variables you’ll need to define.</p>
<pre><code>variable <span class="hljs-string">"vm_name"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"vnet_name"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"location"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"vm_admin_username"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"vm_admin_password"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"azure_devops_organization"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"azure_devops_teamproject"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"azure_devops_deploymentgroup"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"azure_devops_agentname"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}

variable <span class="hljs-string">"azure_devops_pat"</span> {
  <span class="hljs-keyword">type</span> = <span class="hljs-keyword">string</span>
}
</code></pre><h3 id="heading-deployment-group-agent">Deployment Group Agent</h3>
<p>In order to install the deployment group agent to this VM, we must use the <a target="_blank" href="https://docs.microsoft.com/en-us/azure/devops/pipelines/release/deployment-groups/howto-provision-deployment-group-agents?view=azure-devops#use-the-azure-resource-group-deployment-task">Azure DevOps deployment group VM extension</a>.</p>
<pre><code>resource <span class="hljs-string">"azurerm_virtual_machine_extension"</span> <span class="hljs-string">"vm_extension"</span> {
  name                 = <span class="hljs-string">"deploymentgroup_agent"</span>
  virtual_machine_id   = azurerm_windows_virtual_machine.vm.id
  publisher            = <span class="hljs-string">"Microsoft.VisualStudio.Services"</span>
  <span class="hljs-built_in">type</span>                 = <span class="hljs-string">"TeamServicesAgent"</span>
  type_handler_version = <span class="hljs-string">"1.0"</span>
  auto_upgrade_minor_version = <span class="hljs-literal">true</span>

  settings = &lt;&lt;SETTINGS
    {
        <span class="hljs-string">"VSTSAccountName"</span>: <span class="hljs-string">"<span class="hljs-variable">${ var.azure_devops_organization }</span>"</span>,
        <span class="hljs-string">"TeamProject"</span>: <span class="hljs-string">"<span class="hljs-variable">${ var.azure_devops_teamproject }</span>"</span>,
        <span class="hljs-string">"DeploymentGroup"</span>: <span class="hljs-string">"<span class="hljs-variable">${ var.azure_devops_deploymentgroup }</span>"</span>,
        <span class="hljs-string">"AgentName"</span>: <span class="hljs-string">"<span class="hljs-variable">${ var.azure_devops_agentname }</span>"</span>
    }
SETTINGS

  protected_settings = &lt;&lt;PROTECTED_SETTINGS
    {
      <span class="hljs-string">"PATToken"</span>: <span class="hljs-string">"<span class="hljs-variable">${ var.azure_devops_pat }</span>"</span>
    }
PROTECTED_SETTINGS
}
</code></pre><p>The <code>settings</code> are parameters that the VM extension needs in order to register to the proper deployment group. The <code>protected_settings</code> are more sensitive settings, like the PAT.</p>
<p>Here’s what they should be:</p>
<ul>
<li><code>VSTSAccountName</code> (<code>azure_devops_organization</code>) - The name of your Azure DevOps organization. For example, if you use <code>https://dev.azure.com/mycompany</code>, then the name of your org is <code>mycompany</code>.</li>
<li><code>TeamProject</code> (<code>azure_devops_teamproject</code>) - The Azure DevOps team project you’ll be using to release from.</li>
<li><code>DeploymentGroup</code> (<code>azure_devops_deploymentgroup</code>) - The deployment group agent you created above in “Set-Up”.</li>
<li><code>AgentName</code> (<code>azure_devops_agentname</code>) - The name of the agent. It could be useful to name this after the vm.</li>
</ul>
<p>That’s it! Once you <code>terraform plan</code> and <code>apply</code> you’ll have a new VM that has registered to Azure DevOps. Give it several minutes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639648102/_iGQHp485O.jpeg" alt /></p>
]]></content:encoded></item><item><title><![CDATA[Azure App Service Hybrid Connection Performance - Part 1]]></title><description><![CDATA[This is the first post in a series to try to determine roughly the performance of Azure App Service Hybrid Connections. I’ll create a quick experiment to measure any latency introduced by hybrid connections in comparison to an direct connection and a...]]></description><link>https://gaunacode.com/azure-app-service-hybrid-connection-performance-part-1</link><guid isPermaLink="true">https://gaunacode.com/azure-app-service-hybrid-connection-performance-part-1</guid><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Wed, 01 Apr 2020 03:34:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639652004/b7HDfU1Zh.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the first post in a series to try to determine roughly the performance of Azure App Service Hybrid Connections. I’ll create a quick experiment to measure any latency introduced by hybrid connections in comparison to an direct connection and also app service vnet integration.</p>
<h2 id="heading-the-hypothesis">The hypothesis</h2>
<p>I anticipate that there is anywhere from 100-500 ms latency added by simply choosing to use Azure App Service Hybrid Connections. I also anticipate that the Azure App Service vnet integration is faster than hybrid connections.</p>
<h2 id="heading-the-experiment">The experiment</h2>
<p>For the experiment, I’ll test three cases. First, I’ll measure the performance of an Azure App Service hosting an API that issues requests to another API. This other API will be hosted on a VM through IIS and listening on port <code>80</code>. In this case, this other API will be publicly accessible from the internet.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639652004/b7HDfU1Zh.jpeg" alt="Experiment Test Case #1" />Using a direct connection through internet</p>
<p>Secondly, I’ll measure the performance of the same Azure App Service hosting the API issuing requests to the second API. This other API will remain hosted through IIS on a VM and listening on port 80. In this case, the first API will connect to the second API using hybrid connections.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639653447/-5sigpoba.jpeg" alt="Experiment Test Case #2" />Using Hybrid Connections</p>
<p>Third, I’ll measure the performance of the App Service connecting to the second API using Azure App Service vnet integration. The second API will not be publicly accessible and instead it will be accessible only through from within the VNet.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1637639655103/K8WWdc56v.jpeg" alt="Experiment Test Case #3" />Using app service VNet integration</p>
]]></content:encoded></item><item><title><![CDATA[Azure Hybrid Connections]]></title><description><![CDATA[On of the big hold-ups in migration applications to Azure can be on-prem dependencies. Especially when looking to leverage PaaS, dependencies on to on-prem databases or services can be showstoppers. Azure Hybrid Connections are an easy way to add con...]]></description><link>https://gaunacode.com/azure-hybrid-connections</link><guid isPermaLink="true">https://gaunacode.com/azure-hybrid-connections</guid><category><![CDATA[Azure]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Tue, 31 Mar 2020 03:14:41 GMT</pubDate><content:encoded><![CDATA[<p>On of the big hold-ups in migration applications to Azure can be on-prem dependencies. Especially when looking to leverage PaaS, dependencies on to on-prem databases or services can be showstoppers. Azure Hybrid Connections are an easy way to add connectivity from PaaS services to dependencies outside of Azure.</p>
<h2 id="heading-background">Background</h2>
<p><a target="_blank" href="https://docs.microsoft.com/en-us/azure/app-service/app-service-hybrid-connections">Azure App Service Hybrid Connections</a> allows Azure Web Apps and Azure Functions to leverage resources in other networks. To the app service, each Hybrid Connection is mapped to a single TCP host and port combination. Meaning, a database or service on your on-prem network to an app service.</p>
<p>Azure Hybrid Connections leverage one of the oldest services on Azure, Service Bus Relays. Yes, underneath the covers, this Azure service leverage an Azure Service Bus. From one end, the PaaS Azure service connects to a Hybrid Connection resource. From the other end, there is a Hybrid Connection Manager that is installed on the target environment, such as on-prem. This Hybrid Connection Manager connects outbound to the Azure Service Bus .</p>
<h2 id="heading-the-pros">The Pros</h2>
<p><strong>Security through simplicity</strong> - Because the Hybrid Connection Manager uses outbound connectivity to Azure Service Bus, there is no need to open firewall ports. There is no need to expose internal services or databases from the corporate network to the outside world.</p>
<p><strong>Easy to use</strong> - Setting up an Azure Hybrid Connection is quite easy. There’s no intimate level of networking required. The hardest bit might be installing the Hybrid Connection Manager on-prem.</p>
<h2 id="heading-the-cons">The Cons</h2>
<p><strong>Performance</strong> - Depending on your needs, the performance from an Azure Hybrid Connection Manager might not be sufficient. For enterprises and teams maintaining hundreds of apps that have <em>tens</em> of users at any given time, Azure Hybrid Connections might be enough. For running high-availability and intensive workloads, the latency might be too much.</p>
<p><strong>Enabling Procrastination</strong> - This one is highly subjective. I find that when migrating applications to Azure, it’s useful to work on the hardest problems first. Often times, hosting an application though an App Service is by far the easiest part. Instead, moving databases to Azure can be more challenging in terms of technical and people challenges.</p>
]]></content:encoded></item><item><title><![CDATA[Deploying to a static site using Azure DevOps]]></title><description><![CDATA[Without getting into the nitty gritty of how to build a single page app, I’ll showcase a simple process how to deploy a Vue app to an Azure Storage Account leveraging the static website feature.
Assuming you have created an Azure Storage account with...]]></description><link>https://gaunacode.com/deploying-to-a-static-site-using-azure-devops</link><guid isPermaLink="true">https://gaunacode.com/deploying-to-a-static-site-using-azure-devops</guid><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Facundo Gauna]]></dc:creator><pubDate>Sat, 28 Mar 2020 03:37:13 GMT</pubDate><content:encoded><![CDATA[<p>Without getting into the nitty gritty of how to build a single page app, I’ll showcase a simple process how to deploy a Vue app to an Azure Storage Account leveraging the <a target="_blank" href="https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website">static website</a> feature.</p>
<p>Assuming you have created an Azure Storage account with static websites enabled, then you can copy the files to the appropriate container. Here’s <a target="_blank" href="https://gaunacode.com/creating-an-azure-storage-account-for-static-site-hosting-using-terraform">a blogpost</a> of how to create one using Terraform.</p>
<pre><code><span class="hljs-selector-tag">-</span> <span class="hljs-selector-tag">download</span>: <span class="hljs-selector-tag">current</span>
  <span class="hljs-selector-tag">artifact</span>: <span class="hljs-selector-tag">frontend</span>
<span class="hljs-selector-tag">-</span> <span class="hljs-selector-tag">task</span>: <span class="hljs-selector-tag">AzureFileCopy</span><span class="hljs-keyword">@4</span>
  <span class="hljs-attribute">inputs:</span>
    <span class="hljs-attribute">SourcePath:</span> <span class="hljs-string">'$(Pipeline.Workspace)/frontend/*'</span>
    <span class="hljs-attribute">azureSubscription:</span> <span class="hljs-string">'$(AzureServiceConnection)'</span>
    <span class="hljs-attribute">Destination:</span> <span class="hljs-string">'AzureBlob'</span>
    <span class="hljs-attribute">storage:</span> <span class="hljs-string">'$(storageAccountName)'</span>
    <span class="hljs-attribute">ContainerName:</span> <span class="hljs-string">'$web'</span>
</code></pre><p>As you may notice, the files are copied to a <code>$web</code> container in the storage account. This example also relies on the front-end files to be published as an artifact.</p>
<p>I’ll show a quick example a CI pipeline for a Vue app. This app was generated using the Vue CLI. The built app gets published as an artifact called <code>frontend</code>.</p>
<pre><code><span class="hljs-bullet">-</span> <span class="hljs-attr">job:</span> <span class="hljs-string">job_build_front_end</span>
  <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Build Frontend'</span>
  <span class="hljs-attr">pool:</span> 
    <span class="hljs-attr">vmImage:</span> <span class="hljs-string">'ubuntu-latest'</span>
  <span class="hljs-attr">steps:</span> 

  <span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">Npm@1</span>
    <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Restore'</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-attr">command:</span> <span class="hljs-string">'install'</span>
      <span class="hljs-attr">workingDir:</span> <span class="hljs-string">$(frontEndDirectory)</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">Npm@1</span>
    <span class="hljs-attr">displayName:</span> <span class="hljs-string">'Build'</span>
    <span class="hljs-attr">inputs:</span>
      <span class="hljs-attr">command:</span> <span class="hljs-string">'custom'</span>
      <span class="hljs-attr">workingDir:</span> <span class="hljs-string">$(frontEndDirectory)</span>
      <span class="hljs-attr">customCommand:</span> <span class="hljs-string">run</span> <span class="hljs-string">build</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">task:</span> <span class="hljs-string">CopyFiles@2</span>
    <span class="hljs-attr">displayName:</span> <span class="hljs-string">Copy</span> <span class="hljs-string">Files</span> <span class="hljs-string">to</span> <span class="hljs-string">Artifacts</span> <span class="hljs-string">Staging</span>
    <span class="hljs-attr">inputs:</span>
       <span class="hljs-attr">sourceFolder:</span> <span class="hljs-string">$(frontEndDirectory)/dist</span>
       <span class="hljs-attr">contents:</span> <span class="hljs-string">'**'</span> 
       <span class="hljs-attr">targetFolder:</span> <span class="hljs-string">$(Build.ArtifactStagingDirectory)/frontend</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">publish:</span> <span class="hljs-string">$(Build.ArtifactStagingDirectory)/frontend</span>
    <span class="hljs-attr">artifact:</span> <span class="hljs-string">frontend</span>
      <span class="hljs-attr">displayName:</span> <span class="hljs-string">Publish</span> <span class="hljs-string">Frontend</span> <span class="hljs-string">Artifacts</span>
</code></pre><p>That’s it!</p>
]]></content:encoded></item></channel></rss>