<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://failingfast.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://failingfast.io/" rel="alternate" type="text/html" /><updated>2026-11-08T12:26:59+00:00</updated><id>https://failingfast.io/feed.xml</id><title type="html">Failing Fast</title><subtitle>...</subtitle><author><name>Ben Hall</name></author><entry><title type="html">Listen to me on the Day 2 Cloud podcast</title><link href="https://failingfast.io/opentelemetry-podcast/" rel="alternate" type="text/html" title="Listen to me on the Day 2 Cloud podcast" /><published>2023-04-27T00:00:00+01:00</published><updated>2023-04-27T00:00:00+01:00</updated><id>https://failingfast.io/opentelemtry-podcast</id><content type="html" xml:base="https://failingfast.io/opentelemetry-podcast/"><![CDATA[<p><img src="/assets/images/podcast192.png" alt="image-center" class="align-right" /></p>

<p>I was recently invited to be a guest on the Day 2 Cloud podcast (part of the Packet Pushers network), that covers DevOps and cloud-ops topics. The conversation was quite engaging, and although the episodes typically run for 40 minutes, we managed to fill an entire hour!</p>

<p>In the podcast, we discussed OpenTelemetry and observability, which are not only areas of personal interest but should absolutely be something we are looking at for our long-term tech strategies. For those of you who have already heard me discuss these topics, I think this podcast presents a more dynamic and in-depth exploration.</p>

<p><strong>You can listen to the episode here: <a href="https://packetpushers.net/podcast/day-two-cloud-192-opentelemetry-getting-from-visibility-to-observability-with-ben-hall/">Day Two Cloud 192: OpenTelemetry - Getting From Visibility To Observability With Ben Hall - Packet Pushers</a></strong>.</p>

<p>Or get a link to your favourite podcast player: <a href="https://pod.link/1460004936/episode/20e545a2b4fd8e48142eb4a23e67d4ce">Day 2 Cloud (pod.link)</a></p>

<p>I hope you find the conversation insightful and enjoyable. Please feel free to share your thoughts, questions, or feedback in the comments or with me directly.</p>]]></content><author><name>Ben Hall</name></author><category term="Support" /><category term="OpenTelemetry" /><category term="Observability" /><summary type="html"><![CDATA[I was invited to chat with the Packet Pushers folk on 'OpenTelemetry – Getting From Visibility To Observability With Ben Hall'. Looks like I pushed a show that's typically around 40 minutes long, over an hour for my guest appearance. Hope it's a good listen and you learn something new, audience!]]></summary></entry><entry><title type="html">OpenTelemetry, The Missing Ingredient</title><link href="https://failingfast.io/opentelemetry/" rel="alternate" type="text/html" title="OpenTelemetry, The Missing Ingredient" /><published>2022-10-16T00:00:00+01:00</published><updated>2022-10-16T00:00:00+01:00</updated><id>https://failingfast.io/opentelemtry</id><content type="html" xml:base="https://failingfast.io/opentelemetry/"><![CDATA[<ul>
  <li><a href="/opentelemetry-observability">Part 1: The Modern Observability Problem</a></li>
  <li>Part 2: OpenTelemetry, The Missing Ingredient</li>
</ul>

<hr />
<p><br /></p>

<p>In <a href="/opentelemetry-observability">Part 1</a>, we looked at the observability challenges for modern systems - microservices, composable architectures, event-driven, shared components - all that good stuff solving problems well. But they’re really hard to support. We emphasised the importance of the right data being instrumented and we concluded that there are many good observability solutions, both open-source and commercial. But they all do things their own way, greatly reducing our ability to flex and adapt - letting our old opponent vendor lock-in through the door.</p>

<p><img src="/assets/images/opentelemetry/otel.png" alt="image-center" class="align-center" /></p>

<p>In this article, we’ll look at how OpenTelemetry bridges the gap between our code and the observability backend options, supporting a <strong>move away from vendor-specific tooling</strong> for generating and exporting telemetry data.</p>

<h2 id="a-quick-refresher">A Quick Refresher</h2>

<p>Before we get started, let’s refresh our terminology from <a href="/opentelemetry-observability">Part 1</a>:</p>

<p><img src="/assets/images/opentelemetry/refresher.png" alt="image-center" class="align-right" /></p>

<ul>
  <li><strong>Instrumentation</strong> is what produces the telemetry. When we talk about instrumenting, we mean to add libraries and/or code to our applications to collect telemetry such as logs, metrics and traces.</li>
  <li><strong>Telemetry</strong> are insights into the behaviour or inner workings of a system – the data that the system emits about what’s happening inside the ‘black box’, collected via instrumentation. You may also hear these being called signals.</li>
</ul>

<h2 id="a-vendor-neutral-standard">A Vendor-Neutral Standard</h2>

<p>At its core, OpenTelemetry (OTel) is a vendor-neutral standard for telemetry across languages. You only need to instrument your code once and can then easily change to other observability backends when your needs change.</p>

<p>It is also means you can stick with your backend of choice while being able to change the language you use - all supported languages will adhere to the same API specification for instrumentation.</p>

<h2 id="more-than-a-vendor-neutral-standard">More Than A Vendor-Neutral Standard</h2>

<p>OTel also provides <strong>a complete end-to-end implementation</strong> for generating, emitting, collecting, processing and exporting telemetry data to any supported observability back-end. For each language currently supported there core libraries, automatic instrumentation libraries, and exporters.</p>

<p><img src="/assets/images/opentelemetry/otel-libs.png" alt="image-center" class="align-center" /></p>

<h3 id="core-libraries">Core libraries</h3>

<p><strong>The API</strong> is the bare bones interface for instrumentation - no actual telemetry data will be sent to a backend. It’s been done this way because third party libraries wishing to instrument their code should not need to be concerned with how the consuming application implements its OpenTelemetry.</p>

<p><strong>The SDK</strong> is the complete language library that provides implementations of the API so we can instrument our code manually. It is what we pull directly into our applications. It doesn’t implement exporters, which are separate libraries that take a dependency on the SDK. Alternative instrumentation are available in separate libraries implementing the API.</p>

<p><img src="/assets/images/opentelemetry/sdk2.png" alt="image-center" class="align-center" /></p>

<p>Typically, any application that we wish to hook up to a observability backend with OTel will take dependencies on both of these indirectly, through dependencies on instrumentation libraries and exporters.</p>

<h3 id="instrumentation-libraries">Instrumentation Libraries</h3>

<p>Plugin-and-go for many common libraries and frameworks, such as those for web and databases. For example, we can generate telemetry data automatically for ASP.NET Core incoming web requests and outgoing Entity Framework requests. The only steps required are adding the two packages:</p>

<ul>
  <li>OpenTelemetry.Instrumentation.AspNetCore</li>
  <li>OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCore</li>
</ul>

<p>And adding them as services in code:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">public</span> <span class="k">void</span> <span class="nf">ConfigureServices</span><span class="p">(</span><span class="n">IServiceCollection</span> <span class="n">services</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">services</span><span class="p">.</span><span class="nf">AddOpenTelemetryTracing</span><span class="p">((</span><span class="n">builder</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">builder</span>
        <span class="p">.</span><span class="nf">AddAspNetCoreInstrumentation</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">AddEntityFrameworkCoreInstrumentation</span><span class="p">()</span>
        <span class="c1">// Add your exporter(s) here</span>
    <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The OTel project maintains instrumentation and exporter packages for key OSS projects and compliance with the OTel specification, in its main repository for each language. For .NET this is <a href="https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src">open-telemetry/opentelemetry-dotnet</a>. Many others are then available from a sister contrib repository, which for .NET is <a href="https://github.com/open-telemetry/opentelemetry-dotnet-contrib">open-telemetry/opentelemetry-dotnet-contrib</a>.</p>

<h3 id="exporters">Exporters</h3>

<p>Exporters are libraries to send the instrumented telemetry data to backends. This can be through the OpenTelemetry Protocol (OTLP) itself - and that is the long-term goal<strong>*</strong>, or vendor-specific such as Jaegar or Azure Monitor, where the exporter library would translate from the OpenTelemetry data in memory, to the format used by a vendor tool. The availability of these types of exporters makes it much easier to plan and execute a transition to OTLP.</p>

<p><strong>*</strong>Vendors are increasingly offering full support for direct ingress of native OTLP. See <a href="https://opentelemetry.io/vendors/">this list</a> for updates. The Jaeger docs now even state:</p>

<blockquote>
  <p>As of 2022, the Jaeger SDKs are no longer supported, and all users are advised to migrate to OpenTelemetry.</p>
</blockquote>

<h2 id="vendor-neutral-tracing">Vendor-Neutral Tracing</h2>

<p>One of the biggest challenges I’ve faced when debugging distributed systems is getting visibility the complete end-to-end journey of a transaction through all the services involved.</p>

<p>In <a href="/opentelemetry-observability">part 1</a> we introduced distributed tracing as a key part of any observability solution to solve just this problem. We acknowledged that there are many open-source and commercial products that do this already by propagating a trace ID between all operations in a transaction to tie everything together.</p>

<p>But more often than not, we end up with an incomplete picture that the business just accept as a risk, despite the increased effort for support. Some common causes of this include:</p>

<ul>
  <li>Services on mixed tech stacks require different solutions and resource constraints prevent us from instrumenting all of them</li>
  <li>Vendor lock-in, where our chosen backend does not support the tech stack of some of our services</li>
  <li>Clients and agents to instrument and send telemetry are built in-house and resource constraints prevent us from maintaining them for all service tech stacks</li>
</ul>

<p><img src="/assets/images/opentelemetry/missing-id.png" alt="image-center" class="align-center" /></p>

<p>As a single standard for tech stacks and backends, OTel eliminates these challenges. It brings a complete set of tools that really do simply plug in, to greatly reduce the resourcing required, and because it is an open standard, very little resources are required to switch to (or add) other backends.</p>

<h3 id="how-opentelemetry-does-tracing">How OpenTelemetry Does Tracing</h3>

<p>OTel represents each operation in a trace across a system with the concept of a <strong>span</strong>. Amongst other things <a href="https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/overview.md#spans">defined in the OTel specification</a>, each span will hold:</p>

<ul>
  <li>A unique trace ID for the whole ‘journey’ of the current transaction through the system</li>
  <li>A unique ID for the span itself</li>
  <li>A unique ID for the parent span so we can reconstruct the journey in an observability backend</li>
</ul>

<p><img src="/assets/images/opentelemetry/sendspans.png" alt="image-center" class="align-center" /></p>

<p>Each span represents a single operation in the overall transaction through a system. Most backends will represent these against time, in a way you might be familiar with from <a href="https://en.wikipedia.org/wiki/Gantt_chart">Gantt charts</a>.</p>

<p><img src="/assets/images/opentelemetry/chart.png" alt="image-center" class="align-center" /></p>

<p>In this simplified example:</p>

<ol>
  <li><strong>Span A</strong>: request comes into Service A for user details. This is the <strong>root span</strong> and of course, parent to <strong>span B</strong>. Most mainstream web frameworks will be able to auto-instrument this, meaning it will automatically create a trace span for this incoming request.</li>
  <li><strong>Span B</strong>:Service A calls Service B to get a user’s details. We might get this from a library to auto-instrument HTTP calls or we could manually add a trace signal ‘around’ the code that makes the call.</li>
  <li><strong>Span C</strong>: Service B receives the request.</li>
  <li><strong>Span D</strong>: Inside the code to handle the request, a child span represents an SQL query to the database for the user details.</li>
  <li><strong>Span E</strong>: We manually instrument a signal for adding then user if it is not found.</li>
  <li><strong>Span F</strong>: Service B has returned the user details and Service A returns that data to the caller, which is auto-instrumented by our web framework.</li>
</ol>

<h2 id="vendor-neutral-context-propagation">Vendor-Neutral Context Propagation</h2>

<p>OTel uses the concept of <strong>baggage</strong>, which standardises the format of a shared context for values across programming languages and platforms, removing the need for teams to develop their own custom solutions for propagating shared data.</p>

<p>For example, if only <code class="language-plaintext highlighter-rouge">service A</code> sees a user’s ID, we could put in the baggage so that <code class="language-plaintext highlighter-rouge">service B</code> could access in when called as a child/dependency.</p>

<p><img src="/assets/images/opentelemetry/baggage.png" alt="image-center" class="align-center" /></p>

<h2 id="the-collector-a-vendor-neutral-proxy">The Collector: A Vendor-Neutral Proxy</h2>

<p>The star of the show in my opinion, is the <strong>collector</strong>. While most languages have exporters to send telemetry data directly to back-ends, which is fine during development, it is beneficial to offload data quickly to a collector to handle the resource intensive tasks, such as:</p>

<ul>
  <li>Exporting to multiple places</li>
  <li>Enhancing with metadata</li>
  <li>Batching</li>
  <li>Encryption</li>
  <li>Buffering and retries when the back-ends are unresponsive</li>
  <li>Control flow to back-ends to allow service to continue its work</li>
</ul>

<p>The collector can receive and export data in multiple formats, to and from observability tools using the OpenTelemetry Procotol (OTLP) and proprietary ones. It does his by acting as a <strong>pipeline</strong> through receivers, processors and exporters.</p>

<p><img src="/assets/images/opentelemetry/pipeline.png" alt="image-center" class="align-center" /></p>

<h3 id="receivers">Receivers</h3>

<p>Receivers are how telemetry signals are ingressed from sources into the collector before being processed and sent on to observability backends.</p>

<p>The core receiver is for telemetry using OTLP but there are many more available from the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver">contrib repository</a> to support vendor formats and translate these to OTLP:</p>

<ul>
  <li>A <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver">file log receiver</a> that will tail and parse logs from files.</li>
  <li>A <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/jaegerreceiver">Jaeger Receiver</a>, if you have an existing services exporting traces to Jaegar, these can be redirected to your collector and exported to new backends alongside the existing Jaegar instance.</li>
  <li>A <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/k8sclusterreceiver">Kubernetes cluster receiver</a> that collects cluster-level metrics from the k8s API server.</li>
</ul>

<h3 id="processors">Processors</h3>

<p>Processors can be run on data between receiving and exporting. Some examples:</p>

<ul>
  <li>A <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/redactionprocessor">redaction processor</a> that can be configured to prevent sensitive information leaking into traces.</li>
  <li>A <a href="https://pkg.go.dev/github.com/open-telemetry/opentelemetry-collector-contrib/processor/k8sattributesprocessor">k8s attributes processor</a>  that can automatically tag telemetry passing through the collector with k8s metadata.</li>
  <li>A <a href="https://github.com/open-telemetry/opentelemetry-collector/tree/main/processor/batchprocessor">batch processor</a> to reduce the number of outgoing connections used to transmit to backends.</li>
</ul>

<h3 id="exporters-1">Exporters</h3>

<p>Similar to the libraries used directly by applications, these are used to send data to observability backends or other destinations from the collector.</p>

<p>As well as the core OTLP exporter, there are many available from the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter">contrib repository</a></p>

<h3 id="the-sidecar-pattern">The Sidecar Pattern</h3>

<p>Below is a snapshot of a typical use-case. We have a .NET web app exporting logs, metrics and traces using OTLP to a collector. It also shows how we might choose to transition while continuing to output file logs, pulling them into the collector using the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/filelogreceiver">file log receiver</a>.</p>

<p>This method of deployment depicted is where a collector is deployed alongside each service on a one-to-one basis, with a shared lifetime with the service. This tends to put ownership of the collector with the development team(s) and is useful if you need to do processing specific to a service.</p>

<p><img src="/assets/images/opentelemetry/collector2.png" alt="image-center" class="align-center" /></p>

<p>Our collector could then be setup as a pipeline to export those to the back-ends of our choosing.</p>

<p>The configuration for that pipeline would like this (<a href="https://gist.github.com/benbhall/04b3ec79b6a1934be586efbf06a0ef21">complete config file</a>):</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">service</span><span class="pi">:</span>
  <span class="na">pipelines</span><span class="pi">:</span>
    <span class="na">traces</span><span class="pi">:</span>
      <span class="na">receivers</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">otlp</span><span class="pi">]</span>
      <span class="na">exporters</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">otlp</span><span class="pi">,</span> <span class="nv">elasticsearch/trace</span><span class="pi">]</span>
    <span class="na">logs</span><span class="pi">:</span>
      <span class="na">receivers</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">otlp</span><span class="pi">]</span>
      <span class="na">exporters</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">otlp</span><span class="pi">,</span> <span class="nv">elasticsearch/log</span><span class="pi">,</span> <span class="nv">file/rotation_with_default_settings</span><span class="pi">]</span>
    <span class="na">metrics</span><span class="pi">:</span>
      <span class="na">receivers</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">otlp</span><span class="pi">]</span>
      <span class="na">exporters</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">otlp</span><span class="pi">,</span> <span class="nv">prometheus</span><span class="pi">]</span>
</code></pre></div></div>

<h3 id="as-a-single-gateway">As a Single Gateway</h3>

<p>It’s also possible to deploy the collector as standalone <strong>gateway</strong> component, to be scaled independently of the services. This tends to mean ownership of the collector is with platform teams who can then centrally manage policies and permission.</p>

<p><img src="/assets/images/opentelemetry/collector1.png" alt="image-center" class="align-center" /></p>

<p>The ideal setup is for each service or collection of services to have dedicated collectors as agents, owned by the development team. These would export to an instance run as a gateway by the ops teams to apply their own processes such as authentication or metadata.</p>

<h2 id="summary">Summary</h2>

<p>Observability solutions, both open-source and commercial, have done a good job in catching up with the demands of supporting complex modern architectures. The big unsolved problem was the lack of standards, which reduced our ability to flex and adapt, resulting in some level of vendor lock-in.</p>

<p>OpenTelemetry is the missing ingredient, which is set to change the landscape significantly. It is very easy to adopt with a real plug-and-play feel to it, and there is good transition support with libraries to translate between proprietary protocols and OpenTelemetry.</p>

<p>For the .NET readers, <a href="https://github.com/benbhall/opentelemetry-demo">fire up my demo repository</a> and have a go at using OpenTelemetry with .NET and Azure Monitor.</p>

<p><strong>EDIT April 2023</strong>: Listen to me on the Day Two Cloud, a Packet Pushers podcast, talking about OpenTelemetry. <a href="https://packetpushers.net/podcast/day-two-cloud-192-opentelemetry-getting-from-visibility-to-observability-with-ben-hall/">Use this direct link</a> or listen on your favourite podcast platform via <a href="https://pod.link/1460004936/episode/20e545a2b4fd8e48142eb4a23e67d4ce">pod.link</a>.</p>]]></content><author><name>Ben Hall</name></author><category term="Support" /><category term="OpenTelemetry" /><category term="Observability" /><summary type="html"><![CDATA[At its core, OpenTelemetry (OTel) is a vendor-neutral standard for telemetry. But it also provides a complete end-to-end implementation for generating, emitting, collecting, processing and exporting telemetry data to any support observability back-end.]]></summary></entry><entry><title type="html">The Modern Observability Problem</title><link href="https://failingfast.io/opentelemetry-observability/" rel="alternate" type="text/html" title="The Modern Observability Problem" /><published>2022-10-15T00:00:00+01:00</published><updated>2022-10-15T00:00:00+01:00</updated><id>https://failingfast.io/opentelemtry-observability</id><content type="html" xml:base="https://failingfast.io/opentelemetry-observability/"><![CDATA[<ul>
  <li>Part 1: The Modern Observability Problem</li>
  <li><a href="/opentelemetry">Part 2: OpenTelemetry, The Missing Ingredient</a></li>
</ul>

<hr />
<p><br /></p>

<p><img src="/assets/images/opentelemetry/stack3.png" alt="image-center" class="align-right" /></p>

<p>Debugging an application in a single process tends to be easy 💪 and most of what goes wrong with our monoliths will already be recorded as known problems, with standard solutions. We may even have automated their fixes. But if not, we know there are finite routes through the application, and that tracing the cause of most problems is relatively trivial.</p>

<p>But the complexity of interactions in large, modern microservice architectures produces novel, one-off errors, that cross boundaries in space and time. Factor in components being built on a broad mix of tech stacks, and we have a proper challenge supporting them.</p>

<p>Imagine just a small number of services like those represented in the diagram below. We have a mix of synchronous REST communication and asynchronous<strong>*</strong> events. If we get an error in <code class="language-plaintext highlighter-rouge">service H</code> because of something that happened in <code class="language-plaintext highlighter-rouge">service B</code>, this would be non-trivial to investigate - without the right information.</p>

<p><img src="/assets/images/opentelemetry/microservices.png" alt="image-center" class="align-center" /></p>

<p><strong>*</strong> Synchronous HTTP calls will request and receive a response at the same time. Of course sometimes it might take a few seconds, but quick enough so that we can easily link it to any side effects it might have. Asynchronous communications, like events, won’t necessarily trigger an instant response. It could even be a day before an event added to some queue is processed! Clearly, it won’t be as easy to assign blame for some side effect to an event.</p>

<h3 id="visibility-is-not-observability">Visibility is not Observability</h3>

<p>Observability and monitoring are two different things. We’re usually pretty good at monitoring, or if that fails, the users will shout at us. Either way, we tend to know <em>when</em> there is a problem. We don’t always do so well at answering the <em>why</em> questions.</p>

<p>That, for me, is the core observability problem: <strong>Why is the system exhibiting this behaviour?</strong></p>

<p>And if you’re doing observability right, you should be able to answer that question without having to redeploy with additional logs or ‘prints’.</p>

<p>If you can only answer questions about predictable problems - that’s monitoring, not observability.</p>

<h2 id="telemetry-and-the-three-pillars-of-observability">Telemetry and the Three Pillars of Observability</h2>

<ul>
  <li>Your car dashboard displaying how much fuel is left in the tank</li>
  <li>When you filter log output from your code in a tool like Kibana</li>
  <li>When you’ve looked at a histogram of events passing through your servers</li>
</ul>

<p>These are all examples of <strong>telemetry</strong> – insights into the behaviour or inner workings of a system – the data that the system emits about what’s happening inside the ‘black box’. You may also hear these being called <strong>signals</strong>.</p>

<p>And <strong>instrumentation</strong> is what <em>produces</em> the telemetry. When we talk about instrumenting, we mean to add libraries and/or code to our applications that collects telemetry such as logs, metrics and traces.</p>

<p><img src="/assets/images/opentelemetry/signals2.png" alt="image-center" class="align-center" /></p>

<p>In software engineering, we often to simplify telemetry to three distinct things:</p>

<p><strong>Logs</strong>: More specifically, centralised logging – shipping all the text log entries from our components to a single data source to easily draw correlations with searches. Logs typically describe in some detail, what is happening, so are expensive to store a long time. They are not always tied to a specific user/request and thus lack context, but nonetheless, are still heavily relied on to understand system behaviour.</p>

<p><strong>Metrics</strong>: Typically focusing on reliability, the type of stuff discussed for service level indicators (SLIs) and service level agreements (SLAs). As they are largely numerical data like counters and rates, they use significantly less storage than logging. This means it can be kept for longer, allowing for better analysis of trends over time. Caution though, size can increase with complexity – the cardinality – how much extra data we attach to each metric to make them more useful.</p>

<p><strong>Traces</strong>: More specifically, distributed tracing. Think of this as <strong>a stack trace across multiple components</strong> (and boundaries) of a distributed or large system. It offers some of the analytical benefits of metrics but with the cardinality of logging. Distributed tracing also relies on <strong>context propagation</strong>, a component to move shared information and identifiers used to associate traces, between the services.</p>

<p>With tracing instrumented across all our services, properly correlated with logs, then in our fictional system we could see the route our imaginary transaction has taken, leading us to the problem service(s), which in our imagined scenario, is some dodgy data in <code class="language-plaintext highlighter-rouge">service F</code> and the code in <code class="language-plaintext highlighter-rouge">service B</code> not gracefully handling it, such that the error didn’t surface until <code class="language-plaintext highlighter-rouge">service H</code>.</p>

<p><img src="/assets/images/opentelemetry/microservices2.png" alt="image-center" class="align-center" /></p>

<p>A typical observability tool view of traces and logs, if an exception was thrown in <code class="language-plaintext highlighter-rouge">service B</code> after it was called from <code class="language-plaintext highlighter-rouge">service A</code> would be like the image below. With it, we can see exactly what call from <code class="language-plaintext highlighter-rouge">service A</code> made the request and drill down into it further to see associated logs and get a complete picture of the overall transaction.</p>

<p><img src="/assets/images/opentelemetry/ai-spans.png" alt="image-center" class="align-center" /></p>

<p>We’ll look at traces in more detail for <a href="/opentelemetry">part 2, on OpenTelemetry</a>.</p>

<p>Ultimately, logs, metrics, and traces are the well-established ‘3 pillars’ of observability, but there are others, like health checks. Whichever way we choose to break it down, we often have raw telemetry data in varying formats and structures that need to be processed and correlated to answer questions about the behavior of our system.</p>

<h2 id="what-is-the-right-telemetry">What is the Right Telemetry?</h2>

<p>Shift away from that monitoring state of mind where you’re looking for problems you can predict, and make sure your system emits enough of the right telemetry to allow you to troubleshoot the problems you cannot foresee, because microservice architectures in a production environment will provide plenty of those! <strong>Emitting the right telemetry is what makes a system observable</strong>.</p>

<p>What actually stops internal users working or external customers getting what they need done?</p>

<p>What’s the business value of each service?</p>

<p>Keen an open dialogue with your colleagues in the business to understand what questions about the system  matter to them. Use this to decide what is the right data to be instrumented.</p>

<p>This turns observability into a more powerful tool that can be use for what..if scenarios and forecasting.</p>

<h2 id="the-technology-problem">The Technology Problem</h2>

<p><img src="/assets/images/opentelemetry/tech1.png" alt="image-center" class="align-center" /></p>

<p>We have our applications built in a mix of technologies (and more) here on the left, doing stuff we want to know about (this is intentionally getting more complicated as we purposefully shift to composed architectures and smaller microservices).</p>

<p>And we have all these options on the right (and more) that we might want to use to collect and analyse what the things on the left are doing.</p>

<p>The reality will be a bit of all of it. Some files, some custom database, maybe one team has gone their own way with elastic on prem., another in the cloud. All emitting telemetry signals but not necessarily in the same format, which means it becomes really difficult to get a joined up picture of what is going on in side the black box. For example:</p>

<ul>
  <li>You’re not going to be able to aggregate logs and run queries across the system if they’re not using a shared format. I’ve seen log shipping agents written to reformat logs but maintaining the agents and clients to do this, across tech stacks, uses a lot of resources.</li>
  <li>I’ve witnessed confusion between teams, each trying to find out what they should be adding to some common context in a transaction across services owned by a number of teams. A gap that can be easily be filled by establishing a standard but standards take time and effort to build in-house.</li>
</ul>

<h3 id="vendor-lock-in">Vendor Lock-in</h3>

<p>It’s okay to roll our own database or file logging and utilise the abstractions provided by our language frameworks. But at scale, we need the powerful features of dedicated self-hosted or SaaS observability solutions. The problem, is that they instrument code in their own way, which makes it a non-trivial piece of work to switch to a different solution.</p>

<p>We call this ‘vendor lock-in’ and its effects go both ways:</p>

<ul>
  <li>Our services all depend on the logging solution they are now plumbed into</li>
  <li>The logging solution will only support X and Y tech stacks, limiting our freedom to change these for our services</li>
</ul>

<p>Sure, if you have a handful of C# web applications and that’s all you ever make, then maybe it is okay to lock into specific observability backends:</p>

<p><img src="/assets/images/opentelemetry/tech2.png" alt="image-center" class="align-center" /></p>

<p>But we can’t predict the future and we don’t want to create future legacy.</p>

<h2 id="summary">Summary</h2>

<p><img src="/assets/images/opentelemetry/observable.png" alt="image-center" class="align-right" /></p>

<p>To resolve problems in a large microservice system:</p>

<ul>
  <li>We must be able to find out <em>why</em> they have occurred.</li>
  <li>For this, our system must be observable.</li>
  <li>To be observable, a system needs to be instrumented such that the code emits telemetry, which is typically logs, traces and metrics.</li>
  <li>This telemetry must be sent to a backend that supports joining up that telemetry data and answering questions about the system’s behaviour.</li>
</ul>

<p>There are lots of open-source and commercial backends, available as self-hosted or SaaS, many of which are really good at this. The problem, is that they all do things in their own ways - there is no standard. Plumbing our logging, metrics and traces into all our services is a big task, meaning when you have done it once for any vendor, you probably won’t be doing it again any time soon, greatly reducing your freedom to be flexible and adapt.</p>

<p>OpenTelemetry (OTeL) is the evolution of ideas from the OpenTracing and OpenCensus projects, to build a single, vendor-neutral standard. We’ll look at this in <strong><a href="/opentelemetry">Part 2: OpenTelemetry, The Missing Ingredient</a></strong>.</p>

<p><strong>EDIT April 2023</strong>: Listen to me on the Day Two Cloud, a Packet Pushers podcast, talking about OpenTelemetry. <a href="https://packetpushers.net/podcast/day-two-cloud-192-opentelemetry-getting-from-visibility-to-observability-with-ben-hall/">Use this direct link</a> or listen on your favourite podcast platform via <a href="https://pod.link/1460004936/episode/20e545a2b4fd8e48142eb4a23e67d4ce">pod.link</a>.</p>]]></content><author><name>Ben Hall</name></author><category term="Support" /><category term="OpenTelemetry" /><category term="Observability" /><summary type="html"><![CDATA[The observability problem - Why is the system exhibiting this behaviour?]]></summary></entry><entry><title type="html">\`Charbonneau Loops\’ and government IT contracting</title><link href="https://failingfast.io/charbonneau/" rel="alternate" type="text/html" title="\`Charbonneau Loops\’ and government IT contracting" /><published>2022-10-13T00:00:00+01:00</published><updated>2022-10-13T00:00:00+01:00</updated><id>https://failingfast.io/charbonneau</id><content type="html" xml:base="https://failingfast.io/charbonneau/"><![CDATA[<p>A quick post to share a link to this interesting read about outsourcing and in-house capacity, which I can relate to working in public sector IT: <a href="https://sboots.ca/2022/10/12/charbonneau-loops-and-government-it-contracting/">‘Charbonneau Loops’ and government IT contracting</a>.</p>

<p>We’re working hard in our little software engineering corner at <a href="https://www.gov.uk/government/organisations/homes-england">Homes England</a> to build a strong, permanent development capability, and to establish Digital as a directorate that can deliver larger products. We’re not doing this so that we can build everything. But rather, to avoid passing a tipping point, outsourcing more, where developers and testers either leave, or their skillsets become obsolete. Then not only do we lose the knowledge to oversee outsourced work, but we are also not able to support it.</p>

<p><a href="https://twitter.com/xiaopu">Xiaopu Fung</a>, who works in the Canadian public sector, captured it well:</p>

<blockquote>
  <p>We need to build and ship in order to keep flexing our in-house capacity muscles, otherwise we atrophy.</p>
</blockquote>]]></content><author><name>Ben Hall</name></author><category term="Software Teams" /><category term="outsourcing" /><summary type="html"><![CDATA[An interesting read about outsourcing and in-house capacity, relevant to us in IT.]]></summary></entry><entry><title type="html">GitOps Provisioning with GitHub Actions</title><link href="https://failingfast.io/gitops3/" rel="alternate" type="text/html" title="GitOps Provisioning with GitHub Actions" /><published>2022-06-17T00:00:00+01:00</published><updated>2022-06-17T00:00:00+01:00</updated><id>https://failingfast.io/gitops-provisioning-with-github-actions</id><content type="html" xml:base="https://failingfast.io/gitops3/"><![CDATA[<p class="notice--info"><strong>Republished:</strong> Microsoft commissioned this three-part article series from me, and it was first published on CodeProject in June 2022. CodeProject has since closed, so I’m sharing it here. This is the version I submitted; the published one had light edits. Tool and action versions reflect mid-2022.</p>

<p><strong>The series:</strong> <a href="/gitops1/">Part 1: Introduction to GitOps</a> · <a href="/gitops2/">Part 2: GitOps with Terraform and GitHub</a> · <a href="/gitops3/">Part 3: GitOps Provisioning with GitHub Actions</a></p>

<p>In this article, we build upon what we learned in the previous article by showing how to construct a CI/CD pipeline that provisions changes that we merge into our Terraform plan via pull request.</p>

<p>Step by step, we’ll set up GitHub Actions that first validate the updated Terraform plan, then run the plan and provision Azure infrastructure.</p>

<p>Then we’ll prove the pipeline works by changing the plan in a pull request, and watch those changes result in Azure infrastructure being deployed.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>This is a hands-on tutorial that requires the following setup beforehand:</p>

<ul>
  <li>Azure subscription (<a href="https://azure.microsoft.com/en-gb/free/">Free trial here</a>)</li>
  <li>Azure CLI installed (<a href="https://docs.microsoft.com/en-us/cli/azure/install-azure-cli">How to install</a>)</li>
  <li>Terraform installed locally* (<a href="https://www.terraform.io/downloads">Download</a>)</li>
  <li>PowerShell</li>
  <li>An empty repository on GitHub (<a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository">Instructions here</a>)</li>
</ul>

<p>* The Terraform download is just a single binary file, not an installer. You will need to copy it to somewhere in your execution path.</p>

<p>If you’ve skipped the previous article in this series, you can clone the complete code from the <a href="https://github.com/benbhall/gitopsdemo-terraform-azure">repository</a> via your favourite Git UI or on the command line:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/benbhall/gitopsdemo-terraform-azure.git
</code></pre></div></div>

<p>When you have created your Azure account, login to the <a href="https://portal.azure.com/">portal</a> and head for the Subscriptions section by searching in the bar at the top of the page. Take a note of the Subscription ID assigned to it – you will need it later.</p>

<p><img src="/assets/images/gitops/part3-1.png" alt="Figure 1" /></p>

<p>The complete code for this article is <a href="https://github.com/benbhall/gitopsdemo-terraform-actions">available here</a> and also included throughout to follow along.</p>

<h2 id="step-1-setup-the-backend">Step 1: Setup the Backend</h2>

<p>Terraform needs somewhere to persist its state file – a backend - we will be using Azure Blob Storage. As a resource that Terraform itself depends on, we do need to provision ourselves.</p>

<p>In a new PowerShell terminal, authenticate against Azure (use the account you signed up for the trial with):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az login
</code></pre></div></div>

<p>Create a resource group. You are free to change the region but it shouldn’t make any difference to follow along here with <code class="language-plaintext highlighter-rouge">uksouth</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az group create <span class="nt">-n</span> gitopsdemo-tfstates-rg <span class="nt">-l</span> uksouth
</code></pre></div></div>

<p>Create a storage account:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az storage account create <span class="nt">-n</span> gitopsdemostore <span class="nt">-g</span> gitopsdemo-tfstates-rg <span class="nt">-l</span> uksouth <span class="nt">--sku</span> Standard_LRS
</code></pre></div></div>

<p>Create a storage container in that account:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az storage container create <span class="nt">-n</span> gitopsdemotfstates <span class="nt">--account-name</span> gitopsdemostore
</code></pre></div></div>

<h2 id="step-2-create-a-service-principal">Step 2: Create a Service Principal</h2>

<p>‘Service Principal’ is just a fancy name here, for an account that the GitHub Action can use to authenticate itself with Azure to access the storage account we just made, and create all our new resources.</p>

<p>In the same PowerShell terminal, execute the following code, replacing <code class="language-plaintext highlighter-rouge">&lt;subscription-id&gt;</code> with the one you recorded earlier.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>az ad sp create-for-rbac <span class="nt">--name</span> gitopsdemo-tf-sp <span class="nt">--role</span> Contributor <span class="nt">--scopes</span> /subscriptions/&lt;subscription-id&gt; | ConvertFrom-Json
</code></pre></div></div>

<p>Make a note of the values output, as you will need to add these in the next step.</p>

<h2 id="step-3-store-the-secrets-in-github">Step 3: Store the Secrets in GitHub</h2>

<p>Three of the values from step 2 map as follows with secrets that we need to set (the fourth secret is your subscription ID again).</p>

<table>
  <thead>
    <tr>
      <th><strong>GitHub Action Secret Name</strong></th>
      <th><strong>Service Principal Create Output</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AZURE_AD_CLIENT_ID</td>
      <td><code class="language-plaintext highlighter-rouge">appId</code></td>
    </tr>
    <tr>
      <td>AZURE_AD_CLIENT_SECRET</td>
      <td><code class="language-plaintext highlighter-rouge">password</code></td>
    </tr>
    <tr>
      <td>AZURE_AD_TENANT_ID</td>
      <td><code class="language-plaintext highlighter-rouge">tenant</code></td>
    </tr>
    <tr>
      <td>AZURE_SUBSCRIPTION_ID</td>
      <td>Subscription ID recorded earlier</td>
    </tr>
  </tbody>
</table>

<p>Store these as <a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets">encrypted secrets</a> in your GitHub repository by visiting your repository on the GitHub website, navigating to <strong>Settings -&gt; Secrets -&gt; Actions</strong>, and clicking <strong>New repository secret</strong>.</p>

<p><img src="/assets/images/gitops/part3-2.png" alt="Figure 2" /></p>

<h2 id="step-4-configure-the-backend-in-terraform">Step 4: Configure the Backend in Terraform</h2>

<p>During the <code class="language-plaintext highlighter-rouge">init</code> phase, Terraform will look for a backend configuration in your code so that it knows where to store the state file. In your <strong>main.tf</strong>, add the new config to the top of the <code class="language-plaintext highlighter-rouge">terraform</code> section:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">backend</span> <span class="s2">"azurerm"</span> <span class="p">{</span>
     <span class="nx">resource_group_name</span>  <span class="p">=</span> <span class="s2">"gitopsdemo-tfstates-rg"</span>
     <span class="nx">storage_account_name</span> <span class="p">=</span> <span class="s2">"gitopsdemostore"</span>
     <span class="nx">container_name</span>       <span class="p">=</span> <span class="s2">"gitopsdemotfstates"</span>
     <span class="nx">key</span>                  <span class="p">=</span> <span class="s2">"gitopsdemo.tfstate"</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>If you used the script above to create the storage account for Terraform state, you won’t need to change any of these values.</p>

<h2 id="step-5-build-a-github-action-workflow">Step 5: Build a GitHub Action Workflow</h2>

<p>A YAML file for GitHub Actions can look a little overwhelming if you’re new to them, so we’ll break it up here. But if you do get stuck, the complete file is available in the <a href="https://github.com/benbhall/gitopsdemo-terraform-actions/blob/main/.github/workflows/main.yml">repository</a>.</p>

<p>GitHub Actions looks for YAML files in the folder <code class="language-plaintext highlighter-rouge">.github/workflows</code>. The code in those files specifies what events to listen for and what actions to take. Actions are made up of jobs, with each job being a series of steps.</p>

<h3 id="step-51-create-the-yaml-file">Step 5.1: Create the YAML file</h3>

<p>You can do this on the GitHub website but it is better practice to work locally, then commit and push changes up. Don’t worry about Git branches at this point – we can just work on <code class="language-plaintext highlighter-rouge">main</code>.</p>

<p>Start by creating a folder in the root of your repository called <code class="language-plaintext highlighter-rouge">.github</code>, then inside that, another folder <code class="language-plaintext highlighter-rouge">workflows</code>. Inside the <code class="language-plaintext highlighter-rouge">workflows</code> folder, create a new file: <strong>main.yml</strong>.</p>

<p>Git ‘stage’ the new file, commit the change and push it up to GitHub in your UI or using the following commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add main.yml
git commit <span class="nt">-m</span> <span class="s2">"Add empty action file"</span>
git push
</code></pre></div></div>

<p>Get into the habit of pushing changes up regularly!</p>

<h3 id="step-52-set-events-that-trigger-the-workflow">Step 5.2: Set events that trigger the workflow</h3>

<p>Add the following lines to the top of <code class="language-plaintext highlighter-rouge">main.yml</code> to control when and how the workflow will run.</p>

<p>This workflow will trigger on any push or pull request events against the <code class="language-plaintext highlighter-rouge">main</code> branch. For a complete list of events that trigger workflows <a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows">refer to the docs</a>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">main</span> <span class="pi">]</span>
  <span class="na">pull_request</span><span class="pi">:</span>
    <span class="na">branches</span><span class="pi">:</span> <span class="pi">[</span> <span class="nv">main</span> <span class="pi">]</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>
</code></pre></div></div>

<p>The last line will ensure we can still run the workflow manually from the Actions tab on the GitHub website.</p>

<h3 id="step-53-add-the-job">Step 5.3: Add the job</h3>

<p>Under the config we have so far in <code class="language-plaintext highlighter-rouge">main.yml</code>, add a job called <code class="language-plaintext highlighter-rouge">build</code>. We’ll start by pulling the secrets into environment variables the Terraform Azure provider will look for when authenticating in Azure. We’ll also set the working directory to the repository root but you can keep your Terraform files in a subdirectory and point there.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
  <span class="na">build</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">env</span><span class="pi">:</span>
      <span class="na">ARM_CLIENT_ID</span><span class="pi">:</span> <span class="s">${{ secrets.AZURE_AD_CLIENT_ID }}</span>
      <span class="na">ARM_CLIENT_SECRET</span><span class="pi">:</span> <span class="s">${{ secrets.AZURE_AD_CLIENT_SECRET }}</span>
      <span class="na">ARM_SUBSCRIPTION_ID</span><span class="pi">:</span> <span class="s">${{ secrets.AZURE_SUBSCRIPTION_ID }}</span>
      <span class="na">ARM_TENANT_ID</span><span class="pi">:</span> <span class="s">${{ secrets.AZURE_AD_TENANT_ID }}</span>
   
    <span class="na">defaults</span><span class="pi">:</span>
      <span class="na">run</span><span class="pi">:</span>
        <span class="na">working-directory</span><span class="pi">:</span> <span class="s">.</span>
</code></pre></div></div>

<h3 id="step-54-checkout-and-setup-terraform">Step 5.4: Checkout and Setup Terraform</h3>

<p>We need to checkout our repository so the workflow can access our Terraform. We also need to make sure we have the Terraform CLI downloaded and configured. We’ll do this by adding our first two steps under the code we just added:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v3</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">hashicorp/setup-terraform@v2</span>
</code></pre></div></div>

<p>This makes use of a GitHub’s action <code class="language-plaintext highlighter-rouge">checkout@v3</code> and an action from Hashicorp. The latter defaults to fetching the latest version of Terraform, but where a specific version is required, this can be configured (see <a href="https://github.com/hashicorp/setup-terraform">usage documentation</a>).</p>

<h3 id="step-55-terraform-steps">Step 5.5: Terraform steps</h3>

<p>The mandatory steps for Terraform to provision our resources in Azure are <strong>init</strong>, <strong>plan</strong>, and <strong>apply</strong>. But we will be building a production-ready workflow that will gate the <code class="language-plaintext highlighter-rouge">terraform apply</code> provisioning step with a pull request process that has automated checks.</p>

<p><img src="/assets/images/gitops/part3-3.png" alt="Figure 3" /></p>

<ul>
  <li><strong>Format</strong>: Enforce Terraform best practices and produce an error if the configuration is not formatted correctly.</li>
  <li><strong>Init</strong>: Initialize the Terraform configuration/setup working directory.</li>
  <li><strong>Validate</strong>: Without looking at actual resource / remote state, check the code syntax.</li>
  <li><strong>Plan</strong>: Looking at actual remote state, determine what needs to be created, updated, or destroyed in order to move to the new/desired state in the code.</li>
</ul>

<p>Add the following new steps:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Terraform fmt</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">fmt</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">terraform fmt -check</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Terraform Init</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">init</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">terraform init -input=false -migrate-state -force-copy</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Terraform Validate</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">validate</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">terraform validate -no-color</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Terraform Plan</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">plan</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name == 'pull_request'</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">terraform plan -no-color -input=false</span>
        <span class="na">continue-on-error</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>

<p>Errors raised in these steps get added to the pull request as comments, failing the checks and preventing the changes from being merged into the <code class="language-plaintext highlighter-rouge">main</code> branch for provisioning resources. For this to happen we need to add another two steps:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/github-script@v6</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">github.event_name == 'pull_request'</span>
        <span class="na">env</span><span class="pi">:</span>
          <span class="na">PLAN</span><span class="pi">:</span> <span class="s2">"</span><span class="s">terraform</span><span class="se">\n</span><span class="s">${{</span><span class="nv"> </span><span class="s">steps.plan.outputs.stdout</span><span class="nv"> </span><span class="s">}}"</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">github-token</span><span class="pi">:</span> <span class="s">${{ secrets.GITHUB_TOKEN }}</span>
          <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
            <span class="s">const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`</span>
            <span class="s">#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`</span>
            <span class="s">#### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`</span>
            <span class="s">#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`</span>
            <span class="s">&lt;details&gt;&lt;summary&gt;Show Plan&lt;/summary&gt;</span>
            <span class="s">\`\`\`\n</span>
            <span class="s">${process.env.PLAN}</span>
            <span class="s">\`\`\`</span>
            <span class="s">&lt;/details&gt;</span>
            <span class="s">*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;</span>
            <span class="s">github.rest.issues.createComment({</span>
              <span class="s">issue_number: context.issue.number,</span>
              <span class="s">owner: context.repo.owner,</span>
              <span class="s">repo: context.repo.repo,</span>
              <span class="s">body: output</span>
            <span class="s">})</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Terraform Plan Status</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">steps.plan.outcome == 'failure'</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">exit </span><span class="m">1</span>
</code></pre></div></div>

<p>I can’t take credit for this neat bit of code – it comes adapted straight from the <a href="https://github.com/hashicorp/learn-terraform-github-actions/blob/main/.github/workflows/terraform.yml">example</a>.</p>

<p>Lastly, we need an Apply step that will go ahead and provision the resource changes. In our workflow, we are triggering that from the merge to <code class="language-plaintext highlighter-rouge">main</code>.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Terraform Apply</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">github.ref == 'refs/heads/main' &amp;&amp; github.event_name == 'push'</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">terraform apply -auto-approve -input=false</span>
</code></pre></div></div>

<h2 id="step-6-trying-out-the-workflow">Step 6: Trying Out the Workflow</h2>

<p>Create a new branch <code class="language-plaintext highlighter-rouge">bad-formatting-demo</code> locally in your Git UI, or on the command line with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout <span class="nt">-b</span> bad-formatting-demo
</code></pre></div></div>

<p>Break the formatting in <strong>main.tf</strong> – it should be enough if you just tab the <code class="language-plaintext highlighter-rouge">locals</code> block in a bit further.</p>

<p>Stage the updated main.tf, commit the change and push our new branch up to GitHub:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add main.tf
git commit <span class="nt">-m</span> <span class="s2">"Break formatting"</span>
git push <span class="nt">--set-upstream</span> origin bad-formatting-demo
</code></pre></div></div>

<p>Navigate to your repository on the GitHub website and go to the <strong>Pull requests</strong> tab. It should have noticed your new changes and offer to <strong>Compare &amp; pull request</strong> – click that button.</p>

<p><img src="/assets/images/gitops/part3-4.png" alt="Figure 4" /></p>

<p>Then click <strong>Create pull request</strong>.</p>

<p><img src="/assets/images/gitops/part3-5.png" alt="Figure 5" /></p>

<p>Click on <strong>Details</strong> to see the step that failed:</p>

<p><img src="/assets/images/gitops/part3-6.png" alt="Figure 6" /></p>

<h3 id="fixing-the-error">Fixing the Error</h3>

<p>Locally, on the command line inside the folder where our Terraform code is located, run the <a href="https://www.terraform.io/cli/commands/fmt">Terraform format command</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terraform <span class="nb">fmt</span>
</code></pre></div></div>

<p>You will need to add <code class="language-plaintext highlighter-rouge">-recursive</code> if you have subfolders in your <em>own</em> future Terraform code.</p>

<p>This will automatically fix all formatting errors on your files.</p>

<p>Push up the fixed file. The PR checks will automatically run again. If everything else looks good in the other steps it will be ready to go ahead and <strong>Merge pull request</strong>.</p>

<p><img src="/assets/images/gitops/part3-7.png" alt="Figure 7" /></p>

<p>As shown in the diagram above, this will re-trigger the checks and, assuming they all still pass, it will go ahead and run the Apply step provisioning the resources in Azure:</p>

<p><img src="/assets/images/gitops/part3-8.png" alt="Figure 8" /></p>

<h3 id="visibility-of-changes-to-state">Visibility of Changes to State</h3>

<p>One thing we couldn’t see in the last run was how Terraform will show us the intended changes when it sits at the PR for code review.</p>

<p>Go ahead and remove the Application Insights resource on a new local branch, push it up to GitHub and open a new pull request.</p>

<p><img src="/assets/images/gitops/part3-9.png" alt="Figure 9" /></p>

<p>The checks should all pass but click on the <strong>Show all checks -&gt; Details</strong> anyway, and expand the <strong>Terraform Plan</strong> section.</p>

<p>A code reviewer can easily see what changes to the current live state are being proposed in the pull request:</p>

<p><img src="/assets/images/gitops/part3-10.png" alt="Figure 10" /></p>

<p>When we merge this change, triggering the workflow to run through again, to the Apply step, we can see the App Insights resource removed from our resource group in Azure.</p>

<p><img src="/assets/images/gitops/part3-11.png" alt="Figure 11" /></p>

<h2 id="summary">Summary</h2>

<p>In this article, we gave you hands-on experience constructing a CI/CD pipeline that provisions changes that have been merged into your Terraform plan via a pull request. We’ve demonstrated the use of automated checks to support the code review process in ‘gating’ deployments, and we had a go at the end-to-end process for making changes and seeing those changes in the live Azure resources.</p>

<p><strong>The series:</strong> <a href="/gitops1/">Part 1: Introduction to GitOps</a> · <a href="/gitops2/">Part 2: GitOps with Terraform and GitHub</a> · <a href="/gitops3/">Part 3: GitOps Provisioning with GitHub Actions</a></p>]]></content><author><name>Ben Hall</name></author><category term="GitOps" /><category term="gitops" /><category term="azure" /><category term="terraform" /><category term="github" /><category term="github actions" /><summary type="html"><![CDATA[Hands-on: a GitHub Actions pipeline that checks Terraform changes on pull requests and provisions Azure on merge. Part 3 of a Microsoft-commissioned series.]]></summary></entry><entry><title type="html">GitOps with Terraform and GitHub</title><link href="https://failingfast.io/gitops2/" rel="alternate" type="text/html" title="GitOps with Terraform and GitHub" /><published>2022-06-17T00:00:00+01:00</published><updated>2022-06-17T00:00:00+01:00</updated><id>https://failingfast.io/gitops-with-terraform-and-github</id><content type="html" xml:base="https://failingfast.io/gitops2/"><![CDATA[<p class="notice--info"><strong>Republished:</strong> Microsoft commissioned this three-part article series from me, and it was first published on CodeProject in June 2022. CodeProject has since closed, so I’m sharing it here. This is the version I submitted; the published one had light edits. Tool and action versions reflect mid-2022.</p>

<p><strong>The series:</strong> <a href="/gitops1/">Part 1: Introduction to GitOps</a> · <a href="/gitops2/">Part 2: GitOps with Terraform and GitHub</a> · <a href="/gitops3/">Part 3: GitOps Provisioning with GitHub Actions</a></p>

<p>In the previous article, we looked at DevOps, a modern philosophy for working with people, processes, and tools to accelerate the pace of software development; how it has adopted the software development practices of CI (Continuous Integration) and Continuous Deployment (CD); and how it spawned the new concept of GitOps - where a type of source code called IaC (Infrastructure as Code) is stored in Git source control as the single source of truth about what infrastructure is currently deployed.</p>

<p>There are many tools for IaC, but Terraform has gained the most traction everywhere I have worked. Mainly because it ticks every box – even removing any need for additional configuration management (CM) tools such as Ansible in many use cases.</p>

<p>The primary function of Terraform is to provision infrastructure. Meaning, to deploy and manage the state of infrastructure. The Terraform workflow consists of:</p>

<p><img src="/assets/images/gitops/part2-1.png" alt="Figure 1" /></p>

<ol>
  <li><strong>Write</strong>: Make changes to the infrastructure code (performed by a person).</li>
  <li><strong>Init</strong>: Setup the working directory (performed by Terraform).</li>
  <li><strong>Plan</strong>: Determine what needs to be created, updated, or destroyed to move from the current stage in live, to the new/desired state in the code (performed by Terraform).</li>
  <li><strong>Apply</strong>: Make the changes to the live infrastructure (performed by Terraform).</li>
  <li><strong>Destroy</strong>: Remove live resources no longer required (performed by Terraform).</li>
</ol>

<p><em>Reference: <a href="https://learning.oreilly.com/library/view/terraform-up-and/9781098116736/preface01.html">Terraform: Up &amp; Running (O’Reilly)</a></em></p>

<p>In this article we will take the theory we learned in the first article and learn how to apply it in a real-world scenario, provisioning the infrastructure for an Azure Function App using Terraform. We will then look at how we can utilize a Git workflow – branching, committing changes and doing a code review on a pull request – as part of our GitOps processes.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>This is a hands-on tutorial that requires the following setup beforehand:</p>

<ul>
  <li>Azure subscription (<a href="https://azure.microsoft.com/en-gb/free/">Free trial here</a>)</li>
  <li>Azure CLI installed (<a href="https://docs.microsoft.com/en-us/cli/azure/install-azure-cli">How to install</a>)</li>
  <li>Terraform installed locally* (<a href="https://www.terraform.io/downloads">Download</a>)</li>
  <li>PowerShell ideally</li>
  <li>An empty repository on GitHub (<a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-new-repository">Instructions here</a>)</li>
</ul>

<p>* The Terraform download is just a single binary file, not an installer. You will need to copy it to somewhere in your execution path.</p>

<p>When you have created your Azure account, log in to the <a href="https://portal.azure.com/">portal</a> and head for the Subscriptions section by searching in the bar at the top of the page. Take a note of the Subscription ID assigned to it – you will need it later.</p>

<p><img src="/assets/images/gitops/part2-2.png" alt="Figure 2" /></p>

<p>The complete code is <a href="https://github.com/benbhall/gitopsdemo-terraform-azure">available here</a> and also included throughout the article to follow.</p>

<h2 id="step-1-using-the-azure-provider">Step 1: Using the Azure Provider</h2>

<p>Terraform is cloud-agnostic – it can provision infrastructure across many cloud services such as Azure, Amazon Web Services (AWS), and Google Cloud Platform (GCP). It can also be used to provision services from other SaaS providers and APIs, such as the IDaaS (Identity as a Service) vendor auth0, or observability service Datadog.</p>

<p>These plugins provided by services are called Terraform ‘providers’ A complete list of verified providers is available on the <a href="https://registry.terraform.io/browse/providers">Terraform Registry</a>.</p>

<p>A provider consists primarily, of:</p>

<ul>
  <li><strong>Version number</strong>: These typically follow semantic versioning so we know when to update.</li>
  <li><strong>Resources</strong>: The new things we want to provision.</li>
  <li><strong>Data sources</strong>: A way to access resources that already exist. For example, we might need to get the details of a Key Vault to fetch secrets.</li>
</ul>

<p>For working with Azure, there are 3 providers:</p>

<ul>
  <li>Azure RM – this is the basic provider that you will use to interact with Azure Cloud</li>
  <li>Azure Stack – similar for working with on-premises instances</li>
  <li>Azure Active Directory – deals with Azure AD exclusively</li>
</ul>

<p>Create a new file called <strong>main.tf</strong> in the root folder of your repository, and add the following lines of code. Note, that required_providers are optional, but setting the version and source is strongly recommended.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">terraform</span> <span class="p">{</span>
  <span class="nx">required_providers</span> <span class="p">{</span>
      <span class="nx">azurerm</span> <span class="p">=</span> <span class="p">{</span>
        <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/azurerm"</span>
        <span class="nx">version</span> <span class="p">=</span> <span class="s2">"&gt;=3.0.0"</span>
      <span class="p">}</span>
    <span class="p">}</span>  
<span class="p">}</span>

<span class="nx">provider</span> <span class="s2">"azurerm"</span> <span class="p">{</span>
  <span class="nx">features</span> <span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Best practice tip</strong>: For a large production system with lots of providers or provider versions, you should consider moving the providers to a separate file: provider.tf, to make them easier to manage.</p>

<h2 id="step-2-writing-the-terraform-code">Step 2: Writing the Terraform Code</h2>

<p>To deploy our <a href="https://docs.microsoft.com/en-us/azure/azure-functions/functions-overview">serverless function</a> code for the very simple web request, we need to provision the infrastructure for an Azure Function App.</p>

<p>An Azure Function App also requires additional infrastructure:</p>

<ul>
  <li><strong>Resource group</strong>: <em>All</em> resources in Azure must be inside a Resource Group.</li>
  <li><strong>App Service plan</strong>: Essentially, decides what you will pay for – what compute resources you want.</li>
  <li><strong>Storage account</strong>: Necessary for Function operations.</li>
</ul>

<p>So, we’ll need to deploy these too!</p>

<p><img src="/assets/images/gitops/part2-3.png" alt="Figure 3" /></p>

<p>We want to set the regional location for the resources, as well as a common prefix for their names. In more complex production systems, keep these organized <a href="https://www.terraform.io/language/values/variables">in variable definition ‘tfvars’ files</a>. To keep this example simple and contained in a single <strong>main.tf</strong> file, we will use local variables.</p>

<p>Add the following to the top of the file:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">location</span> <span class="p">=</span> <span class="s2">"uksouth"</span>
  <span class="nx">prefix</span>   <span class="p">=</span> <span class="s2">"gitopsdemo"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>At the bottom of our <strong>main.tf</strong> file, under everything we have added so far, paste in the code for the infrastructure that we will need:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"azurerm_resource_group"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="p">=</span> <span class="s2">"${local.prefix}-rg"</span>
  <span class="nx">location</span> <span class="p">=</span> <span class="nx">local</span><span class="err">.</span><span class="nx">location</span>
  <span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_storage_account"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">name</span>                     <span class="p">=</span> <span class="s2">"${local.prefix}storageacct"</span>
  <span class="nx">resource_group_name</span>      <span class="p">=</span> <span class="nx">azurerm_resource_group</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">name</span>
  <span class="nx">location</span>                 <span class="p">=</span> <span class="nx">azurerm_resource_group</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">location</span>
  <span class="nx">account_tier</span>             <span class="p">=</span> <span class="s2">"Standard"</span>
  <span class="nx">account_replication_type</span> <span class="p">=</span> <span class="s2">"LRS"</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_service_plan"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="p">=</span> <span class="s2">"${local.prefix}-asp"</span>
  <span class="nx">resource_group_name</span> <span class="p">=</span> <span class="nx">azurerm_resource_group</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="p">=</span> <span class="nx">azurerm_resource_group</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">location</span>
  <span class="nx">os_type</span>            <span class="p">=</span> <span class="s2">"Linux"</span>
  <span class="nx">sku_name</span>            <span class="p">=</span> <span class="s2">"Y1"</span>
<span class="p">}</span>

<span class="nx">resource</span> <span class="s2">"azurerm_linux_function_app"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">name</span>                       <span class="p">=</span> <span class="s2">"${local.prefix}-function"</span>
  <span class="nx">resource_group_name</span>        <span class="p">=</span> <span class="nx">azurerm_resource_group</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">name</span>
  <span class="nx">location</span>                   <span class="p">=</span> <span class="nx">azurerm_resource_group</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">location</span>
  
  <span class="nx">service_plan_id</span>            <span class="p">=</span> <span class="nx">azurerm_service_plan</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">id</span>
  <span class="nx">storage_account_name</span>       <span class="p">=</span> <span class="nx">azurerm_storage_account</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">name</span>
  <span class="nx">storage_account_access_key</span> <span class="p">=</span> <span class="nx">azurerm_storage_account</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">primary_access_key</span>

  <span class="nx">site_config</span> <span class="p">{}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That’s it for our Terraform.</p>

<p>We now have all of the infrastructure we need to deploy a Function App to Azure. Declared in a single file. We can use it to easily provision (or re-provision) the same resources across multiple environments, in a fraction of the time it would take to perform the task manually. All via automation if we wish, without any of the common risks, like configuration drift and inconsistencies.</p>

<p>Best of all, by declaring our infrastructure as code, we are on our way to practicing GitOps. We can now take advantage of Git developer tooling, including version history, branching, and pull requests with code reviews, to ensure the infrastructure described in the code matches what is actually provisioned in our live systems. Depending on the systems in place for developers at your organization, you might also link up your code changes with tools used to track project work such as Azure DevOps Boards.</p>

<h2 id="step-3-working-with-git">Step 3: Working with Git</h2>

<p>Terraform greatly improves DevOps practices by tracking state. But problems with the state can, and do, occur. By using Git as the single source of truth for the current state of our infrastructure we can mitigate that risk.</p>

<p>Go ahead and push your changes up to GitHub – via the CLI or your favorite Git UI.</p>

<h3 id="the-code-review">The Code Review</h3>

<p>An essential control that GitOps borrows from software development is the pull request - where we want someone to <em>manually</em> check changes made to the code. This is commonly referred to as a ‘code review’.</p>

<p>We’re going to add <a href="https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview">Application Insights</a> to our Function App now. As with application development, we will do this on a separate branch to avoid breaking the code currently in live on <em>main</em>. We’ll only merge it into the main branch <em>after</em> it has been code reviewed.</p>

<p>Create a new branch ‘add-app-insights’ locally in your Git UI, or on the command line with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git checkout <span class="nt">-b</span> add-app-insights
</code></pre></div></div>

<p>Add a new resource:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"azurerm_application_insights"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">name</span>                <span class="p">=</span> <span class="s2">"${local.prefix}-appinsights"</span>
  <span class="nx">resource_group_name</span> <span class="p">=</span> <span class="nx">azurerm_resource_group</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">name</span>
  <span class="nx">location</span>            <span class="p">=</span> <span class="nx">azurerm_resource_group</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">location</span>
  <span class="nx">application_type</span>    <span class="p">=</span> <span class="s2">"web"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So that the Function App can talk to the Application Insights instance, add a new app setting to the function’s resource with the key:</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"azurerm_linux_function_app"</span> <span class="s2">"main"</span> <span class="p">{</span>
   
  <span class="err">…</span>

  <span class="nx">app_settings</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">AppInsights_InstrumentationKey</span> <span class="p">=</span> <span class="nx">azurerm_application_insights</span><span class="err">.</span><span class="nx">main</span><span class="err">.</span><span class="nx">instrumentation_key</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Stage the updated main.tf, commit the change, and push our new branch up to GitHub:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git add main.tf
git commit <span class="nt">-m</span> <span class="s2">"Add app insights"</span>
git push <span class="nt">--set-upstream</span> origin add-app-insights
</code></pre></div></div>

<p>Navigate to your repository on the GitHub website and go to the <strong>Pull requests</strong> tab. It should have noticed your new changes and offer to <strong>Compare &amp; pull request</strong> – click that button.</p>

<p><img src="/assets/images/gitops/part2-4.png" alt="Figure 4" /></p>

<p>The screen to open a PR is your opportunity to explain any complex changes to the person reviewing it. In our case, it’s a self-explanatory addition so we can leave the comments blank.</p>

<p><img src="/assets/images/gitops/part2-5.png" alt="Figure 5" /></p>

<p>It’s also a very good time to have one last check of the changes you are asking to pull into the main branch. I can’t count the number of times I’ve caught a mistake at this point.</p>

<p><img src="/assets/images/gitops/part2-6.png" alt="Figure 6" /></p>

<p>Click the button to <strong>Create pull request</strong> when you’re happy to proceed.</p>

<p>Another member or members of your team can then verify your code. When setting up the repository for a GitOps workflow, as with application code, it is a good idea to set rules to enforce the number of approvals required, who should approve, and other automated checks (which we can look at in the next article).</p>

<p><img src="/assets/images/gitops/part2-7.png" alt="Figure 7" /></p>

<p>Then it can be merged into the main branch with <strong>Merge pull request</strong>.</p>

<p>We’ll look at using GitHub Actions to automatically deploy these changes to Azure in the next article.</p>

<h2 id="summary">Summary</h2>

<p>In this article, we gave you hands-on experience writing Terraform code to deploy some simple Azure infrastructure, and using the Git workflow used by software developers to ‘gate’ infrastructure changes to our <em>main</em> branch – with <em>main</em> being the source of truth for what the desired state of our infrastructure is.</p>

<p>In the next article we will construct a CI/CD pipeline that can use automated checks in addition to the code review process, and provision the resources to Azure.</p>

<p><strong>The series:</strong> <a href="/gitops1/">Part 1: Introduction to GitOps</a> · <a href="/gitops2/">Part 2: GitOps with Terraform and GitHub</a> · <a href="/gitops3/">Part 3: GitOps Provisioning with GitHub Actions</a></p>]]></content><author><name>Ben Hall</name></author><category term="GitOps" /><category term="gitops" /><category term="azure" /><category term="terraform" /><category term="github" /><category term="github actions" /><summary type="html"><![CDATA[Hands-on: write Terraform for an Azure Function App and gate infrastructure changes with a GitHub pull request. Part 2 of a Microsoft-commissioned series.]]></summary></entry><entry><title type="html">GitOps with Terraform, GitHub and Azure</title><link href="https://failingfast.io/gitops/" rel="alternate" type="text/html" title="GitOps with Terraform, GitHub and Azure" /><published>2022-06-17T00:00:00+01:00</published><updated>2022-06-17T00:00:00+01:00</updated><id>https://failingfast.io/gitops</id><content type="html" xml:base="https://failingfast.io/gitops/"><![CDATA[<p>Microsoft commissioned me to write a three-part series to demonstrate how to construct a complete end-to-end GitOps workflow using Terraform plans, GitHub, GitHub Actions, and Azure.</p>

<p>All three were first published on CodeProject. It has since closed, so I’ve republished them here:</p>

<ul>
  <li><a href="/gitops1/">Introduction to GitOps</a>. This article takes a look at GitOps — its origins, what it actually is, and how it relates to the term IaC and the pipeline concepts of CI and CD.</li>
  <li><a href="/gitops2/">GitOps with Terraform and GitHub</a>. In this article, you will see a hands-on tutorial that shows how to take a simple Terraform plan that deploys some simple Azure infrastructure (such as VMs), and commits the code to a GitHub repository. Then, you will learn how to make changes to the code in a fork or branch, commit the changes, and do code review on the changes using a pull request.</li>
  <li><a href="/gitops3/">GitOps Provisioning with GitHub Actions</a>. This article gives you hands-on experience in constructing a CI/CD pipeline that provisions changes that have been merged into a Terraform plan via a pull request. You will also see how to use automated checks to support the code review process in ‘gating’ deployments, and review the end-to-end process for making changes and seeing those changes in the live Azure resources.</li>
</ul>]]></content><author><name>Ben Hall</name></author><category term="GitOps" /><category term="gitops" /><category term="azure" /><category term="github" /><category term="github actions" /><summary type="html"><![CDATA[A 3-part series, first published on CodeProject and now republished here, that demonstrates how to construct a complete end-to-end GitOps workflow using Terraform plans, GitHub, GitHub Actions, and Azure.]]></summary></entry><entry><title type="html">Introduction to GitOps</title><link href="https://failingfast.io/gitops1/" rel="alternate" type="text/html" title="Introduction to GitOps" /><published>2022-06-17T00:00:00+01:00</published><updated>2022-06-17T00:00:00+01:00</updated><id>https://failingfast.io/introduction-to-gitops</id><content type="html" xml:base="https://failingfast.io/gitops1/"><![CDATA[<p class="notice--info"><strong>Republished:</strong> Microsoft commissioned this three-part article series from me, and it was first published on CodeProject in June 2022. CodeProject has since closed, so I’m sharing it here. This is the version I submitted; the published one had light edits. Tool and action versions reflect mid-2022.</p>

<p><strong>The series:</strong> <a href="/gitops1/">Part 1: Introduction to GitOps</a> · <a href="/gitops2/">Part 2: GitOps with Terraform and GitHub</a> · <a href="/gitops3/">Part 3: GitOps Provisioning with GitHub Actions</a></p>

<p>In the article series, we’ll explain some GitOps theory and then get some hands-on practice using Terraform, Azure, and GitHub. We will start by taking a look at GitOps - defining the need, what it actually is, and how it relates to the term IaC (Infrastructure as Code) and the pipeline concepts of CI (Continuous Integration) and CD (Continuous Deployment).</p>

<p>In this article, we will look at the history of software engineering practices and how they have led to the evolution of GitOps.</p>

<h2 id="once-there-was-waterfall">Once, There Was Waterfall</h2>

<p>For a long time, we had waterfall – an approach where the entire software development lifecycle was completed as a single, large piece of work, and the first time the software was used in anger, was on release day. With such long cycles, it was a long time before development teams got feedback and a chance to release a new version.</p>

<p>This worked just fine for the methods of software distribution available at the time – shorter cycles would have been impractical. This way of working provided Operations teams with plenty of time to provision the infrastructure for launch day.</p>

<p><img src="/assets/images/gitops/part1-1.png" alt="Figure 1" /></p>

<h2 id="then-came-agile">Then Came Agile</h2>

<p>Arguably the main benefit of adopting agile methodologies in software development, was the idea of making smaller changes and releasing much more frequently – something that was facilitated by the new methods of distribution becoming widely available – moving away from solid media.</p>

<p><img src="/assets/images/gitops/part1-2.png" alt="Figure 2" /></p>

<p>This methodology was definitely capable of getting code out faster, but Operations teams were working with tools and practices that could not always support the increased frequency of changes.</p>

<h2 id="devops-became-a-thing">DevOps Became a Thing</h2>

<p>This is one reason why DevOps evolved – to drive a culture where developer and operations teams work much more closely together. And working together, they were able to automate builds, tests, and deployments, to support the increased release cadence from agile.</p>

<p>The size and complexity of applications have grown though, with systems built from huge numbers of microservices, and the demands on those systems are growing too. As such, infrastructure solutions are going where there is the ability to scale and reduce infrastructure resources, dynamically, in response to loads. This is what the cloud does very well.</p>

<p><img src="/assets/images/gitops/part1-3.png" alt="Figure 3" /></p>

<p>Developers had benefited greatly from working closely with Operations, but all these cloud services being fired up is a lot to manage, and it is easy with old methods, to lose track of what infrastructure we <em>have</em>, what infrastructure we <em>need</em>, and whether the two marry up.</p>

<h2 id="then-there-was-gitops">Then, There Was GitOps</h2>

<p>In hindsight, it seems obvious. For operations activities to keep up with developer cadence, they should adopt the same DevOps best practices used for application development and apply them to automate the provision of infrastructure.</p>

<p>At the core of this approach is Git source control, used, just like software developers, as the single source of truth for what infrastructure is currently deployed. Layered on that we have three core components: IaC (Infrastructure as Code), Pull Requests, and CI/CD.</p>

<p><img src="/assets/images/gitops/part1-4.png" alt="Figure 4" /></p>

<h3 id="infrastructure-as-code">Infrastructure as Code</h3>

<p>The standout change with GitOps is the use of Git. Git is a version control system that tracks changes to code over time. To use Git, we now need to represent our infrastructure in code form. This is one of the three key components of GitOps – IaC (Infrastructure as Code) – where we use code to describe the desired state of our infrastructure. In this series of articles, we will be using Terraform and IaC that has gained a lot of traction in recent years.</p>

<h3 id="pull-requests-as-a-change-gate">Pull Requests as a Change Gate</h3>

<p>Operations teams use Git workflows to work collaboratively on our shared source of truth. A pull request is a key part of Git workflows. It is a way for someone working on a clone of a repository to request that changes they have made in their clone are pulled into the main repository.</p>

<p>When a pull request is opened, it is normal to require a code review. This is an opportunity for other team members to inspect the new code, write comments suggesting changes, and grant their approval – a repository will be set to block any incoming code without approval – a ‘gate’ for all changes to our infrastructure.</p>

<p><img src="/assets/images/gitops/part1-5.png" alt="Figure 5" /></p>

<p>Since Git will keep a history of all changes, it also provides a clear audit log.</p>

<h3 id="continuous-integration-and-deployment">Continuous Integration and Deployment</h3>

<p>CI (Continuous Integration) is another best practice borrowed from DevOps. It refers to automating how we integrate code changes with a repository. It helps multiple contributors to merge changes into a shared, central repository, by reducing the risks of adverse effects through changes that break things. This is achieved with tools that run automatically to execute tests when new code is added, typically in a pull request. In our flow shown in the diagram above, we can see that our infrastructure updates are only going to be merged in from the pull request after 2 conditions are met:</p>

<ol>
  <li>Code review approved by the team</li>
  <li>Automated checks (or tests) pass</li>
</ol>

<p>CD (Continuous Deployment) refers to a setup whereby all new changes pulled into our repository are automatically deployed to production (if they pass the pull request checks). This is an ideal to aim for, which requires all your processes, particularly your tests, to be excellent. Some teams choose to gate production environments with manual approvals too. Whatever level of maturity your CD is at, a common theme is the ‘pipelines’ (or tools) for automating all of the infrastructure provisioning steps.</p>

<p>CD can also refer to Continuous Delivery – where the repository is always kept in a releasable state, even if you are not deploying every change. While your release rhythm is a matter of preference in application development, the shorter the release cycle, the smaller the changes are, reducing risk. A purist might argue that for GitOps, it is non-negotiable as the repository should reflect the current state of production. But we can argue that if the audit trail from the deployment pipeline to the repository is in place, it is always clear whether our repository is reflecting the current state or a future desired state at any point in time.</p>

<h2 id="a-quick-note-on-push-vs-pull-deployments">A Quick Note on Push vs. Pull Deployments</h2>

<p>GitOps deployments can either be push- or pull-based. They differ in the way they keep the environments in the desired state. Pull-based is preferred as it is more secure and offers better support for Continuous Delivery. But push-based is simpler to get started with and the way that Terraform is designed to work.</p>

<p>Push-based will be more familiar to those coming from application development. When the application code is updated in the repository, a build pipeline gets triggered that ultimately pushes the changes to the production environment(s). It wouldn’t automatically notice any changes made directly to environments.</p>

<p>Pull-based deployments are not triggered by code changes. We need an ‘operator’ service that continuously compares desired state in the repository code with the actual current state in the deployed infrastructure. If it finds changes, it pulls in the changes, updating the infrastructure to match the repository.</p>

<h2 id="summary">Summary</h2>

<p>GitOps provides a single, unified tool for an organization to manage both their infrastructure <em>and</em> application development; building on improvements to collaboration made with DevOps. There are many benefits, including:</p>

<ul>
  <li><strong>Disaster recovery</strong>: Redeploy any or all of your infrastructure rapidly. Or roll back and forward to any point in time in the Git log to deploy a particular desired state of your infrastructure.</li>
  <li><strong>Security:</strong> Using Git means that all changes are logged in the history and change details can be recorded in pull requests and Git commit messages – answering those audit questions of the who, what and why for infrastructure changes. GitOps also reduces the risk of sharing login credentials – developers no longer need to have access to servers.</li>
  <li><strong>Self-documenting deployments</strong>: All changes made to your environments go through your Git repository. You only have to look at the latest commit on your <em>main</em> branch to know exactly what is in production. No need to remote into servers to audit state.</li>
</ul>

<p>In the next article, we will put theory into practice and write some IaC using Terraform, push it up to a Git repository on GitHub, and have a go at a simple workflow using a code review process with a pull request to merge infrastructure changes into our code.</p>

<p><strong>The series:</strong> <a href="/gitops1/">Part 1: Introduction to GitOps</a> · <a href="/gitops2/">Part 2: GitOps with Terraform and GitHub</a> · <a href="/gitops3/">Part 3: GitOps Provisioning with GitHub Actions</a></p>]]></content><author><name>Ben Hall</name></author><category term="GitOps" /><category term="gitops" /><category term="azure" /><category term="terraform" /><category term="github" /><category term="github actions" /><summary type="html"><![CDATA[Where GitOps came from, what it actually is, and how it relates to IaC, pull requests and CI/CD. Part 1 of a Microsoft-commissioned series.]]></summary></entry><entry><title type="html">Manual Docker Setup on Windows</title><link href="https://failingfast.io/docker-wsl-2/" rel="alternate" type="text/html" title="Manual Docker Setup on Windows" /><published>2021-12-11T00:00:00+00:00</published><updated>2021-12-11T00:00:00+00:00</updated><id>https://failingfast.io/docker-on-wsl</id><content type="html" xml:base="https://failingfast.io/docker-wsl-2/"><![CDATA[<p>If, like me, you’re moving between a number of environments for development, you may not always have access to Docker Desktop.</p>

<p>Well, don’t panic! It is still possible to get up and running in Windows without Docker Desktop. Why? Because we’re developers with super powers and there is <em>nothing</em> we cannot do when we’re given the time to solve a problem.</p>

<p>Notice I said ‘given the time’ - just because we can, doesn’t mean we should all of the time.  It’s complicated and comes with an ongoing commitment to maintaining it all - not ideal for productivity, and your security team won’t be pleased if you don’t keep everything patched.</p>

<p>That said, there have been occasions when Docker Desktop has not been available to me and it has been useful to know how to use Docker in Windows, nonetheless.</p>

<p>In this article I’ll walk you through the process that I follow with Windows Subsystem for Linux (WSL) 2 that lives in a small executable, wsl.exe, in c:\windows\system32. It will allow us to run Linux containers natively on Windows, without emulation.</p>

<h2 id="prerequisites">Prerequisites</h2>

<p>These are shared with Docker Desktop so, if you had previously setup Docker Desktop, you may not need to complete these.</p>

<ol>
  <li>Install Windows 10 version 1903 or higher or Windows 11.</li>
  <li>Enable the WSL 2 feature on Windows.</li>
  <li>Download and install the Linux kernel update package.</li>
</ol>

<h2 id="creating-a-wsl-2-distribution">Creating a WSL 2 Distribution</h2>

<p>1. First, install a Linux distro on WSL 2 - either from the Microsoft Store or from the command line. I’m using Ubuntu in this example.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">wsl</span><span class="w"> </span><span class="nt">--install</span><span class="w"> </span><span class="nt">-d</span><span class="w"> </span><span class="nx">Ubuntu</span><span class="w">
</span></code></pre></div></div>

<p>2. When the installation completes, an automatic process to create a non-root user begins. Other distros may require you to configure a non-root user and sudo access yourself.</p>

<p>3. You should also confirm the command installed WSL 2 (not v1):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">wsl</span><span class="w"> </span><span class="nt">--list</span><span class="w"> </span><span class="nt">--verbose</span><span class="w">
</span></code></pre></div></div>

<p>4. And if it did not, run the following command to upgrade it to v2:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">wsl</span><span class="w"> </span><span class="nt">--set-version</span><span class="w"> </span><span class="nx">Ubuntu</span><span class="w"> </span><span class="nx">2</span><span class="w">
</span></code></pre></div></div>

<p>5. Remember, you will be managing this VM yourself, so now is a good time to update the Ubuntu distro. Switch to your Ubuntu distro’s WSL 2 Bash shell:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">wsl</span><span class="w"> </span><span class="nt">--distribution</span><span class="w"> </span><span class="nx">Ubuntu</span><span class="w">
</span></code></pre></div></div>

<p>And run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt upgrade
</code></pre></div></div>

<p>Next, install Docker manually by setting up Docker Engine and launching <code class="language-plaintext highlighter-rouge">dockerd</code>. Refer to the complete official <a href="https://docs.docker.com/engine/install/ubuntu/">guide for Ubuntu</a> if you get stuck, but the steps below should get you started.</p>

<h2 id="setting-up-the-docker-repository">Setting up the Docker Repository</h2>

<p>Continue in your VM Bash shell.</p>

<p>1. Install packages to allow apt to use a repository over HTTPS:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="se">\</span>
    ca-certificates <span class="se">\</span>
    curl <span class="se">\</span>
    gnupg <span class="se">\</span>
    lsb-release
</code></pre></div></div>

<p>2. Add Docker’s official GPG key:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://download.docker.com/linux/ubuntu/gpg | <span class="nb">sudo </span>gpg <span class="nt">--dearmor</span> <span class="nt">-o</span> /usr/share/keyrings/docker-archive-keyring.gpg
</code></pre></div></div>

<p>3. Use the <em>stable</em> repository:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="se">\</span>
  <span class="s2">"deb [arch=</span><span class="si">$(</span>dpkg <span class="nt">--print-architecture</span><span class="si">)</span><span class="s2"> signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu </span><span class="se">\</span><span class="s2">
  </span><span class="si">$(</span>lsb_release <span class="nt">-cs</span><span class="si">)</span><span class="s2"> stable"</span> | <span class="nb">sudo tee</span> /etc/apt/sources.list.d/docker.list <span class="o">&gt;</span> /dev/null
</code></pre></div></div>

<h2 id="installing-docker-engine-on-the-wsl-2-vm">Installing Docker Engine on the WSL 2 VM</h2>

<p>Continue in your VM Bash shell.</p>

<p>1. Update the apt package index next:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get update
</code></pre></div></div>

<p>2. For this example, install the latest version of Docker Engine and <code class="language-plaintext highlighter-rouge">containerd</code> with the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get <span class="nb">install </span>docker-ce docker-ce-cli containerd.io
</code></pre></div></div>

<p>3. Test it with the following command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker run hello-world
</code></pre></div></div>

<p>You have now manually installed the Docker Engine on an Ubuntu WSL 2 VM. However, since you’re managing it yourself, there’s still more to do.</p>

<h2 id="additional-setup-and-challenges">Additional Setup and Challenges</h2>

<p>To manage Docker as a non-root user, rather than using <code class="language-plaintext highlighter-rouge">sudo</code> to run Docker commands, you must create a group, add a user, and possibly modify some file permissions. I won’t go through the details here, but you can follow the <a href="https://docs.docker.com/engine/install/linux-postinstall/">post-installation steps for Linux</a> if you’ve got the time to do a proper job.</p>

<p>The post-installation steps also offer guidance on other challenges you might face while managing your own Docker VM, such as:</p>

<ul>
  <li>Configuring Docker to start on boot</li>
  <li>Logging</li>
  <li>Accepting connections to Docker daemon from remote hosts</li>
  <li>Configuring remote access</li>
  <li>Enabling IPv6</li>
  <li>Fixing IP forwarding problems</li>
</ul>

<h2 id="troubleshooting-cannot-connect-to-the-docker-daemon">Troubleshooting: Cannot Connect to the Docker Daemon</h2>

<p>You were warned - Docker Engine and WSL 2 VM setup is a complex operation to take on. You’re now the admin of a Linux system and responsible for solving any problems. For example, you may have received the following error when you tried to run <code class="language-plaintext highlighter-rouge">hello-world</code>:</p>

<p><img src="/assets/images/docker/dockerconnect.png" alt="Cannot connect to the Docker daemon" /></p>

<p>You received this error because you are not working with a Debian-based distribution and must start the Docker service manually using <code class="language-plaintext highlighter-rouge">systemctl</code> or the <code class="language-plaintext highlighter-rouge">service</code> command. You probably won’t have <code class="language-plaintext highlighter-rouge">systemctl</code> out-of-the-box with the Ubuntu WSL distro, so use the service command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>service docker start
</code></pre></div></div>

<p>When you rerun the <code class="language-plaintext highlighter-rouge">hello-world</code> test, you should now get a <code class="language-plaintext highlighter-rouge">“Hello from Docker!”</code>.</p>]]></content><author><name>Ben Hall</name></author><category term="Containers" /><category term="Docker" /><category term="WSL" /><summary type="html"><![CDATA[An alternative using WSL 2, where Docker Desktop is not available to you in Windows]]></summary></entry><entry><title type="html">.NET 6 LINQ Improvements - Count Performance</title><link href="https://failingfast.io/linq-net6-count/" rel="alternate" type="text/html" title=".NET 6 LINQ Improvements - Count Performance" /><published>2021-09-11T00:00:00+01:00</published><updated>2021-09-11T00:00:00+01:00</updated><id>https://failingfast.io/net6-linq-count</id><content type="html" xml:base="https://failingfast.io/linq-net6-count/"><![CDATA[<p>For some C# collection types, a call to Count() may have to enumerate the <em>entire</em> collection to determine the number of elements.</p>

<p>.NET 6 has introduced a new method <code class="language-plaintext highlighter-rouge">TryGetNonEnumeratedCount()</code> to count the number of elements in a sequence more efficiently.</p>

<p>From the <a href="https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.trygetnonenumeratedcount?view=net-6.0">API docs</a>:</p>

<blockquote>
  <p>The method performs a series of type tests, identifying common subtypes whose count can be determined without enumerating; this includes <code class="language-plaintext highlighter-rouge">ICollection&lt;T&gt;</code>, <code class="language-plaintext highlighter-rouge">ICollection</code> as well as internal types used in the LINQ implementation.</p>
</blockquote>

<p>The method really just avoids the developer having to check the underlying type of a collection - you may still need to call <code class="language-plaintext highlighter-rouge">Count()</code> when it cannot determine the count quickly and so returns <code class="language-plaintext highlighter-rouge">false</code> and the <code class="language-plaintext highlighter-rouge">out</code> parameter returns 0.</p>]]></content><author><name>Ben Hall</name></author><category term=".NET" /><category term="LINQ" /><category term=".NET6" /><category term="LINQ" /><summary type="html"><![CDATA[.NET 6 introduces new LINQ method to count the number of elements in a sequence more efficiently.]]></summary></entry></feed>