<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by patst on Medium]]></title>
        <description><![CDATA[Stories by patst on Medium]]></description>
        <link>https://medium.com/@pat-st?source=rss-26e3253ec3d1------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*9rykld9Bbrfa9aen5wrafA.png</url>
            <title>Stories by patst on Medium</title>
            <link>https://medium.com/@pat-st?source=rss-26e3253ec3d1------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 07:47:57 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@pat-st/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Securing a Model Context Protocol Server with EntraID]]></title>
            <link>https://itnext.io/securing-a-model-context-protocol-server-with-entraid-47a0fea72a76?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/47a0fea72a76</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[github]]></category>
            <category><![CDATA[model-context-protocol]]></category>
            <category><![CDATA[oauth]]></category>
            <category><![CDATA[azure]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Sun, 21 Sep 2025 08:20:04 GMT</pubDate>
            <atom:updated>2025-09-21T10:16:47.132Z</atom:updated>
            <content:encoded><![CDATA[<p>We want to build an MCP server secured by EntraID, which can then be used by Github Copilot in VSCode.</p><p>There are plenty of tutorials out there on how to get started building MCP servers and clients, but most of them stop after building functional requirements and do not describe the authorization and authentication implementation.</p><p>Some tools do not need to authenticate users, because they only provide read access to publicly available resources. But the advanced use cases will need access with an authenticated identity.</p><p>This article will describe:</p><ul><li>history of authorization in MCP</li><li>how EntraID can be used to secure an MCP server (and what features are missing)</li><li>how to build a Typescript MCP server secured by EntraID which can be used in VSCode with Github Copilot</li><li>provide users tools, depending on which permissions they have</li></ul><p>The demo code is provided in the repository at <a href="https://github.com/patst/entraid-mcp-server"><strong>https://github.com/patst/entraid-mcp-server</strong></a> .<br>Clone the repository and follow the instructions below to configure the required variables.</p><h3>Model Context Protocol</h3><p>The Model Context Protocol is a standardized communication protocol that enables AI agents to interact with external tools, databases, and services. It can provide the bridge between large language models and the already existing APIs (enterprise) applications.</p><p>MCP servers expose ‘<em>tools</em>’ that AI agents can discover and call. These tools can range from simple utilities like text processing to complex operations like database queries, API calls or file system operations. The protocol defines how these tools are described, discovered, and invoked, creating a consistent interface for AI agent interactions.</p><p>More about the MCP basic architecture and building blocks can be found in the awesome MCP specification at <a href="https://modelcontextprotocol.io/specification/2025-06-18/architecture">https://modelcontextprotocol.io/specification/2025-06-18/architecture</a></p><h3><em>Authentication and authorization in MCP</em></h3><p>The MCP protocol’s first specification was released in November 2024. <br>Since then, the security topic has received a lot of attention in the specification releases:</p><ol><li><strong>2024–11–05</strong>: Only mentions that “MCP cannot enforce security principals” and implementation should “follow best practices”</li><li><strong>2025–03–26</strong>: Dedicated <a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization">Authorization chapter</a>. <strong>OAuth 2.1</strong> defined as security standard. MCP Server acts as <strong>Authorization Server</strong></li><li><strong>2025–06–18</strong>: The MCP server now only takes the <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#standards-compliance">OAuth resource server</a> role. A <em>protected resource metadata</em> document (see below) needs to be provided</li></ol><p>It is worth reading through the specification; great to see the improvements.<br>Especially the last specification release, which treats the MCP server as a resource server in an OAuth sense, really simplifies the implementation steps required.</p><p>The authorization flow is described here:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MOCs6V1z_faAG3aQo3ElCQ.png" /><figcaption>source: <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#sequence-diagram">https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#sequence-diagram</a></figcaption></figure><h3>EntraID configuration</h3><p><a href="https://learn.microsoft.com/en-us/entra/fundamentals/what-is-entra">EntraID</a> is used as an Identity Provider in a lot of companies. <br>It supports OAuth as an authentication protocol with public and secret clients.</p><p>For this tutorial, we will need an <a href="https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app">Application Registration</a> which can be created using the Azure CLI:</p><pre>az ad app create --display-name &quot;MCP-Server Demo&quot; \<br>  --requested-access-token-version &quot;2&quot; \<br>  --sign-in-audience &quot;AzureADMyOrg&quot;</pre><p>Open the application registration in the Azure Portal for the final configuration to expose an API:</p><ul><li>at `<em>Expose an API</em>`, expose a scope named `<em>user_impersonation</em>` for the app registration (it may already exist, because the Azure CLI created it)</li><li>at `<em>Expose an API</em>`, add the ClientIds as preauthorized client applications:<br>- `<em>aebc6443–996d-45c2–90f0–388ff96faa56</em>` — <a href="https://github.com/microsoft/vscode/blob/28580e2402b299a451868c3388309b951781b2d3/extensions/microsoft-authentication/src/common/scopeData.ts#L8">default Client ID for VSCode</a>. This is required to use the MCP Server with Github Copilot from VSCode<br> - `<em>04b07795–8ddb-461a-bbee-02f9e1bf7b46</em>` — Default Client ID for Azure CLI, allows fetching of tokens for test purposes</li></ul><p>We need the <em>ClientId</em> of the created application registration together with your <em>TenantId</em>.<br>Create a <strong>.env</strong> file in the root directory of the <a href="https://github.com/patst/entraid-mcp-server">demo repository</a> with this content:</p><pre>AZURE_TENANT_ID=&lt;valueFromEntraID&gt;<br>AZURE_CLIENT_ID=&lt;valueFromEntraID&gt;<br>BASE_URL=http://localhost:3001</pre><h3>OAuth resource server implementation</h3><p>The MCP protocol requires the server to provide a <a href="https://datatracker.ietf.org/doc/html/rfc9728">OAuth 2.0 Protected Resource Metadata (RFC9728)</a> endpoint.</p><p>The demo project uses expressjs as web server and the code for the endpoint looks like this:</p><pre>app.get(&#39;/.well-known/oauth-protected-resource&#39;, async (_: Request, res: Response) =&gt; {<br>  res.json({<br>    resource: `${process.env.BASE_URL}/mcp`,<br>    authorization_servers: [`https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/v2.0`],<br>    bearer_methods_supported: [&quot;header&quot;],<br>    scopes_supported: [`api://${process.env.AZURE_CLIENT_ID}/user_impersonation`]<br>  });<br>});</pre><p>Whenever an MCP Client calls the MCP server and the server decides that the client needs to be authorized, the server returns the metadata which points the client to an identity server and provides all required information to fetch an access token.</p><p>We provide a list of supported scopes, which contains only our <em>user_impersonation</em> scope at the moment. <br>We use the environment variables for the EntraId, TenantId and ClientId we configured above.</p><h3>Authentication middleware</h3><p>Our server should support both authenticated and anonymous access.</p><p>The idea behind that is to allow clients to express they want to use the server in anonymous mode. The server then only provides the tools which can be used without an identity.<br>The client must specify the HTTP header `<em>x-use-anonymous</em>` to state that intent.</p><pre>const authMiddleware = async function (req: Request, res: Response, next: any) {<br>  if (req.headers[&quot;x-use-anonymous&quot;]) {<br>    // Skip authentication for anonymous mode<br>    next();<br>  } else {<br>    // Require Bearer token authentication<br>    bearerAuthMiddleware(req, res, next);<br>  }<br>};</pre><p>The `<em>bearerAuthMiddleware</em>` uses a implementation provided by the MCP SDK:</p><pre>import { requireBearerAuth } from &quot;@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js&quot;;<br><br>//...<br><br>// this middleware checks authorization token and returns a WWW-Authenticate error with infos where to get an auth token<br>const bearerAuthMiddleware = requireBearerAuth({<br>  verifier: tokenVerifier,<br>  requiredScopes: [&quot;user_impersonation&quot;],<br>  resourceMetadataUrl: `${process.env.BASE_URL}/.well-known/oauth-protected-resource`<br>})</pre><p>I was surprised to not find a predefined `<em>tokenVerifier</em>` implementation.<br>Either it is missing in the SDK or I was not able to find it.<br>The token verification is very important to check if the access tokens are really issued FOR your application and signed by your EntraID tenant.</p><p>The <a href="https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets">JSON Web Key Sets</a> URL provided by EntraId is used for checking the signature of the token:</p><pre>const jwksClient = new JwksClient({<br>  jwksUri: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}/discovery/v2.0/keys`<br>});<br><br>const tokenVerifier: OAuthTokenVerifier = {<br>  verifyAccessToken: async (token: string): Promise&lt;AuthInfo&gt; =&gt; {<br>    // validate signature<br>    const tokenHeader: string = token.split(&quot;.&quot;)[0] as string;<br>    const signingKey = await jwksClient.getSigningKey(JSON.parse(atob(tokenHeader))[&quot;kid&quot;]);<br>    await new Promise((resolve: any, reject) =&gt;<br>        jwt.verify(token, signingKey.getPublicKey(), (err, decoded) =&gt; err ? reject(err.message) : resolve(decoded))<br>    );<br>    console.log(&quot;Access token verified successfully&quot;);<br>    <br>    // Extract relevant claims<br>    const tokenPayload: string = token.split(&quot;.&quot;)[1] as string;<br>    const tokenParsed: any = JSON.parse(atob(tokenPayload));<br>    // Convert the response to AuthInfo format<br>    return {<br>      token,<br>      clientId: tokenParsed.aud,<br>      scopes: tokenParsed.scp ? tokenParsed.scp.split(&#39; &#39;) : [],<br>      expiresAt: tokenParsed.exp,<br>      extra: {<br>        groups: tokenParsed.groups ? tokenParsed.groups : [],<br>        username: tokenParsed.preferred_username,<br>      }<br>    };<br>  }<br>}</pre><p>The `<em>return</em>` part of the function returns an `<em>AuthInfo</em>` object defined by the MCP SDK. The `<em>extra</em>` attributes allow specifying arbitrary values, which can later be used for making authorization decisions if the scopes are not enough.</p><h3>Dynamic tool access based on authentication</h3><p>Now we are able to authenticate users. The next step is to make authorization decisions based on the identity.</p><p>There are two possible places for that:</p><ol><li>provide different lists of tools, resources, prompts, etc. based on an identity (e.g. hide admin tools if the user has no admin permission)</li><li>Return different output during tool calls (e.g. a user can only look up customer information he has access to)</li></ol><h4>Different tools based on user identity</h4><p>First, we decide to show a tool only if the user is authenticated.<br>This is done by accessing the <em>AuthInfo</em> object during the <em>ListToolsRequestSchema</em> API call.</p><pre>server.setRequestHandler(ListToolsRequestSchema, async function (_, extra) {<br>  const authInfo = extra.authInfo;<br><br>  // Base tools available to everyone (including anonymous users)<br>  const tools: Tool[] = [<br>    {<br>      name: ToolName.ECHO,<br>      description: &quot;Echoes back the input&quot;,<br>      inputSchema: zodToJsonSchema(EchoSchema) as ToolInput,<br>    },<br>  ];<br>  <br>  // Additional tools only for authenticated users<br>  if (authInfo &amp;&amp; authInfo?.extra?.username) {<br>    tools.push({<br>      name: ToolName.USER_DETAILS,<br>      description: &quot;Returns details about the authenticated user and their permissions.&quot;,<br>      inputSchema: zodToJsonSchema(UserDetailsSchema) as ToolInput,<br>    });<br>  }<br><br>  return { tools };<br>});</pre><p>In that example, the `<em>user details</em>` tool is only displayed if the user is authenticated. We could read the `<em>groups</em>` attribute from the `<em>authInfo?.extra?</em>` to make decisions based on groups as well.</p><h4>Different response for tool call base on user identity</h4><p>The user’s identity is available during the `<em>CallToolRequestSchema</em>` as well and can be used to display different data, depending on user permissions:</p><pre>server.setRequestHandler(CallToolRequestSchema, async (request, extra) =&gt; {<br>  const { name, arguments: args } = request.params;<br><br>  switch (name) {<br>    // ...<br>    case ToolName.USER_DETAILS: {<br>      console.log(&quot;Returning user info&quot;, extra.authInfo?.extra);<br>      return {<br>        content: [{ type: &quot;text&quot;, text: `Detected user ${extra.authInfo?.extra?.username}` }],<br>      }<br>    }<br>    // ...<br>  }<br>});</pre><p>Another interesting use case is to use the on-behalf-of flow to call other APIs on behalf of the currently logged-in user. Details for that can be found here: <a href="https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow">https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow</a></p><p>This would allow passing the user’s identity down the call chain for transitive APIs. (This only works for real users and not service principals)</p><h3>Running the demo</h3><p>After finishing the implementation, we now want to test the MCP server.</p><p>Start the MCP server with running `<em>npm run dev</em>`.</p><h4>Testing with MCP inspector</h4><p>We will use the <a href="https://modelcontextprotocol.io/docs/tools/inspector">MCP Inspector</a> for local testing.</p><p>Run `<em>npm run inspector</em>` in the root folder to start the inspector.</p><p>Unfortunately, EntraID is not fully compliant with the MCP authorization specification:</p><ol><li>the Entra OIDC discovery document is missing the <a href="https://github.com/modelcontextprotocol/typescript-sdk/issues/832"><em>code_challenge_methods_supported</em></a> attribute (only workaround: serve a copy of the document)</li><li><em>resource</em> request parameter is not supported (see <a href="https://github.com/microsoft/vscode/issues/254009)">AADSTS901002</a>)</li></ol><p>These two points do not allow using the OAuth flow provided by the MCP inspector. Instead, we will use the Azure CLI to get an access token and test our MCP Server (remember: we authorized the Azure CLI to fetch access tokens for our application in the <em>EntraID configuration</em> part):</p><pre>az login # use your user to login<br>az account get-access-token --scope api://${AZURE_CLIENT_ID}/user_impersonation -o tsv --query &quot;accessToken&quot;</pre><p>This will return an access token which can be copied in the <em>Authorization</em> part of the MCP inspector.</p><h4>Integration test with Github Copilot in VSCode</h4><p>Now the real test in VSCode together with Github Copilot.</p><p>Start the server (if not already running) with `<em>npm run dev</em>`.</p><p>Then open VSCode and configure your locally running MCP server in the `<em>mcp.json</em>`:</p><pre>{<br>  &quot;servers&quot;: {<br>    &quot;local-mcp-server&quot;: {<br>      &quot;url&quot;: &quot;http://localhost:3001/mcp&quot;,<br>      &quot;type&quot;: &quot;http&quot;<br>    }<br>  },<br>  &quot;inputs&quot;: []<br>}</pre><p>When VS Code connects to the server:</p><ol><li>It discovers the authentication requirements from the `.well-known/oauth-protected-resource` endpoint</li><li>Prompts the user to authenticate with EntraID</li><li>Includes the access token in subsequent requests</li><li>Receives the appropriate set of tools based on the user’s authentication status</li></ol><p>For testing anonymous access, simply add the `<em>x-use-anonymous</em>` header:</p><pre>{<br>  &quot;servers&quot;: {<br>    &quot;local-mcp-server&quot;: {<br>      &quot;url&quot;: &quot;http://localhost:3001/mcp&quot;,<br>      &quot;type&quot;: &quot;http&quot;,<br>      &quot;headers&quot;: {<br>        &quot;x-use-anonymous&quot;: &quot;true&quot;<br>      }<br>    }<br>  }<br>}</pre><h3>Key takeaways</h3><p>Implementing OAuth 2.0 authentication for MCP servers opens up new possibilities for enterprise integration while maintaining security best practices. This tutorial demonstrated how to:</p><ul><li>Configure EntraID as an identity provider for MCP servers</li><li>Implement OAuth 2.0 Protected Resource Metadata endpoints</li><li>Build authentication middleware that supports both authenticated and anonymous access</li><li>Dynamically control tool availability based on user identity and permissions</li><li>Integrate the secured MCP server with GitHub Copilot in VSCode</li></ul><p>The MCP specification’s evolution from basic security mentions to full OAuth 2.0 support shows the protocol’s maturity and enterprise readiness. While some compatibility issues exist with EntraId (resource parameter support), workarounds allow for functional authentication flows.</p><p>With authenticated MCP servers, AI agents can now access enterprise resources securely, respecting user permissions and organizational boundaries. This enables more sophisticated use cases like personalized tool access, audit trails, and integration with existing identity management systems.</p><p>As the MCP ecosystem continues to grow, secure authentication will become increasingly important for enterprise adoption and real-world AI agent deployments.</p><h4>Links</h4><ul><li>Demo Repository: <a href="https://github.com/patst/entraid-mcp-server">https://github.com/patst/entraid-mcp-server</a></li><li>MCP Spec for Authorization: <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization">https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization</a></li><li>Entra ID resource parameter not supported issue: <a href="https://github.com/microsoft/vscode/issues/254009">https://github.com/microsoft/vscode/issues/254009</a></li><li><em>code_challenge_not_supported</em> issue: <a href="https://github.com/modelcontextprotocol/typescript-sdk/issues/832">https://github.com/modelcontextprotocol/typescript-sdk/issues/832</a></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/658/1*VlGZ5bufdZtd3OSDP6HAvQ.png" /><figcaption>EntraID and the Model Context Protocol can work together</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=47a0fea72a76" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/securing-a-model-context-protocol-server-with-entraid-47a0fea72a76">Securing a Model Context Protocol Server with EntraID</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Send traces from Azure API-Management to OpenTelemetry Collector]]></title>
            <link>https://itnext.io/send-traces-from-azure-api-management-to-opentelemetry-collector-e0fd5e27f735?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/e0fd5e27f735</guid>
            <category><![CDATA[api]]></category>
            <category><![CDATA[opentelemetry]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[observability]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Thu, 09 Jan 2025 07:45:09 GMT</pubDate>
            <atom:updated>2025-01-09T09:33:11.283Z</atom:updated>
            <cc:license>http://creativecommons.org/licenses/by/4.0/</cc:license>
            <content:encoded><![CDATA[<p>This story is about connecting <a href="https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts"><em>Azure API-Management</em></a> with an <a href="https://opentelemetry.io/docs/what-is-opentelemetry/"><em>OpenTelemetry</em></a> (<em>OTel</em>) collector using a custom receiver.</p><p>Tracing is a very important component in a distributed system to get insights into what is happening and what might be slow or even broken.</p><p><em>Azure API-Management</em> is a Platform-as-a-Service offering which provides an API-Management platform for APIs across different environments/cloud-providers. One of the provider components are API-Gateways which are responsible for routing, security, throttling, caching, and <strong>observability</strong>.</p><p>The different features can be configured and activated using <a href="https://learn.microsoft.com/en-us/azure/api-management/api-management-howto-policies"><em>Policies</em></a>. Policies can be configured per API, for certain methods or globally for all APIs. Examples for policies are JWT validation, authentication to backend services or sending either synchronous or asynchronous calls to services.</p><p>For this article the observability of the API-Gateway is important (<a href="https://learn.microsoft.com/en-us/azure/api-management/observability">docs</a>). Azure provides integration into the Azure ecosystem with connections to <em>Azure Application Insights</em> (metrics), <em>Azure Monitor</em> (logs/metrics) and <em>EventHub</em> (logging).</p><p>The piece unfortunately missing is the integration into a tracing solution.</p><p>This is where <em>OpenTelemetry</em> comes to the rescue. <em>OpenTelemetry</em> is the de-facto standard for observability, providing specifications and SDKs for several observability signals (e.g. Logs, Metrics, Traces).</p><p>With <a href="https://grafana.com/oss/tempo/"><em>Grafana Tempo</em></a> we already have a central place to store all our traces in. Our goal now is to be able to ingest traces from the API-Gateways to our central tracing backend.</p><h3>Architecture</h3><p>Most of the required components can be used from the OpenTelemetry project. The missing piece, is getting trace information from the API-Gateways to an OpenTelemetry <strong>collector</strong>.</p><p>A collector is a “<em>Vendor-agnostic way to receive, process and export telemetry data</em>” (source: <a href="https://opentelemetry.io/docs/collector/">OTel</a>).</p><p>An OpenTelemetry collector has different parts which are described in this picture:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tGTfZr0qKEqb7XrjVZSPAQ.png" /><figcaption>OpenTelemetry collector architecture (source: <a href="https://opentelemetry.io/docs/collector/">https://opentelemetry.io/docs/collector/</a> )</figcaption></figure><p>We can use the OTLP exporter to export traces to Grafana Tempo. To get the traces from the API-Gateway to the collector we will implement a custom OpenTelemetry <strong>receiver</strong>.</p><p>The receiver is responsible for getting trace information and then map it to the OpenTelemetry object model, which is described in the <a href="https://opentelemetry.io/docs/specs/otel/overview/#tracing-signal"><em>Tracing Signal specification</em></a>.</p><p>Lucky for us there is already a well-written tutorial for <a href="https://opentelemetry.io/docs/collector/building/receiver/"><em>Building a receiver</em></a> in the OpenTelemetry docs.</p><p>We do not want to block API calls with our tracing solution, therefore we need an asynchronous communication way between the API-Gateway and the OpenTelemetry receiver. To implement that, we will use <a href="https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-about"><em>Azure EventHubs</em></a> as event store.</p><p>The complete architecture diagram looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/605/1*oA78bwgfhjhvsaITQBSp1g.png" /><figcaption>Custom API-Management OpenTelemetry integration architecture</figcaption></figure><p>A request traverses the architecture in the steps described in the image above:</p><ol><li>A User (or application) calls an API published in the API-Management. The request is send to the API-Gateway</li><li>A policy inside the API-Gateway checks if a <strong>traceparent</strong> header exists. If none already exists a new one will be created, otherwise only a new SpanId will be generated. The new traceparent header will be passed on to the API backend.</li><li>The requests is forwarded to the API backend contains the implementation</li><li>The API backend returns the response to the API-Gateway.</li><li>The required information for a trace is gathered and send as event to the EventHub</li><li>The caller receives the response for the API call (if there is one)</li><li>The custom OpenTelemetry receiver listens for new events in the EventHub and if there is one it will be converted according to the OpenTelemetry trace specification</li><li>The OpenTelemetry collector exports the trace to a trace backend, which can be Grafana Tempo or Jaeger.</li></ol><h3>Implementation</h3><p>This is not intended as production-ready implementation, but as a starting point, if you want to build such an architecture.</p><p>First we need to bootstrap a OpenTelemetry collector project using the OpenTelemetry <a href="https://github.com/open-telemetry/opentelemetry-collector/tree/main/cmd/builder">Collector Builder (ocb)</a>.</p><p>First we create a builder config and then generate the code from the configuration (builder-config.yaml ):</p><pre>dist:<br>  name: otelcol-dev<br>  description: Basic OTel Collector distribution for Developers<br>  output_path: ./otelcol-dev<br><br>exporters:<br>  - gomod:<br>      go.opentelemetry.io/collector/exporter/debugexporter v0.115.0<br>  - gomod:<br>      go.opentelemetry.io/collector/exporter/otlphttpexporter v0.115.0<br><br>processors:<br>  - gomod:<br>      go.opentelemetry.io/collector/processor/batchprocessor v0.115.0<br>extensions:<br>  - gomod:<br>      github.com/open-telemetry/opentelemetry-collector-contrib/extension/basicauthextension v0.115.0<br><br>providers:<br>  - gomod: go.opentelemetry.io/collector/confmap/provider/envprovider v1.18.0<br>  - gomod: go.opentelemetry.io/collector/confmap/provider/fileprovider v1.18.0<br>  - gomod: go.opentelemetry.io/collector/confmap/provider/httpprovider v1.18.0<br>  - gomod: go.opentelemetry.io/collector/confmap/provider/httpsprovider v1.18.0<br>  - gomod: go.opentelemetry.io/collector/confmap/provider/yamlprovider v1.18.0</pre><pre># generate the code for the collector using the collector builder tool<br># change the cpu architecture if you are not using linux_amd64<br>curl --proto &#39;=https&#39; --tlsv1.2 -fL -o ocb \<br>https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/cmd%2Fbuilder%2Fv0.116.0/ocb_0.116.0_linux_amd64<br>chmod +x ocb<br>./ocb --config builder-config.yaml</pre><p>This way we can configure what extensions, receivers, exporter and processor should be included in our custom collector. Above is mainly the default configuration.</p><p>After that we have a skeleton generated which looks like this:</p><pre>.<br>├── builder-config.yaml<br>├── ocb<br>└── otelcol-dev<br>    ├── components.go<br>    ├── components_test.go<br>    ├── go.mod<br>    ├── go.sum<br>    ├── main.go<br>    ├── main_others.go<br>    ├── main_windows.go<br>    └── otelcol-dev</pre><p>After that we initialise our custom receiver code which follows exactly the <a href="https://opentelemetry.io/docs/collector/building/receiver/#setting-up-go-module">OpenTelemetry tutorial</a>.</p><pre>mkdir apimtracer<br>cd apimtracer<br>go mod init github.com/patst/azure-apim-otel-receiver/apimtracer<br>cd ..<br>go work init<br>go work use otelcol-dev<br>go work use apimtracer</pre><p>I do not want to go into the detailled implementation steps, but only highlight the required functions and structs. The whole example code can be found at <a href="https://github.com/patst/azure-apim-otel-receiver">https://github.com/patst/azure-apim-otel-receiver</a> .</p><h4>Configuration and event structure</h4><p>First we need to define the possible configuration parameters for our custom receiver in the apimtracer/config.go file:</p><pre>type Config struct {<br> FullyQualifiedNamespace string `mapstructure:&quot;fully_qualified_namespace&quot;`<br> EventHubName            string `mapstructure:&quot;event_hub_name&quot;`<br> ConsumerGroup           string `mapstructure:&quot;consumer_group&quot;`<br> StorageContainerUrl     string `mapstructure:&quot;storage_container_url&quot;`<br>}</pre><p>These values can be used in our collector configuration later. It contains all required parameters for the receiver to connect to an Event Hub.</p><p>Next we define the event structure we are expecting to receive for each API request. This structure will later be mapped to a object adhering to the OpenTelemetry trace specification:</p><pre>type ApimTracePayload struct {<br>    ApiName          string  `json:&quot;api_name&quot;`<br>    SubscriptionName string  `json:&quot;subscription_name&quot;`<br>    ProductName      string  `json:&quot;product_name&quot;`<br>    TraceParent      *string `json:&quot;traceparent,omitempty&quot;`<br>    ParentSpanId     *string `json:&quot;parent_span_id,omitempty&quot;`<br>    HttpTarget       string  `json:&quot;http_target&quot;`<br>    HttpMethod       string  `json:&quot;http_method&quot;`<br>    HttpHost         string  `json:&quot;http_host&quot;`<br>    HttpStatusCode   int     `json:&quot;http_status_code&quot;`<br>    HttpClientIp     string  `json:&quot;http_client_ip&quot;`<br>    RequestStartTime int64   `json:&quot;request_start_time&quot;`<br>    RequestEndTime   int64   `json:&quot;request_end_time&quot;`<br>}</pre><p>We want to collect information about the duration, HTTP status codes, client ip, the API name and some more attributes which are later queryable. We expect a JSON string to be received from Azure Event Hubs which can be parsed to this struct (json.Unmarshall([]byte(eventData), &amp;apimTracePayLoad)).</p><h4>Mapping event to trace object</h4><p>The complete mapping logic can be found in the trace-receiver.go file (<a href="https://github.com/patst/azure-apim-otel-receiver/blob/494f3d53da6aee13c623df31f3707deb30c69bf2/apimtracer/model.go#L30">source</a>). Basically it extracts all information from the event and creates a OpenTelemetry span object:</p><pre> resourceSpan := traces.ResourceSpans().AppendEmpty()<br> resource := resourceSpan.Resource()<br> resourceAttrs := resource.Attributes()<br> resourceAttrs.PutStr(&quot;apim.product.name&quot;, apimTracePayload.ProductName)<br> resourceAttrs.PutStr(conventions.AttributeServiceName, &quot;apim&quot;)<br> resourceAttrs.PutStr(&quot;apim.api.name&quot;, apimTracePayload.ApiName)<br> resourceAttrs.PutStr(&quot;apim.subscription.name&quot;, apimTracePayload.SubscriptionName)<br> // ...<br> apiSpan := scopeSpans.Spans().AppendEmpty()<br> apiSpan.SetName(fmt.Sprintf(&quot;apim: %s %s %s&quot;, apimTracePayload.HttpMethod, apimTracePayload.ApiName, apimTracePayload.HttpTarget))apiSpan.SetKind(ptrace.SpanKindServer)<br> // example traceparent: 00-a9b7d8bbcecf8c7d9d24c69cc86f694d-2defb8587ed6dfef-01<br> // format: version-trace_id-parent_id-trace_flags<br> fmt.Printf(&quot;Extracting span and trace id from traceparent %s&quot;, *apimTracePayload.TraceParent)<br> traceString := (*apimTracePayload.TraceParent)[3:35]<br> traceId, err := uuid.Parse(traceString)<br> if err != nil {<br>  return nil, fmt.Errorf(&quot;error converting traceId to uuid: traceString: %s err: %v&quot;, traceString, err)<br> } else {<br>  apiSpan.SetTraceID(pcommon.TraceID(traceId))<br> }<br><br> decodedSpanId, err := hex.DecodeString((*apimTracePayload.TraceParent)[36:52])<br> apiSpan.SetSpanID(pcommon.SpanID(decodedSpanId))<br> fmt.Printf(&quot;Extracted TraceId=%s and SpanId=%s, apiSpanId=%s&quot;, (*apimTracePayload.TraceParent)[3:35], (*apimTracePayload.TraceParent)[36:52], apiSpan.SpanID().String())<br> if apimTracePayload.ParentSpanId != nil {<br>  apiSpan.SetParentSpanID(pcommon.SpanID([]byte(*apimTracePayload.ParentSpanId)))<br> }<br><br> // request start and end time are provided in unix milliseconds<br> apiSpan.SetStartTimestamp(pcommon.NewTimestampFromTime(time.UnixMilli(apimTracePayload.RequestStartTime))) // example: OTel Start time     : 2024-12-10 14:26:32.435836303 +0000 UTC<br> apiSpan.SetEndTimestamp(pcommon.NewTimestampFromTime(time.UnixMilli(apimTracePayload.RequestEndTime)))<br> // ...<br> attributes := apiSpan.Attributes()<br> attributes.PutStr(conventions.AttributeHTTPTarget, apimTracePayload.HttpTarget)<br> attributes.PutStr(conventions.AttributeHTTPMethod, apimTracePayload.HttpMethod)<br> attributes.PutStr(conventions.AttributeHTTPHost, apimTracePayload.HttpHost)<br> attributes.PutStr(conventions.AttributeHTTPClientIP, apimTracePayload.HttpClientIp)<br> attributes.PutInt(conventions.AttributeHTTPStatusCode, int64(apimTracePayload.HttpStatusCode))</pre><p>We extract the span and trace id of the traceparent header. The header format is defined in the format version-trace_id-parent_id-trace_flags (<a href="https://www.w3.org/TR/trace-context/#traceparent-header">spec</a>). An example is <em>00-a9b7d8bbcecf8c7d9d24c69cc86f694d-2defb8587ed6dfef-01.</em></p><p>Additionally, we set the ParentSpanId if one is present in the event data. This way the tracing backend can display dependencies and hierarchy between the different spans.</p><h4>Starting the Receiver</h4><p>The next step is to implement the listening logic for the receiver.</p><p>Since we want to connect to Azure Event Hubs, we can use the Go SDK (github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs) and follow the <a href="https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-go-get-started-send">Azure instructions</a> for setting up the connection:</p><pre>func (apimtracerRcvr *apimtracerReceiver) Start(_ context.Context, host component.Host) error {<br> apimtracerRcvr.host = host<br> ctx := context.Background()<br> ctx, apimtracerRcvr.cancel = context.WithCancel(ctx)<br><br> tokenCredential, _ := azidentity.NewDefaultAzureCredential(&amp;azidentity.DefaultAzureCredentialOptions{})<br> checkClient, err := container.NewClient(apimtracerRcvr.config.StorageContainerUrl, tokenCredential, &amp;container.ClientOptions{})<br> if err != nil {<br>  return err<br> }<br> // create a checkpoint store that will be used by the event hub<br> checkpointStore, err := checkpoints.NewBlobStore(checkClient, nil)<br> if err != nil {<br>  return err<br> }<br><br> consumerClient, err := azeventhubs.NewConsumerClient(apimtracerRcvr.config.FullyQualifiedNamespace, apimtracerRcvr.config.EventHubName, apimtracerRcvr.config.ConsumerGroup, tokenCredential, &amp;azeventhubs.ConsumerClientOptions{})<br> if err != nil {<br>  return err<br> }<br> defer consumerClient.Close(ctx)<br><br> processor, err := azeventhubs.NewProcessor(consumerClient, checkpointStore, &amp;azeventhubs.ProcessorOptions{})<br> if err != nil {<br>  return err<br> }<br> //  for each partition in the event hub, create a partition client with processEvents as the function to process events<br> dispatchPartitionClients := func() {<br>  for {<br>   partitionClient := processor.NextPartitionClient(context.TODO())<br><br>   if partitionClient == nil {<br>    break<br>   }<br><br>   go func() {<br>    err := apimtracerRcvr.processEvents(partitionClient)<br>    if err != nil {<br>     apimtracerRcvr.logger.Error(fmt.Sprintf(&quot;Error processing events: %v&quot;, err))<br>     return<br>    }<br>   }()<br>  }<br> }<br> // run all partition clients<br> go dispatchPartitionClients()<br><br> processorCtx, processorCancel := context.WithCancel(context.TODO())<br> defer processorCancel()<br><br> if err := processor.Run(processorCtx); err != nil {<br>  return err<br> }<br><br> return nil<br>}</pre><p>We use the DefaultAzureCredential struct. You can either login with the AzureCLI if running locally or provide credentials via environment variables.</p><p>The listener will create a connection to a Event Hub and waits for messages. Once an event is received the processEvents(partitionClient) function is called, which then maps the event to a trace:</p><pre>func (apimtracerRcvr *apimtracerReceiver) processEvents(partitionClient *azeventhubs.ProcessorPartitionClient) error {<br> defer closePartitionResources(partitionClient)<br> for {<br>  receiveCtx, receiveCtxCancel := context.WithTimeout(context.TODO(), 1*time.Minute)<br>  events, err := partitionClient.ReceiveEvents(receiveCtx, 100, nil)<br>  receiveCtxCancel()<br><br>  if err != nil &amp;&amp; !errors.Is(err, context.DeadlineExceeded) {<br>   return err<br>  }<br>  apimtracerRcvr.logger.Info(fmt.Sprintf(&quot;\nProcessing %d event(s)&quot;, len(events)))<br>  for _, event := range events {<br>   apimtracerRcvr.logger.Debug(fmt.Sprintf(&quot;Trace received with body %v\n&quot;, string(event.Body)))<br>   trace, err := mapToTrace(string(event.Body))<br>   if err != nil {<br>    apimtracerRcvr.logger.Error(fmt.Sprintf(&quot;Error mapping trace: %v . Ignoring the trace&quot;, err))<br>    continue<br>   }<br>   err = apimtracerRcvr.nextConsumer.ConsumeTraces(receiveCtx, *trace)<br>   if err != nil {<br>    return err<br>   }<br>  }<br>  if len(events) != 0 {<br>   if err := partitionClient.UpdateCheckpoint(context.TODO(), events[len(events)-1], nil); err != nil {<br>    return err<br>   }<br>  }<br> }<br>}</pre><p>Most of the code is wiring and logging. The important part is the call of the mapToTrace(string(event.Body)) call.</p><p>The mapped trace is then passed on to the next consumer in the OpenTelemetry collector execution chain with apimtracerRcvr.nextConsumer.ConsumeTraces(receiveCtx, *trace). After that the normal OpenTelemetry collector logic can be used, e.g. for batching, mapping and exporting to a OpenTelemetry backend.</p><h4>Implementation Summary</h4><p>The number of self-written lines of code is very low. In total it should be less than 200 lines of code. The rest of the code is generated using the awesome OpenTelemetry tooling.</p><p>We can now build our custom collector using a Dockerfile and have a executable version of our collector with the Azure API-Management tracing receiver.</p><p>The complete code with a Dockerfile for the example is available here: <a href="https://github.com/patst/azure-apim-otel-receiver/tree/main">https://github.com/patst/azure-apim-otel-receiver/tree/main</a></p><h3>Creating The API-Management Policy</h3><p>Next we want to implement the API-Management policy which allows us to send traces to Azure Event Hubs.</p><p>Policies use <a href="https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-9.0">razor templates</a> and some predefined methods and objects can be used in the templates. For our implementation we need these policies:</p><ul><li><em>set-variable</em>: To pass on variables between different steps in our policy</li><li><em>set-header</em>: To modify the HTTP Headers for a request, in order to make sure to pass on a traceparent header to the backend.</li><li><em>authentication-managed-identity</em>: Authorizes the API-Management to our Event Hub to send events (You must grant the API-Management system identity the required permission!).</li><li><em>send-one-way-request</em>: To asynchronously send events to Event Hubs using the REST-API using the previously fetched access token as authorization parameter.</li></ul><p>The first step is to check, if there is already an existing traceparent header in the request.</p><ol><li>header exists: We extract the traceId and reuse it. We only create a new spanId for our request.</li><li>header doesn’t exists: We create a complete new traceheader string</li></ol><p>In both cases the result is saved in the traceparent header (existing header is overwritten, if it already existed).</p><p>Additionally we save the parentSpanId for later, to pass it on in the event data to the OpenTelemetry receiver. Code Snippet:</p><pre>  &lt;inbound&gt;<br>    &lt;set-variable name=&quot;parent_span_id&quot; value=&quot;@{ string traceparent = (string)context.Request.Headers.GetValueOrDefault(&quot;traceparent&quot;, &quot;&quot;); if (traceparent == &quot;&quot;) { return null; } else { return traceparent.Split(&#39;-&#39;)[2]; } }&quot; /&gt;<br>    &lt;set-header name=&quot;traceparent&quot; exists-action=&quot;override&quot;&gt;<br>      &lt;value&gt;@{<br>        const string allowedChars = &quot;abcdefghijkmnopqrstuvwxyz0123456789&quot;;<br>        int stringLength=16;<br>        Random rand = new Random();<br>        char[] spanIdChars = new char[stringLength];<br>        for (int i = 0; i &lt; stringLength; i++) {<br>        spanIdChars[i] = allowedChars[rand.Next(0, allowedChars.Length)];<br>        }<br>        // Check if the trace_id already exists<br>        string traceparent = (string)context.Request.Headers.GetValueOrDefault(&quot;traceparent&quot;, &quot;&quot;);<br>        string traceId = &quot;&quot;;<br>        if (string.IsNullOrEmpty(traceparent)) {<br>          traceId = Guid.NewGuid().ToString().Replace(&quot;-&quot;, &quot;&quot;);<br>        } else {<br>          traceId = traceparent.Split(&#39;-&#39;)[1];<br>        }<br>        return &quot;00-&quot; + traceId + &quot;-&quot; + new string(spanIdChars, 0, stringLength) +&quot;-01&quot;;<br>        }&lt;/value&gt;<br>    &lt;/set-header&gt;<br>    &lt;base /&gt;<br>  &lt;/inbound&gt;</pre><p>The next part of the policy is active after the request has been executed ( outbound part of the policy).</p><p>We get an access token for the Event Hub and use the <em>fire-and-forget</em> policy to not block the normal API request execution longer than required.</p><p>In the set-body attribute we create the JSON payload for the event.</p><pre>  &lt;outbound&gt;<br>    &lt;authentication-managed-identity resource=&quot;https://[eventhub-namespace-name].servicebus.windows.net&quot; output-token-variable-name=&quot;eventHubAccessToken&quot; ignore-error=&quot;false&quot; /&gt;<br>    &lt;send-one-way-request mode=&quot;new&quot; timeout=&quot;10&quot;&gt;<br>      &lt;set-url&gt;https://[eventhub-namespace-name].servicebus.windows.net/[eventhub-name]/messages&lt;/set-url&gt;<br>      &lt;set-method&gt;POST&lt;/set-method&gt;<br>      &lt;set-header name=&quot;Authorization&quot; exists-action=&quot;override&quot;&gt;<br>        &lt;value&gt;@((string)context.Variables[&quot;eventHubAccessToken&quot;])&lt;/value&gt;<br>      &lt;/set-header&gt;<br>      &lt;set-body&gt;@{<br>        return new JObject(<br>        new JProperty(&quot;api_name&quot;,context.Api.Name),<br>        new JProperty(&quot;product_name&quot;,context.Product.Name),<br>        new JProperty(&quot;traceparent&quot;,context.Request.Headers.GetValueOrDefault(&quot;traceparent&quot;, &quot;&quot;)),<br>        new JProperty(&quot;parent_span_id&quot;,(string)context.Variables[&quot;parent_span_id&quot;]),<br>        new JProperty(&quot;http_target&quot;,context.Request.Url.Path),<br>        new JProperty(&quot;http_method&quot;,context.Request.Method),<br>        new JProperty(&quot;http_host&quot;,context.Request.Url.Host),<br>        new JProperty(&quot;http_status_code&quot;,context.Response.StatusCode),<br>        new JProperty(&quot;http_client_ip&quot;,context.Request.IpAddress),<br>        new JProperty(&quot;request_start_time&quot;,((DateTimeOffset)context.Timestamp).ToUnixTimeMilliseconds()),<br>        new JProperty(&quot;request_end_time&quot;, ((DateTimeOffset)System.DateTime.UtcNow).ToUnixTimeMilliseconds()),<br>        new JProperty(&quot;subscription_name&quot;,context.Subscription.Name)<br>        ).ToString();<br>        }&lt;/set-body&gt;<br>    &lt;/send-one-way-request&gt;<br>    &lt;base /&gt;<br>  &lt;/outbound&gt;</pre><p>The payload contains exactly the attributes we have been implementing in our custom OpenTelemetry receiver.</p><p>The complete policy definition can be found in the GitHub repository (<a href="https://github.com/patst/azure-apim-otel-receiver/blob/main/api-policy.xml">source</a>).</p><h4>Required Azure infrastructure</h4><p>This article does not provide a detailled documentation how to create the required infrastructure components in Azure. A list of stuff you need to provision:</p><ul><li>Azure API-Management service (<a href="https://learn.microsoft.com/en-us/azure/api-management/get-started-create-service-instance">Azure docs</a>)</li><li>Azure Event Hub Namespace, Event Hub and consumer group (<a href="https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-create">Azure docs</a>)</li><li>Permit the API-Management Service <em>SystemAssignedIdentity</em> to publish events in your event hub (assign <a href="https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-event-hubs-data-sender"><em>Azure Event Hubs Data Sender</em></a> role)</li></ul><h3>Summary And Next Steps</h3><p>With the above described steps it is possible to send traces for requests received in the Azure API-Management to any OpenTelemetry compatible backend. It can be easily integrated in already existing OpenTelemetry collector architectures.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/342/1*4IFzCMP0MbWh-rIp-opb2A.png" /><figcaption>Azure API-Management + OpenTelemetry is a great connection</figcaption></figure><p>In general, it would be awesome to not be required to build this as a customer, but get it of-the-shelves from Azure.</p><p>The code still needs some cleanup, test cases and error handling to make sure it is really production ready. Nevertheless, it can be a starting point for further development.</p><h4>Sources</h4><ul><li>Example code repository with policy and receiver code: <a href="https://github.com/patst/azure-apim-otel-receiver">https://github.com/patst/azure-apim-otel-receiver</a></li><li>Azure API-Management docs: <a href="https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts">https://learn.microsoft.com/en-us/azure/api-management/api-management-key-concepts</a></li><li>OpenTelemetry custom receiver tutorial: <a href="https://opentelemetry.io/docs/collector/building/receiver/">https://opentelemetry.io/docs/collector/building/receiver/</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e0fd5e27f735" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/send-traces-from-azure-api-management-to-opentelemetry-collector-e0fd5e27f735">Send traces from Azure API-Management to OpenTelemetry Collector</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[GitOps and AI: Explaining Errors in CI before applying them]]></title>
            <link>https://itnext.io/gitops-and-ai-explaining-errors-in-ci-before-applying-them-d248dee7b5dd?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/d248dee7b5dd</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[gitops]]></category>
            <category><![CDATA[devops]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Mon, 19 Feb 2024 08:05:28 GMT</pubDate>
            <atom:updated>2024-02-19T08:05:28.774Z</atom:updated>
            <content:encoded><![CDATA[<h3>GitOps and AI: Explain Errors before applying them to K8S</h3><p>Shift-Left in your deployment process: Use Kubectl together with Azure OpenAI to explain errors in Kubernetes deployment manifests before they are deployed!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/640/1*XYIxpDmR1Oc_uVYTP0tJXQ.jpeg" /><figcaption>Photo by <a href="https://unsplash.com/@jannerboy62?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Nick Fewings</a> on <a href="https://unsplash.com/photos/selective-focus-photography-of-white-arrow-signage-S7cyjr_3prc?utm_content=creditCopyText&amp;utm_medium=referral&amp;utm_source=unsplash">Unsplash</a></figcaption></figure><p><a href="https://internaldeveloperplatform.org/">Internal Developer Platforms </a>(IDPs) have become invaluable tools for developers and organisations. They streamline and standardise application development and deployment.</p><p>Many of them are based on Kubernetes. Another pattern which is on the rise and which I would call the new standard for infrastructure and application deployment is the <a href="https://www.gitops.tech/">GitOps</a> pattern.</p><p>With <a href="https://fluxcd.io/">Flux</a> and <a href="https://argo-cd.readthedocs.io/en/stable/">ArgoCD</a> the CloudNativeComputingFoundation has two excellent graduated tools available for this.</p><p>The (simplified) flow looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/514/1*moqLXMGtTl5LTxs00TrUhw.png" /><figcaption>Simplified GitOps flow (image by patst)</figcaption></figure><p>This article will describe one of the challenges I observed. In addition, I describe how to use OpenAI to improve the developer experience by explaining the error messages that occur.</p><h4>The Challenge</h4><p>Having all the tooling implemented I took the time to look at the Git commit history of the infrastructure repositories. What I found, was a lot of commit messages like “<em>fix deployment</em>”, “<em>fix 2</em>”, “<em>this time it works (really!)</em>”.</p><p>Seeing this it became clear that one important puzzle piece is still missing in this GitOps process: <strong>Change validation</strong>!</p><p>Being a software developer I am very used to checks in Pull Requests, e.g. check if CI tests are still working, the application compiles, etc.</p><p>But in our current GitOps process we have been doing: <strong>Nothing</strong>!</p><h4>How to get out of the “trial-and-error”-cycle?</h4><p>The solution will depend on the tooling you are using, but the goal we have is to get feedback about the changes <strong>before</strong> they are applied by ArgoCD or Flux.</p><blockquote>&gt; We could call it shift-left for infrastructure changes.</blockquote><p>Using Kubernetes, everything ends with a kubectl apply. Regardless, if you are using Helm Charts, plain Kubernetes YAML or Kustomize: You will be able to generate <strong>Kubernetes</strong> <strong>resource definitions in YAML</strong>.</p><p>We will use this common denominator together with the kubectl --dry-run feature to get the validations we need.</p><p>The process will look like this:</p><ol><li>developer/operator makes changes in a <strong>feature branch</strong> of the infrastructure repository</li><li>CI pipeline will template the changes (=&gt; generate Kubernetes resource YAML)</li><li>templated YAML will be sent to the Kubernetes API-Server in--dry-run mode</li><li>we will save any error message in a file</li></ol><h4>Implementation</h4><p>Before we start, these are the <strong>prerequisites</strong>:</p><ul><li><a href="https://kubernetes.io/docs/reference/kubectl/">Kubectl</a> installed</li><li>(read-only) user with access to the Kubernetes clusters</li><li><a href="https://helm.sh/">Helm</a> installed</li></ul><blockquote>&gt; In this step I assume you are using Helm Charts. If you are using something else, just make sure that you will have file(s) with YAML which can be send to the Kubernetes API-Server.</blockquote><p>Make sure you are in the directory containing the helm chart and run:</p><pre># set the Kubernetes context to the namespace, the changes should be applied to later<br>helm template &quot;my-release&quot; . --debug  &gt; templated.yaml</pre><p>Did you saw it? You already have your <strong>first check implemented</strong>!</p><p>“<em>helm template</em>” will fail if the syntax of your helm chart is not correct. In that case helm will return an exit code of “<em>1</em>” and your pipeline will fail. Our first success is, developers can <strong>no longer accidentally merge syntactical wrong definitions</strong>!</p><blockquote>&gt; You can save the helm exit code in a variable with HELM_EXIT_CODE=$? to implement better error handling</blockquote><p>Next step is to send the YAML to the Kubernetes API-Server to get further feedback:</p><pre># set the Kubernetes context to the namespace, the changes should be applied to later <br>kubectl apply --dry-run=server -f app.yaml 2&gt; validation_errors.txt || echo &quot;Validation failed&quot;</pre><p>It is important to use --dry-run=server. The <a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands">Kubectl docs</a> describe the difference:</p><blockquote><strong>dry-run:</strong> Must be “none”, “server”, or “client”. If client strategy, only print the object that would be sent, without sending it. If server strategy, submit server-side request without persisting the resource.</blockquote><p>Running in server mode makes sure the API-Server validates the request, because webhooks inside the cluster are called. Some of the errors I observed frequently:</p><ul><li>using a wrong StorageClass</li><li>setting a wrong SecurityContext (e.g. runAsNonRoot: false)</li><li>…</li></ul><p>Make sure to <strong>append</strong> the content of the file validation_errors.txt written above to the P<strong>ull Request </strong>as <strong>comment</strong>. This improves developer experience, because the error is directly visible inside the Pull Request and users do not have to open the pipeline runs output.</p><h3>Improve error messages with Azure OpenAI</h3><p>Instead of just presenting developers with an error message, it’s better to provide them with a solution. Sometimes, Kubernetes error messages can be confusing and the solution might not be immediately clear.</p><p>Thanks to advancements in AI, we can use an LLM to do the heavy lifting. In this example, we use <strong>Azure OpenAI</strong>, but it will work similarly with vanilla OpenAI or any other LLM.</p><p>Before we start, these are the <strong>prerequisites</strong>:</p><ul><li><a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/">Azure OpenAI</a> deployment (e.g. with <strong>GPT-3.5</strong> or <strong>GPT-4</strong>)</li><li>Azure OpenAI API Key</li><li><a href="https://curl.se/">cURL</a></li><li><a href="https://jqlang.github.io/jq/">jQ</a></li></ul><p>We will use the output generated in the step above (validation_errors.txt) and add another step to our validation pipeline.</p><p>We will create a payload for the Azure OpenAI service, which includes:</p><ul><li>a <a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/system-message">system message</a> setting the context for what we are trying to do</li><li>the error messages of our helm dry-run</li></ul><pre>request=$(jq --arg errorMsg &quot;$(cat validation_errors.txt)&quot; -n &#39;{<br>  &quot;messages&quot;: [<br>    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;Your are a helpful assistant. \n Context: \n- You are answering question about kubernetes deployments\n - Each answer ends with a joke about Kubernetes \n - You get error messages from the Kubernetes API Server \n - you provide YAML code snippets how to fix the error&quot;},<br>    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: $errorMsg}<br>  ],<br>  &quot;max_tokens&quot;: 800,<br>  &quot;temperature&quot;: 0.9,<br>  &quot;frequency_penalty&quot;: 0,<br>  &quot;presence_penalty&quot;: 0,<br>  &quot;top_p&quot;: 0.95,<br>  &quot;stop&quot;: null<br>}&#39;)</pre><p>The complete API description can be found in the <a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/reference">Azure OpenAI REST API reference</a>.</p><p>The next step is to create a REST call and send the payload to Azure OpenAI. Then we extract the text from the response message using jQ:</p><pre># make sure to replace &quot;&lt;your-openai-instance-name&gt;&quot; with the name of your instance<br>deploymentName=&quot;gpt-4&quot; # make sure the deployment exists in your instance<br>apiKey=&quot;&quot; # set your api-key<br>curl &quot;https://&lt;your-openai-instance-name&gt;.openai.azure.com/openai/deployments/${deploymentName}/chat/completions?api-version=2023-12-01-preview&quot; \<br>    -H &quot;Content-Type: application/json&quot; \<br>    -H &quot;api-key: $apiKey&quot; \<br>    --output response.json \<br>    --data &quot;$request&quot;<br>jq -r &#39;.choices[0].message.content&#39; response.json &gt; output.txt<br># add the content of &quot;output.txt&quot; to your pull request!</pre><p>Make sure to add the content of output.txt to your pull requests as a comment.</p><p>This process will help developers understand the error messages better and find solutions more quickly, enhancing productivity and efficiency.</p><p>To improve the feedback, you can play around with the system message template.</p><p>Feel free to share your experiences in the comments!</p><h4>Further reading:</h4><ul><li>Azure OpenAI docs — <a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/">https://learn.microsoft.com/en-us/azure/ai-services/openai/</a></li><li>Helm template docs — <a href="https://helm.sh/docs/helm/helm_template/">https://helm.sh/docs/helm/helm_template/</a></li><li>Prompt engineering techniques — <a href="https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/advanced-prompt-engineering?pivots=programming-language-chat-completions">https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/advanced-prompt-engineering?pivots=programming-language-chat-completions</a></li><li>ArgoCD: <a href="https://argo-cd.readthedocs.io/en/stable/">https://argo-cd.readthedocs.io/en/stable/</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d248dee7b5dd" width="1" height="1" alt=""><hr><p><a href="https://itnext.io/gitops-and-ai-explaining-errors-in-ci-before-applying-them-d248dee7b5dd">GitOps and AI: Explaining Errors in CI before applying them</a> was originally published in <a href="https://itnext.io/">ITNEXT</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OpenTelemetry: Sending Traces from Ingress-Nginx to multi-tenant Grafana Tempo]]></title>
            <link>https://medium.com/better-programming/opentelemetry-sending-traces-from-ingress-nginx-to-multi-tenant-grafana-tempo-e98d482c733?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/e98d482c733</guid>
            <category><![CDATA[opentelemetry]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[observability]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Mon, 03 Apr 2023 19:04:33 GMT</pubDate>
            <atom:updated>2023-04-27T08:30:56.435Z</atom:updated>
            <cc:license>http://creativecommons.org/licenses/by/4.0/</cc:license>
            <content:encoded><![CDATA[<h3>OpenTelemetry: Sending Traces From Ingress-Nginx to Multi-Tenant Grafana Tempo</h3><h4>Helpful instructions to help you on your Observability journey</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/250/1*JN36EPvNr5ZfJTRQszWuKg.png" /><figcaption>OpenTelemetry logo by <a href="https://opentelemetry.io/">https://opentelemetry.io/</a></figcaption></figure><p><a href="https://opentelemetry.io/">OpenTelemetry</a> has established itself as the standard for Observability after emerging out of the OpenTracing and OpenCensus standards. It tries to standardise different kinds of Signals (logs, traces, metrics). The Observability signal relevant to this article is traces.</p><p>To better understand, it’s worth reading the basic <a href="https://opentelemetry.io/docs/concepts/">OpenTelemetry concepts</a>.</p><p>We use <a href="https://grafana.com/oss/tempo/">Grafana Tempo</a> as the backend for our traces. It allows us to store the trace information in an inexpensive object storage instead of hosting an ElasticSearch database when using a tool like Jaeger.</p><p>Because we are providing shared Kubernetes clusters for application development teams, another very important feature of Grafana Tempo is <a href="https://grafana.com/docs/tempo/latest/configuration/multitenancy/">multi-tenancy</a>.</p><h3>Basic Flow</h3><p>Each development team has a set of Kubernetes namespaces with a tenant_id label. The applications are boosted with an OpenTelemetry library (see the <a href="https://opentelemetry.io/docs/instrumentation/">list of supported languages at this link</a>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*x91W6z28z4pw8U3fSSN7vA.png" /><figcaption>Trace flow, source: self-drawn</figcaption></figure><p>Traces originate in the applications, then are sent to an OpenTelemetry collector installed in the cluster with the awesome OpenTelemetry <a href="https://opentelemetry.io/docs/k8s-operator/">Kubernetes Operator</a>. A collector pipeline has three main parts:</p><ul><li><a href="https://opentelemetry.io/docs/collector/configuration/#receivers">Receivers</a> are components that accept traces sent from workloads. We use the built-in <a href="https://github.com/open-telemetry/opentelemetry-collector/blob/main/receiver/otlpreceiver/README.md">otlp<strong> </strong>receiver</a>, which accepts traces send via gRPC. A collector can accept traces from different sources using different receiver configurations.</li><li><a href="https://opentelemetry.io/docs/collector/configuration/#processors">Processors</a> can transform, filter, or route traces. We use the processor to enrich traces with information about Kubernetes Pod and Namespace names of the sending workloads and extract a tenant id from a Namespace label.</li><li><a href="https://opentelemetry.io/docs/collector/configuration/#exporters">Exporters</a> send traces to backends like Grafana Tempo. Here we use the <a href="https://github.com/open-telemetry/opentelemetry-collector/tree/main/exporter/otlpexporter">otlp exporter</a> via gRPC.</li></ul><p>The collector pipeline for the workloads extracts the tenant id from a Namespace label using the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/k8sattributesprocessor">k8sattributes</a> processor. (The processor requires some permissions to query the Kubernetes API server. See the processor&#39;s README file.)</p><pre>receivers:<br>  otlp:<br>    protocols:<br>      grpc:<br>processors:<br>  k8sattributes/default:<br>    extract:<br>      labels:<br>      - tag_name: tenantId<br>        key: &quot;tenant-id&quot;<br>        from: namespace<br>  routing:<br>    from_attribute: tenantId<br>    attribute_source: resource<br>    table:<br>    - value: tenant1<br>      exporters: [logging, otlp/tenant1]<br>    - value: tenant2<br>      exporters: [logging, otlp/tenant2]<br>exporters:<br>  otlp/tenant1:<br>    endpoint: tempo.grafana.svc.cluster.local:4317<br>    tls:<br>      insecure: true<br>    headers:<br>      X-Scope-OrgID: tenant1<br>  otlp/tenant2:<br>    endpoint: tempo.grafana.svc.cluster.local:4317<br>    headers:<br>      X-Scope-OrgID: tenant2<br>    tls:<br>      insecure: true<br>service:<br>  pipelines:<br>    traces:<br>      receivers: [otlp]<br>      processors: [k8sattributes/default, routing]<br>      exporters: [ otlp/tenant1, otlp/tenant2]</pre><p>Next, we define an exporter for each tenant we have. They all configure the same endpoint URL for Grafana tempo but have a different X-Scope-OrgID header value. The <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/routingprocessor">routing</a> processor routes traces to the different exporters depending on the tenant id.</p><p>This is a bit cumbersome, but there is no way to use variables inside the headers block for the otlp exporter. This would enable us to have only one exporter entry with variable content in the X-Scope-OrgID header.</p><h3>Ingress-Nginx and OpenTelemetry</h3><p>Ingress-Nginx has supported OpenTelemetry since version 1.7.0 (released on March 24th, 2023. See the pull request <a href="https://github.com/kubernetes/ingress-nginx/pull/9062">#9062</a>). We were looking forward to this new feature because it allows the collection trace information not only at the workload level but already one step earlier when the traffic enters the Kubernetes cluster.</p><p>To enable the OpenTelemetry integration using the I<a href="https://github.com/kubernetes/ingress-nginx/tree/main/charts/ingress-nginx">ngress-Nginx helm chart</a>, just add these parameters:</p><pre>controller:<br>  config:<br>    enable-opentelemetry: &quot;true&quot;<br>    opentelemetry-config: &quot;/etc/nginx/opentelemetry.toml&quot;<br>    opentelemetry-operation-name: &quot;HTTP $request_method $service_name $uri&quot;<br>    opentelemetry-trust-incoming-span: &quot;true&quot;<br>    otlp-collector-host: &quot;otel-collector.grafana.svc.cluster.local&quot;<br>    otlp-collector-port: &quot;4317&quot;<br>    otel-max-queuesize: &quot;2048&quot;<br>    otel-schedule-delay-millis: &quot;5000&quot;<br>    otel-max-export-batch-size: &quot;512&quot;<br>    otel-service-name: &quot;nginx-proxy&quot; # Opentelemetry resource name<br>    otel-sampler: &quot;AlwaysOn&quot; # Also: AlwaysOff, TraceIdRatioBased<br>    otel-sampler-ratio: &quot;1.0&quot;<br>    otel-sampler-parent-based: &quot;false&quot;</pre><p>While the workloads are always running inside a Kubernetes namespace, which has the tenant id attached as a label, the Ingress-Nginx controller is a shared deployment operated by the platform team, and the tenant information cannot be extracted from the namespace.</p><p>We had to extract the tenant information from another location to route the traces originating in the ingress controller to the correct Grafana Tempo tenant.</p><h4>Ingress-Nginx OpenTelemetry directives</h4><p>While the Ingress-Nginx controller documentation currently falls short, we found the required information in the module which is used for the OpenTelemetry part. It is located in the <a href="http://open-telemetry/opentelemetry-cpp-contrib">following GitHub project</a>.</p><p>We were able to find the directives which are provided by the OpenTelemetry module. Especially the directive opentelemetry_attribute was very interesting for us. It allows us to add <a href="https://opentelemetry.io/docs/concepts/signals/traces/#attributes">custom attributes to spans</a>.</p><p>We configured the ingress controller to add these directives to each server block in the nginx config. The configuration example below is for the helm chart:</p><pre>controller:<br>  config:<br>    server-snippet: |<br>      opentelemetry_attribute &quot;ingress.namespace&quot; &quot;$namespace&quot;;<br>      opentelemetry_attribute &quot;ingress.service_name&quot; &quot;$service_name&quot;;<br>      opentelemetry_attribute &quot;ingress.name&quot; &quot;$ingress_name&quot;;<br>      opentelemetry_attribute &quot;ingress.upstream&quot; &quot;$proxy_upstream_name&quot;;</pre><p>After making this configuration change, traces sent from the ingress controller contain information about the Ingresses namespace, name, and where it routes traffic to.</p><p>But where to get the tenant id from? Lucky for us, all of our namespaces follow a naming schema like tenantId-&lt;namespaceName&gt;. We can extract the tenant id from the namespace attribute attached to the spans.</p><p>The collector pipeline above is changed to make this possible. Here’s what the code looks like:</p><pre>receivers:<br>  otlp:<br>    protocols:<br>      grpc:<br>processors:<br>  k8sattributes/default:<br>    extract:<br>      labels:<br>      - tag_name: tenantId<br>        key: &quot;tenant-id&quot;<br>        from: namespace<br>  # in case of ingress-nginx traces the ingress namespace is in the attributes.<br>  # we need to extract the account id from there and eventually transfer it to the resources<br>  # see Resource: https://opentelemetry.io/docs/concepts/glossary/#resource<br>  # vs Attribute: https://opentelemetry.io/docs/concepts/glossary/#attribute<br>  attributes/ingress:<br>    actions:<br>    - key: &quot;ingress.namespace&quot;<br>      pattern: &quot;(?P&lt;temp_tenant_id&gt;.{6})&quot;&lt;&lt;&lt; <br>      action: extract # this overwrites existing keys<br>  groupbyattrs: # copy the attribute containing the tenant id to the resource attribute which is used for routing<br>    keys:<br>      - temp_tenant_id<br>  resource/ingress:<br>    attributes:<br>    - key: &quot;tenantId&quot;<br>      from_attribute: temp_tenant_id<br>      action: insert<br>    - key: &quot;temp_tenant_id&quot;<br>      action: delete<br>routing:<br>  from_attribute: tenantId<br>  attribute_source: resource<br>  table:<br>  - value: tenant1<br>    exporters: [logging, otlp/tenant1]<br>  - value: tenant2<br>    exporters: [logging, otlp/tenant2]<br>exporters:<br>  otlp/tenant1:<br>    endpoint: tempo.grafana.svc.cluster.local:4317<br>    tls:<br>      insecure: true<br>    headers:<br>      X-Scope-OrgID: tenant1<br>  otlp/tenant2:<br>    endpoint: tempo.grafana.svc.cluster.local:4317<br>    headers:<br>      X-Scope-OrgID: tenant2<br>    tls:<br>      insecure: true<br>service:<br>  pipelines:<br>    traces:<br>      receivers: [otlp]<br>      processors: [k8sattributes/default, attributes/ingress, groupbyattrs, resource/ingress, routing]<br>      exporters: [ otlp/tenant1, otlp/tenant2]</pre><p>Using this pipeline, the namespace information of the Ingress-Nginx is used. The tenant id is extracted with a regular expression and stored in a temporary attribute. Then the attribute is copied to the resources<strong> </strong>using the groupbyattrs processor.</p><p>This is required because OpenTelemetry has the concept of attributes and resources. The k8sattributes processor stores the information in the resources part. We use these attributes already for routing (this is why we configured attribute_source: resource).</p><p>This is a bit ugly, but we needed help finding a better way to do this. If you have ideas feel free to put suggestions in the comments!</p><h3>Next Steps</h3><p>The collector pipeline above can be further optimised by using the following:</p><ul><li>the <a href="https://github.com/open-telemetry/opentelemetry-collector/tree/main/processor/batchprocessor">batch</a> processor for sending traces batched</li><li>the <a href="https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor">tail-sampling</a> processor to filter out traces for endpoints like /actuator or /healthwhich are called very frequently by the Kubernetes API server but bring no real value to be recorded in Grafana Tempo</li></ul><p>Grafana Tempo and the OpenTelemetry collector usage are very new for us now. If any important points are missing above and you see further optimisations leave a hint in the comments.</p><p>Hopefully, the instructions help you as well on your Observability journey!</p><h3>Further Read</h3><ul><li><a href="https://opentelemetry.io/">OpenTelemetry.io</a></li><li><a href="https://medium.com/@magstherdev/opentelemetry-up-and-running-b4c58eaf8c05">OpenTelemetry: Up and Running</a></li><li><a href="https://grafana.com/oss/tempo/">Grafana Tempo</a></li><li><a href="https://kubernetes.github.io/ingress-nginx/user-guide/third-party-addons/opentelemetry/">Ingress-Nginx OpenTelemetry</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e98d482c733" width="1" height="1" alt=""><hr><p><a href="https://medium.com/better-programming/opentelemetry-sending-traces-from-ingress-nginx-to-multi-tenant-grafana-tempo-e98d482c733">OpenTelemetry: Sending Traces from Ingress-Nginx to multi-tenant Grafana Tempo</a> was originally published in <a href="https://betterprogramming.pub/">Better Programming</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Pitfalls autoscaling self-hosted Azure DevOps build agents in Kubernetes]]></title>
            <link>https://faun.pub/pitfalls-autoscaling-self-hosted-azure-devops-build-agents-in-kubernetes-f87b10325551?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/f87b10325551</guid>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[platform-engineering]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[azure-devops]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Mon, 13 Feb 2023 20:46:00 GMT</pubDate>
            <atom:updated>2023-02-13T20:46:00.102Z</atom:updated>
            <content:encoded><![CDATA[<p>Our internal developer platform not only gives application development teams the possibility to run their containerised workloads, but also provides them build agents without worrying about the underlying infrastructure. We use Azure DevOps as a version control system and use YAML to define <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/?view=azure-pipelines">Azure Pipelines</a>.</p><p>We cannot use the Microsoft hosted agents, because all of our resources are running in private virtual networks and are not accessible from the hosted agent network. This means we use <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/agents?view=azure-devops&amp;tabs=browser#install">self-hosted agents</a>, which are running in a Kubernetes cluster.</p><p>They are automatically scaled up and down depending on how many build jobs are currently queued. For autoscaling we use the KEDA <a href="https://keda.sh/docs/2.9/scalers/azure-pipelines/"><em>azure-pipelines scaler</em></a><em>.</em></p><p>Using the build agents got more and more popular among the development teams the last months. This led to the appearance of some problems and the discovery of new requirements.<strong> </strong>We needed to make adjustments, especially as a reaction to teams with more monolithic architectures.</p><h3>Initial Setup</h3><p>The build agents are running as <em>Deployment</em> inside a Kubernetes cluster and are connected to a <a href="https://keda.sh/docs/2.9/concepts/scaling-deployments/">ScaledObject</a> provided by KEDA.</p><p>The build agent <em>replica count</em> is increased and decreased by KEDA, depending on the amount of build jobs queued. This only works if enough resources are available inside the Kubernetes cluster. (=&gt; This means a Kubernetes Node is available on which a new build agent can be started.)</p><p>We get the best resource utilisation when the cluster reacts to changes of resource requests. For that we use the <a href="https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler">cluster autoscaler</a>.</p><p>The cluster autoscaler observes the resource requests and scales up new Kubernetes Nodes, if more resources are requested than available. On the other side it scales down Nodes if there are too many unused resources in the cluster.</p><p>In the beginning, we scheduled the build agents with resource requirements as described below:</p><pre>...<br>resources:<br>  requests:<br>    cpu: &quot;1&quot;<br>    memory: 1024Mi<br>  limits:<br>    # cpu:  no CPU limit!<br>    memory: 4096Mi<br>...</pre><p>This setup worked fine for a long time, because we had many short running jobs, which only consumed resources for a short time. After onboarding larger applications on the platform we observed the first problems.</p><p>Proceeding in the description of these problems I assume you are familiar with <a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/">Kubernetes Resource Management for Pods and Containers</a>.</p><h4>Out-of-memory killed build agents</h4><p>Our alerting showed Kubernetes Pods getting <em>OOM killed</em> (<em>Exit Code 137</em>). The reason was easy to detect: Compiling a large Java project and executing many JUnit tests just consumed more than 4GB of RAM, which was defined as memory limit. The Kubernetes scheduler reacts with restarting the Pod. Each time this happens, the build job will show as failed in Azure DevOps and must be restarted. This is a poor Developer Experience.</p><p>The initial reflex was to <strong>increase</strong> the build agents <strong>memory limit</strong> to 8 GB RAM. But while fixing the issue above this surprisingly led to a set of new problems.</p><p><strong>Too low memory requests</strong></p><p>Some build pipelines are spawning parallel build jobs with Java compiling, Unit- and Integration-Testing and more. Since we had a very low memory request of 1 GB the Kubernetes scheduler placed multiple build agents running these jobs on one Node.</p><p>Let’s assume the Kubernetes Node has a VM Type with usable resources of 6 vCPUs and 16 GB RAM. In theory more than four build agents can be placed on the Node, if we take only the requests into consideration. And that is exactly what happened.</p><p>The outcome where two bad situations:</p><ol><li>The builds jobs saturated the Nodes complete CPU resources while compiling Java code. Things got very slow and build jobs timed out because of CPU starvation.</li><li>If the build jobs used a lot of memory (remember the new memory limit of 8 GB) they used the whole RAM the Node has. This resulted in swapping and the Kubernetes scheduler started evicting Pods due to a <em>Memory pressure </em>event. (See the docs for <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#node-out-of-memory-behavior">Node out of memory behaviour</a>)</li></ol><p>We got a lot of alerts from our monitoring and thought about possible solutions. The obvious fix was <strong>increasing</strong> the CPU and memory <strong>requests</strong> for the build agents. Now the Kubernetes scheduler placed less agents on one Node. Additionally, we configured (soft)anti-affinitieson the build agents Pod to get them scheduled on different Nodes if possible (See K8S Docs <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity">Assigning Pods to Nodes</a>).</p><h4>Bad resource utilisation</h4><p>What we disliked about this solution was a decreasing resource utilisation. Now less build agent Pods where scheduled on a Node. But only some of our build jobs are really requiring these extra resources added by increasing the requests and limits. (A solution will be described in the “<em>different agent types</em>” section later)</p><p>Having less jobs run on a Node made the cluster autoscaler becoming more active scaling up and down Nodes depending on queued jobs. This led to another two interesting error scenarios. (<strong>*sigh*</strong>)</p><h4>Killed build jobs</h4><p>We again started seeing messages like “<em>We stopped hearing from the build agent</em>” in the Azure DevOps build logs. The builds seemed to just stop with no special error message in the logs.</p><p>This gave us some headaches. First we expected some network problems, but found no proof for that.</p><p>The cancelled build jobs did not consume excessive resources and the Node showed no special events as well. The build jobs with errors where randomly distributed across all customers. It seemed that longer running jobs are more often killed. This was a very interesting finding.</p><p>Our build agents pick up jobs which can run for a long time (e.g. over 40 minutes). When <a href="https://keda.sh/">KEDA</a> decides to scale down a build agent the Pod receives a SIGTERM event and is put into Terminating state.</p><p>The Kubernetes scheduler waits for the time specified in <a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-termination">terminationGracePeriodSeconds</a> (default: 30 seconds) and if the Pod is not stopped after this time it receives a SIGKILL event and is stopped immediately. If a build job is still running it won&#39;t finish and we see an error message like &quot;<em>we have stopped hearing from the build agent</em>&quot;.</p><p>Our solution was to increase the terminationGracePeriodSeconds to 50 minutes. This is the longest time we guarantee the application teams a build job can run, even if in 99% of the cases longer builds will work.</p><p><strong>Cluster autoscaler and </strong><strong>max-graceful-termination-sec</strong></p><p>Even after setting the terminationGracePeriodSeconds for Pods to a higher value we still experienced problems with killed build agents. This is related to the Kubernetes <a href="https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler">cluster autoscaler.</a></p><p>The cluster autoscaler checks the resource consumption of workloads and if Nodes have a low resource usage they are stopped and removed.</p><p>The default scale down threshold is 50% resource usage. That means if the Node has memory and CPU requests lower than 50% of its capacity it is a scale down target. Scale down will not happen immediately but after a scale-down-unneeded interval (default: <em>10 minutes</em>). If the resource usage does not increase during that period the Node will be scaled down. Scale down happens in multiple steps:</p><ul><li>add a taint NoSchedule to the Node, no new workloads are placed on the Node</li><li>send a SIGTERM event to all Pods to terminate them (they are rescheduled on other Nodes if required)</li><li>wait until all Pods are evicted or max-graceful-termination-sec is reached and they are killed</li><li>delete the Nodes virtual machine</li></ul><p><strong>What does that mean for our build jobs?</strong></p><p>When there is only one build agent left running on a Node, which usually has 6 CPU cores, the Node will get evicted. Since we have configured terminationGracePeriodSeconds to 50 minutes the build job may continue to run up to this time.</p><p><strong>BUT</strong> the Node does not wait that long because max-graceful-termination-sec is only set to 5 minutes (<em>default</em>). This results in the Pod getting killed with a SIGKILL event anyway after that time.</p><p>Our solution was to increase the max-graceful-termination-sec for the cluster autoscaler to match the build agents terminationGracePeriodSeconds (= 50 minutes). This should be the worst case and most of the build jobs will finish earlier.</p><p>Things started looking good after all these changes.</p><h3>Different build agent types in Azure DevOps</h3><p>The last thing that needed improvement is the resource usage of the build agents. Until that point every build job had the same resource requests and limits. We as a platform team cannot decide which build job could cope with less resources and which job requires more. We needed a way to give the developers an option to chose the right build agent sizing for their jobs.</p><p>The plan was to have different build agent Deployments with different limits (e.g. <em>4 GB</em> memory and <em>8 GB</em> memory). Next the developers had to choose which agent should execute their build job.</p><p>Azure DevOps has two different mechanism to select an agent for a build job:</p><ol><li>Build <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/pools-queues?view=azure-devops&amp;tabs=yaml%2Cbrowser">Agent pools</a>: Can be specified in the pipeline YAML. Agent pools must be added to an Azure DevOps project prior usage and pipelines need permission to run on a certain agent pool. (Permission can be granted for all pipelines in a project at once)</li><li><a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/demands?view=azure-devops&amp;tabs=yaml">Demands + Capabilities</a>: A self-hosted build agent has <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/agents?view=azure-devops&amp;tabs=browser#capabilities">capabilities</a>. These are either auto-detected for some tools if a certain binary (e.g. a Javac executable) is found in <em>PATH o</em>r custom capabilities can be added by setting environment variables (we use this later). A pipeline can then specify <em>demands</em> to formulate requirements a build agent must have to be able to run a job.</li></ol><p>Demands and capabilities were the feature we wanted to use because we are providing the build agents for the whole Azure DevOps Organisation and do not want to create a new agent pool and grant permission to all projects each time we add an agent type.</p><p>KEDA supports scaling with demands since <em>Version 2.8</em> (introduced with <a href="https://github.com/kedacore/keda/pull/2795">#2795</a>). We specified different triggers for the different agent types we have:</p><pre>apiVersion: keda.sh/v1alpha1<br>kind: ScaledObject<br>metadata:<br>  name: scaledobj-agent4gb<br>spec:<br>  scaleTargetRef:<br>    name: azagent-agent4gb<br>  pollingInterval: 30<br>  maxReplicaCount: 10<br>  minReplicaCount: 0<br>  triggers:<br>    - type: azure-pipelines<br>      metadata:<br>        targetPipelinesQueueLength: &quot;0&quot;<br>        poolName: &quot;SELF-HOSTED-AGENTS&quot;<br>        organizationURLFromEnv: &quot;https://&lt;ADO-ORG-URL&gt;&quot;<br>        demands: &quot;limit4GB&quot; <br>      authenticationRef:<br>        name: pipeline-trigger-auth<br>---<br>apiVersion: keda.sh/v1alpha1<br>kind: ScaledObject<br>metadata:<br>  name: scaledobj-agent8gb<br>spec:<br>  scaleTargetRef:<br>    name: azagent-agent8gb<br>  pollingInterval: 30<br>  maxReplicaCount: 10<br>  minReplicaCount: 0<br>  triggers:<br>    - type: azure-pipelines<br>      metadata:<br>        targetPipelinesQueueLength: &quot;0&quot;<br>        poolName: &quot;SELF-HOSTED-AGENTS&quot;<br>        organizationURLFromEnv: &quot;https://&lt;ADO-ORG-URL&gt;&quot;<br>        demands: &quot;limit8GB&quot; <br>      authenticationRef:<br>        name: pipeline-trigger-auth<br></pre><p>The ScaledObjects all look the same apart from the demands part. They are all in the same Azure DevOps Agent Pool.</p><p>Important is that the Deployment to which scaleTargetRef points really has the capability specified as demand in the trigger. Otherwise the scaled up build agents will not be able to execute the jobs. (See the <a href="https://keda.sh/docs/2.9/scalers/azure-pipelines/#supporting-demands-in-agents">docs</a>)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/501/1*KxgR2aCROeCc0MGx4ZdMog.png" /><figcaption>Scaling build agents with KEDA</figcaption></figure><p>During testing this configuration looked good. We have a Helm chart for the Azure DevOps build agents and by adding a new entry to a list we can add/remove/reconfigure the available build agent types.</p><p>A customer then can select a certain agent type by adding this to a pipeline:</p><pre>pool:<br>  name: &#39;SELF-HOSTED-AGENTS&#39;<br>  demands: &quot;limit8GB&quot;</pre><p>We are still thinking about the capability naming in order to be able to change things later. Maybe T-Shirt sizes would work better than specifying an exact number like ‘<em>limit8GB</em>’ for memory limits.</p><h3>Too many agents scaled</h3><p>After rolling out these changes we noticed an overall increase in build agent Pods running.</p><p>Since specifying a demand is optional and especially in the beginning most of our customers did not configure any demands a shortcoming in the current KEDA <em>azure-pipelines</em> scaler became clear:</p><p>It was possible to state, that a job only runs on agent with certain capabilities, <strong>BUT</strong> the trigger definition did not allow to configure that only jobs with certain demands should scale the definition.</p><p>Sounds complicated, but just think of the default case of no demands specified. This default will match all of our trigger definitions. If we have five different build agent triggers defined, each build job without demand is spawning five new Pods to run the build job. Of course only one will execute the build jobs and the others will be scaled down eventually, but this behaviour will lead to a lot of Nodes being scaled up and down.</p><p>We added a new configuration attribute called <a href="https://github.com/kedacore/keda/issues/4138">requireAllDemands</a> to solve this issue. This was implemented in <a href="https://github.com/kedacore/keda/pull/4203">#4203</a> and will be rolled out in the next KEDA release (2.10).</p><p>With this configuration flag we are able to configure one trigger as default one (<a href="https://github.com/kedacore/keda/issues/4138">requireAllDemands</a>: false) and all others will only be scaled if their demand is really requested by a build agent (<a href="https://github.com/kedacore/keda/issues/4138">requireAllDemands</a>: true).</p><h3>Outlook</h3><p>Providing build agents sounded very simple in the beginning, but what this article hopefully made clear is that there are quite a few things to consider.</p><p>Having been running GitLab and Jenkins before, sometimes Azure DevOps felt a bit cumbersome to use and lacking features. But at the moment we are quite happy with our setup. We are running several thousand jobs per day on our build agents and rarely see any severe errors.</p><p>One area which requires our attention next is providing teams the opportunity to run containers in their build jobs. To do this they need a “<em>docker-like-thing” </em>without handing out root access or mounting a shared daemon. How we are solving this is probably worth an article in the future.</p><p>What are your experiences with Azure DevOps and self-hosted build agents? Any particular problems we still have not covered yet? Feel free to add them in the comments.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*f97d9JzKyDdTtruJ.png" /></figure><h4>👋 If you find this helpful, please click the clap 👏 button below a few times to show your support for the author 👇</h4><h4>🚀<a href="http://from.faun.to/r/8zxxd">Join FAUN Developer Community &amp; Get Similar Stories in your Inbox Each Week</a></h4><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f87b10325551" width="1" height="1" alt=""><hr><p><a href="https://faun.pub/pitfalls-autoscaling-self-hosted-azure-devops-build-agents-in-kubernetes-f87b10325551">Pitfalls autoscaling self-hosted Azure DevOps build agents in Kubernetes</a> was originally published in <a href="https://faun.pub/">FAUN.dev() 🐾</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Difference in security challenges while moving from traditional deployments to GitOps]]></title>
            <link>https://medium.com/@pat-st/difference-in-security-challenges-while-moving-from-traditional-deployments-to-gitops-a2b7deac64a4?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/a2b7deac64a4</guid>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[continuous-integration]]></category>
            <category><![CDATA[it-security]]></category>
            <category><![CDATA[development]]></category>
            <category><![CDATA[gitops]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Wed, 25 Jan 2023 14:32:32 GMT</pubDate>
            <atom:updated>2023-01-25T14:32:32.284Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/685/1*4kMY2JYJ0rjObLxOUGm3tA.png" /></figure><p>In the past years we moved from traditional operations to continuous deployment to have <strong>better automation</strong> and reduce the risk of errors. During that change applications where no longer deployed manually by people in the operation department, but automated by pipelines run by a CI server like GitLab, Jenkins or whatever the teams where using at the moment.</p><h3>Traditional attack surfaces</h3><p>In the traditional push based deployment world one of the main attack surface has been <strong>losing credentials</strong> for administrative accounts. This could be some server admin password for a virtual machine or application server. Getting those credentials would allow a potential attacker to change the machine configuration and deploy stuff which should better be not running in your environment.</p><p>In the traditional operation department based deployment model this risk was often enhanced by missing transparency about ‘who is using what password?’, which leads to situation where you pretty much never can rotate a password without the fear of breaking something. This was somehow solved by introducing password managers or pulling credentials from a central secret store.</p><p>Using continuous deployment with a tool like GitLab, Jenkins, Azure DevOps pipelines or Github actions was the automation of the manual repetitive deployment tasks. Using this it was possible to increase the amount of deployments by a huge factor and decreasing the time a feature needs to production drastically.</p><p>By using it suddenly the build server plays a very central role in your deployment process and needs to have your deployment credentials. Until then it was a tool to produce some kind of application binary and store it in a place where the operator can fetch and deploy it. Getting rid of this manual work (which is always a good idea!) suddenly a tool historically only used by developers needs be secured much better. In combination with build servers like Jenkins which tend to have a lot of plugins installed, it brings a lot of new challenges.</p><h3>From imperative to declarative deployments</h3><p>What both techniques had in common was the push principle: Whenever something new should be deployed somebody (operator or CI server) was running some commands to deploy the application to a virtual machine, application server or some other deployment target.</p><p>This process was imperative: The deployment manual or pipeline contains a sequential list of commands to be run for a deployment. In cases of errors it might be hard to continue at a certain point because partly successful deployments can be hard to be rolled back or existing resources cause problems.</p><p>Then the raise of declarative infrastructure came: Tools like <a href="https://www.terraform.io/">terraform</a> or cloud specific tools like <a href="https://aws.amazon.com/cloudformation/">CloudFormation</a> (AWS) or <a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/overview">ARM</a> (Azure) templates allowed teams to describe their infrastructure and have a external software making sure this description gets somehow deployed and reconciled in subsequent runs.</p><p>Now not only new application versions could be deployed using automation but your whole infrastructure landscape could be deployed by running a pipeline.</p><p>The huge advantage over the traditional imperative deployment model was <strong>idempotency</strong>: It is very hard to make sure your pipeline works in subsequent runs using scripting in bash. Tools like terraform on the other hand have this functions already build-in. They can rerun deployments and if the declaration of your infrastructure did not change, then following deployments will recognise there is nothing to do.</p><h3>From push to pull</h3><p>After moving to a declarative description of our environments (application version + infrastructure) that next evolution is moving from deployment pipelines which do the deployments (<strong>push</strong>) to operators which are running inside an environment and watch your Git repositories for changes (<strong>pull</strong>).</p><p>This eliminates the risk of having deployment credentials in your pipelines. The operator is already running in the target environment and can therefore be much better secured.</p><p>Using Kubernetes as operation platform we see this pattern when we use <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/operator/">operators</a> for custom resources or by using Kubernetes at all: We create a <em>Deployment</em> object in Kubernetes and somebody is observing this and creating Pods, ReplicaSets, Volumes and other resources on our behalf.</p><p>GitOps tools like <a href="https://argo-cd.readthedocs.io/en/stable/">ArgoCD</a> or <a href="https://fluxcd.io/">FluxCD</a> brought this pattern to a new level. We can watch Git repositories for changes to our helm charts, Kubernetes YAMLs or whatever we have and the changes are just getting installed in as many Kubernetes clusters as we like without any manual intervention.</p><p>This article is not about GitOps in general. Just read the awesome documentation at <a href="https://www.gitops.tech/">https://www.gitops.tech/</a> to learn more about that.</p><h3>New attack surfaces</h3><blockquote>With great power comes great responsibility. (Spiderman? ;-) )</blockquote><p>As we moved from manual deployments done by somebody, to a world where creation <strong>and destruction </strong>of our complete infrastructure is only one Git commit away, we have to make sure no bad actor is able to compromise our repositories.</p><p>Automating deployments moved the risk of failures from the people in the operation department to pipelines running on a build server. Now GitOps is shifting some risks one step further left to the Git repository. That is traditionally the place where developers are working on.</p><p>One of the main advantages of GitOps is the Git history where you can find a complete auditable trail of changes done to your repository (= your infrastructure). Enabling <a href="https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work">signing of commits</a> using GPG makes sure you know who did the change at what time.</p><p>So far so good, but there is a catch: In case somebody gets too many permissions to your repository he can alter the history using <a href="https://git-scm.com/docs/git-push#Documentation/git-push.txt--f"><em>git force push</em></a>. As Git is a decentral version control system you can restore the history from a different workstation or server. But spotting the changed may not be that easy.</p><h3>Final thoughts</h3><p>While using the GitOps principles on a daily basis it was a nice look back where we came from and what did change in only a few years. Switching from the traditional or build server based deployments to GitOps shouldn’t be anything you are afraid of. (That was <strong>NOT</strong> the goal of this article ;-) )</p><p>Nevertheless, it is always a good approach to not only see the shiny new world with all its possibilities, but take a moment to reflect what might go wrong. Working with User stories this might be the right moment to think about an <a href="https://medium.com/serious-scrum/evil-user-storys-story-telling-for-it-security-e4a9ec94193c">Evil User story</a>.</p><p>In the last chapter we learned that the Git repository is something which should be secured well. The next article will focus how to <a href="https://medium.com/@pat-st/secure-your-gitops-repository-in-azure-devops-83695f4b493a">secure a repository in Azure DevOps repositories.</a></p><p>The list of security issues was of course not exhaustive. If you see any important points missing feel free to add them in the comments.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a2b7deac64a4" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Secure your GitOps repository in Azure DevOps]]></title>
            <link>https://systemweakness.com/secure-your-gitops-repository-in-azure-devops-83695f4b493a?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/83695f4b493a</guid>
            <category><![CDATA[security]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[azure-devops]]></category>
            <category><![CDATA[gitops]]></category>
            <category><![CDATA[development]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Wed, 25 Jan 2023 14:32:32 GMT</pubDate>
            <atom:updated>2023-01-28T04:55:37.670Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/916/1*p-yk22qBQKLHG7c8NZ2cYA.png" /></figure><p>In the <a href="https://medium.com/@pat-st/difference-in-security-challenges-while-moving-from-traditional-deployments-to-gitops-a2b7deac64a4">last article</a> we discussed the security challenges moving from a traditional imperative deployment model to a declarative GitOps model. For more details check out the article at <a href="https://medium.com/@pat-st/difference-in-security-challenges-while-moving-from-traditional-deployments-to-gitops-a2b7deac64a4">https://medium.com/@pat-st/difference-in-security-challenges-while-moving-from-traditional-deployments-to-gitops-a2b7deac64a4</a> .</p><p>In the last article we discovered that our Git repository is a new threat to our security. Now we will learn how to configure <a href="https://azure.microsoft.com/en-us/products/devops/repos">Azure DevOps repositories</a> to be secure.</p><h3>Limit access to repository</h3><p>Of course many problems we describe below could be solved by just denying access to our repositories to most people. This seems like the easy solution, but personally I like <strong>transparency</strong> in the organisation because that allows developers to learn from each other and fosters collaboration between different teams.</p><p>If <em>Team A</em> has a certain problem <em>Team B</em> already solved it can find the solution just by looking at their code. If a team sees a error in the repository of another team it even can just create a pull request to make the code better.</p><p>What I propose as ideal solution is to give everybody in the organisation<strong> at least read access </strong>to your repositories and <strong>deny unauthenticated access</strong>. This will not expose your repositories publicly but enables teams learn what is already available in their Azure DevOps organisation.</p><h3>Preventing unapproved changes</h3><p>One risk factor we discovered are unapproved changes to our Git history, e.g. <em>git force pushes</em> which rewrite the history. To reduce the risk of that happening we will protect the <em>main</em> branch in our git repository. Details about that can be found in the Azure DevOps docs at <a href="https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&amp;tabs=browser">Git branch policies</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vwEBtjK8rB5slHR7BV2UGg.png" /><figcaption>Azure DevOps branch policy</figcaption></figure><p>Above screenshot shows the configuration for the main branch. Important is to <strong>NOT</strong> check the checkbox ‘<em>Allow requestors to approve their own changes</em>‘. Otherwise we would forfeit the purpose of this configuration, because one person alone would be able to merge changes.</p><p>With this minimal change we achieved:</p><ul><li>no direct changes to <em>main</em> branch allowed</li><li>apart from the pull request creator at least a <em>second</em> person is required to complete the pull request</li><li>when new changes are pushed a <strong>new</strong> approval is required</li></ul><p>There are more settings to fine tune the behaviour. You can take a special look what should happen if new changes are pushed. You must find the right balance between security requirements and developer experience. If it takes a long time to get the required approvals to get a pull request merged this will be bad for your turnaround times and will demotivate the developers.</p><p>Having pull requests for every change opens up a whole new set of possibilities:</p><ul><li>you can create additional <strong>automated checks</strong> like running <a href="https://github.com/aquasecurity/tfsec">tfsec</a> or <a href="https://runterrascan.io/">terrascan</a> for terraform files or some <strong>CVE checker</strong> for your Java application. The check results can directly be pushed as comments to your pull request and help reviewers to get a <strong>clear picture</strong> about the changes in a pull request.</li><li>you can have pull requests created by tools like <a href="https://github.com/renovatebot/renovate">Renovatebot</a> to get dependency versions updated automatically. We use this feature very extensively to update all kind of dependencies (terraform modules, CLI Tool versions, …)</li><li>discuss about documentation changes or architecture decision records with other stakeholders directly in the pull request</li><li>knowledge transfer because at least a second pair of eyes has seen each code change</li></ul><p>In my experience having pull requests is a very good tool to work together and make <strong>informed decisions</strong>. It is not always about solving every issue in the pull request, but about creating new tasks or user stories for fixing some technical debt you may introduce by purpose.</p><p>Very important is to keep your branches <strong>shortlived</strong>! Nobody wants to review changes to hundreds or thousands lines of code on the one hand and on the other hand no developer should work on branches which live for several days. Otherwise merge conflicts will arise and productivity will decrease!</p><h3>Deployment repository structure</h3><p>Now taking a look at a infrastructure repository the layout may look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/370/1*VuQ3IcwMcbQj_miebAzNog.png" /><figcaption>GitOps deployment repository</figcaption></figure><p>We have different <strong>folders for different environments</strong>. There are different strategies to describe environments, e.g. using <strong>Git branches</strong> or <strong>dedicated repositories</strong> for different environments. In the last years I tried out each and every of these strategies and (in my opinion) the only working option is using directories as representation of environments.</p><p>Now we want to give the developers more freedom to make changes to <em>DEV</em> and <em>UAT</em> environments and have <strong>stricter security </strong>rules for the <em>PROD</em> environment.</p><p><strong>The problem:</strong> Git does not know about permissions in general and for folders in particular.</p><p>The solution to this challenge is different for every platform, but usually you can configure different conditions for different path changes. In Azure DevOps we can solve this using <a href="https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&amp;tabs=browser#automatically-include-code-reviewers">reviewer policies</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/952/1*nNKuiBFrkAKHeXOUl3Q9fQ.png" /><figcaption>Azure DevOps reviewer policy configuration</figcaption></figure><p>We can define different reviewer policies for our different stages. This way we can enforce approval from different groups per stage. We use this feature to enforce approval for production environments from application owners while <em>DEV</em> and <em>UAT</em> approvals can be given be the developers themselves.</p><p>The good thing in Azure DevOps: The approval groups can be <strong>backed up by Azure Active Directory</strong> groups. That way we can use the standardised company legitimation processes to gain access for deployments to different environments.</p><h3>Build validation and status checks</h3><p>Now we have secured who is allowed to make changes to our repositories. The next step is to increase the confidence of merging changes. Sometimes just seeing some deployment YAML files or Terraform HCL files does not really helps to find out what this merged pull request might to in the end.</p><p>The solution is to make<strong> dry-runs</strong> for your changes. How that exactly looks like is depending on your deployment tools:</p><ul><li><strong>Terraform</strong>: Just run a <em>terraform plan</em> for the changes in your pull request. Terraform will come up with a diff between your current state and the state described in your branch. (<strong>Hint</strong>: Don’t bother to use the Microsoft Terraform extension for Azure DevOps. It is not maintained. Instead use the excellent Extension by Charles Zip called <a href="https://marketplace.visualstudio.com/items?itemName=charleszipp.azure-pipelines-tasks-terraform">Azure Pipelines terraform tasks</a>). You can have a pipeline to create the terraform plan and allow the merge of the pull request only if the pipeline succeeds. In addition to that the created terraform plan can be viewed to spot accidental changes. We created a custom pipeline step which creates a short summary of the plan and pushes it as comment to our pull request.</li><li><strong>Helm Charts</strong>: A good start is to display the diff between the <strong>values.yaml</strong> files you may have. As you will learn at some point this is only one part of the story, especially while updating helm chart versions of third party charts. In addition to that you need to run a <a href="https://helm.sh/docs/helm/helm_template/">helm template</a> command on the main branch and your feature branch to create a meaningful diff. This allows you to preview the changes which will really be happening on a Kubernetes cluster!</li></ul><p>In addition to that you should run security testing tools like described above, e.g. <em>tfsec</em> or <em>terrascan</em> for Terraform.</p><h3>Know your committer</h3><p>Git has the nice feature to allow you to chose any email address in your commit message. So in the end you cannot be really sure that <em>&lt;Mike.Anybody@somemail.yxz&gt;</em> is really who he claims to be. This is really bad if you want to keep an auditable trail of changes to your repositories.</p><p>Fortunately there is a (more or less) easy solution to that: <a href="https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification">Signing of commits</a>. Each committer has a SSH or GPG keys and can sign his commits with this key. Knowing the public signatures of these keys allows to verify that they are coming from known and trusted sources.</p><p>Unfortunately at this point Azure DevOps is not able to enforce the commit signing. There is very old <a href="https://developercommunity.visualstudio.com/t/show-whether-commits-are-verified-with-gpg-key-on/383281">User Voice Request</a> for this with disappointing feedback until now.</p><h3>Final thoughts</h3><p>Azure DevOps does offer a lot more configuration properties to control who is allowed to do what on a repository. In my experience the above described configurations will bring you a lot of security without sacrificing to much usability. The status checks and build validations even add a lot of confidence while doing changes to your applications.</p><p>Being an application developer a lot of this does not look very new and we applied great parts of it already in our development processes in the last decade. That is what is most interesting about it:</p><blockquote>Operations is just another style of development nowadays.</blockquote><p>While maturing as a team you will find new status to add to your pipeline or even retire some if they bring no use. What is important to always give transparence what is happening at the moment and what is required to get something merged. Make sure your pipelines do not get too big, this might lead to unmanageable script monsters. ;-)</p><p>If you are missing something very important feel free to add a hint in the comments.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=83695f4b493a" width="1" height="1" alt=""><hr><p><a href="https://systemweakness.com/secure-your-gitops-repository-in-azure-devops-83695f4b493a">Secure your GitOps repository in Azure DevOps</a> was originally published in <a href="https://systemweakness.com/">System Weakness</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Azure KeyVault Password creation using terraform]]></title>
            <link>https://medium.com/@pat-st/azure-keyvault-password-creation-using-terraform-d3270cbed371?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/d3270cbed371</guid>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[terraform]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[coding]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Mon, 09 Jan 2023 08:19:48 GMT</pubDate>
            <atom:updated>2023-05-03T12:28:14.942Z</atom:updated>
            <cc:license>https://creativecommons.org/licenses/by-nc/4.0/</cc:license>
            <content:encoded><![CDATA[<h3>Azure KeyVault Password creation using Terraform</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*b0oZ-Da1LxW4TdujAOiC9Q.png" /><figcaption>Azure KeyVault</figcaption></figure><p>Create KeyVault secrets without storing them in the Terraform state to allow an automated external rotation later.</p><p>We are heavily users of Terraform and most of our infrastructure is deployed in Azure.</p><p>Naturally we have places where we need to create and store secrets in an Azure KeyVault. Unfortunately the Azure KeyVault offers no option to create and auto-rotate secrets.</p><p>In addition to just creating a secret we want a rotation process where we can change a password (e.g. for a Postgres database) without any downtime in the consuming application.</p><p>For Postgres this can be done using different roles which inherit their permissions from a group role. There are some good descriptions like <a href="https://thnee.se/postgresql-rotate-password/">this one</a> with more details.</p><p><em>Baseline</em>: We need multiple secrets which are rotated at different times. The secrets values must be usable in Terraform.</p><h3># Naive solution</h3><p>What we did first was using the Terraform <a href="https://registry.terraform.io/providers/hashicorp/random/latest/docs">random provider</a> to generate a password and then use the <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_secret">azurerm_keyvault_secret</a> resource to store the password in a KeyVault:</p><pre>data &quot;azurerm_key_vault&quot; &quot;example&quot; {<br>  name                = &quot;mykeyvault&quot;<br>  resource_group_name = &quot;some-resource-group&quot;<br>}<br><br>resource &quot;random_password&quot; &quot;admin-pw&quot; {<br>  length           = 32<br>  special          = true<br>}<br><br>resource &quot;azurerm_key_vault_secret&quot; &quot;admin-pw&quot; {<br>  name         = &quot;admin-pw&quot;<br>  key_vault_id = azurerm_key_vault.example.id<br>  value        = random_password.admin-pw.result<br>}</pre><p>This works quite well for initial creation of secrets. But there are some downsides to this approach:</p><p>First the generated password is stored in <strong>plain text </strong>in your Terraform state (you better don’t lose you state files anyway!).</p><p>Specifying an expiration can be difficult because you need a datasource with a date and time which does not change on every <em>terraform plan </em>execution. Otherwise you will see constant changes in you Terraform pipeline. You can try to achieve that to a certain extent using the <a href="https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/rotating">time_rotating</a> resource provided by Hashicorps <a href="https://registry.terraform.io/providers/hashicorp/time/latest/docs">Time Provider</a>.</p><p>Implementing something like having two passwords where one is active and the second can be rotated if it is not used anymore will give you a real headache.</p><p>This ‘<em>rolling password update</em>‘ is not something you want to solve in Terraform. You will end up with a lot of dependent time resources, conditional blocks where you are trying to sort out which change should trigger which resource updates and so on.</p><p>We managed to come quite far trying but as we have several Terraform modules with dependencies to each other we often ended up having some datasource or condition which can only be evaluated deferred during the apply phase. This leads to a lot of changes in Terraform plans.</p><h3># External password rotation</h3><p>We ended up using a password rotation mechanism external to Terraform. Azure even offers some help in their ‘<a href="https://learn.microsoft.com/en-us/azure/key-vault/secrets/tutorial-rotation-dual?tabs=azure-cli#create-and-deploy-the-key-rotation-function"><em>Automate the rotation of a secret for resources that have two sets of authentication credentials</em></a><em>’</em> article.</p><p>This can only work if the password itself is not managed with a Terraform <strong><em>resource</em></strong> like the <em>azurerm_keyvault_secret</em>. The reason for that is that an external password rotation would be overwritten by the next <em>terraform apply</em> with the old value because Terraform sees the configuration drift and applies what it thinks is the desired configuration.</p><p>Instead we use the <a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault_secret"><em>azurerm_keyvault_secret</em></a><em> </em><strong><em>datasource</em></strong> for retrieving a secret from KeyVault. For that to work the secret <strong>must</strong> exist when the <strong><em>datasource</em></strong> is refreshed. If not the Terraform build will fail due to a ‘<em>KeyVault Secret not found’</em> error.</p><p>There are several ways to create the secret.</p><p>One option is to create the secret in KeyVault manually prior running Terraform. This would be very bad from an automation perspective because secret creation (and deletion) should be completely automated.</p><p>We spend quite some time on this issue and came up with a different solution. We are having a <em>null_resource</em> with a<em> local-exec</em> provisioner which is run once and makes sure the secret is created in KeyVault:</p><pre>resource &quot;null_resource&quot; &quot;create-password&quot; {<br>  provisioner &quot;local-exec&quot; {<br>    when    = create<br>    command = &lt;&lt;-EOF<br>  set -e  <br>  password=$(openssl rand -base64 32)<br>  <br>  # output none to NOT display the passwords in logs<br>  az keyvault secret set --vault-name &quot;$KEYVAULT_NAME&quot; --name &quot;${SECRET_NAME}&quot; --value &quot;$password&quot; --output none<br><br>EOF<br>    environment = {<br>      SECRET_NAME            = var.secret_name<br>      KEYVAULT_NAME          = var.key_vault.name<br>    }<br>  }<br>}<br><br>data &quot;azurerm_key_vault_secret&quot; &quot;password&quot; {<br>  depends_on   = [null_resource.create-password]<br>  key_vault_id = var.key_vault.id<br>  name         = var.secret_name<br>}</pre><p>Then we use the normal <em>azurerm_keyvault_secret</em> datasource to use the secret. The datasource has a <em>depends_on</em> attribute to make sure the secret is created first!</p><p>During the first Terraform run you will see that the datasource is evaluated deferred because of the <em>depends_on</em> block. Following runs will evaluate the datasource during plan phase because the <em>null_resource</em> is already existing and not changed.</p><p>If the secrets value is changed in KeyVault the next Terraform execution will pick up the new value and following modules can change their configs accordingly. We have a small shell script which runs on a schedule and updates the secrets in KeyVault and triggers the Terraform pipeline afterwards.</p><p>To make sure the secret gets removed from KeyVault when it is no longer needed we use a destroy provisioner:</p><pre>resource &quot;null_resource&quot; &quot;delete-password&quot; {<br>  #because Destroy-time provisioners and their connection configurations may only reference attributes of the related resource, via &#39;self&#39;, &#39;count.index&#39;, or &#39;each.key&#39;.<br>  triggers = {<br>    name           = var.secret_name<br>    key_vault_name = var.key_vault.name<br>  }<br>  provisioner &quot;local-exec&quot; {<br>    when    = destroy<br>    command = &lt;&lt;EOF<br>  set -e<br>  if [ -z &quot;$SECRET_NAME&quot; ]; then<br>    echo &quot;Secret name to delete is not set(\$SECRET_NAME).&quot;<br>    exit 1<br>  fi<br>  if [ -z &quot;$KEYVAULT_NAME&quot; ]; then<br>    echo &quot;KeyVaultNAME is not set(\$KEYVAULT_NAME).)&quot;<br>    exit 1<br>  fi<br>  echo &quot;Deleting secret ${SECRET_NAME} from vault $KEYVAULT_NAME&quot;<br>  az keyvault secret delete --only-show-errors --vault-name &quot;$KEYVAULT_NAME&quot; --name &quot;${SECRET_NAME}&quot; || echo &quot;Could not delete secret ${SECRET_NAME}&quot;<br>EOF<br>  }<br>}</pre><p><strong>Hint</strong>: For this to work you must have the <a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli">Azure CLI</a> installed and be logged in with a user with KeyVault secret <em>set</em> and <em>delete</em> permission!</p><p>If you are using a KeyVault with soft delete enabled you should check in the create script if there is a deleted secret with the same name as you are trying to create. This can happen if a secret with that name was destroyed by Terraform and is now about to be recreated. If so you must recover the secret first before you can set a new value. For that add this at the beginning of the create secret script:</p><pre>echo &quot;Checking if the secret exists in DELETED state and recover them, otherwise we cannot use the name again&quot;<br>deleted_secret_exists=$(az keyvault secret list-deleted --vault-name &quot;$KEYVAULT_NAME&quot; --query &quot;contains([].id, &#39;https://$KEYVAULT_NAME.vault.azure.net/secrets/$SECRET_NAME&#39;)&quot; -o tsv)<br>if [ $deleted_secret_exists == true ]; then<br>  echo &quot;Deleted secret exists. Recovering them before setting new password values&quot;<br>  az keyvault secret recover --only-show-errors --output none --vault-name $KEYVAULT_NAME --name &quot;${SECRET_NAME}&quot;<br>  sleep 2 # prevent &#39;is currently being recovered and cannot be re-created; retry later.&#39;<br>fi</pre><p>In this case the Azure CLI logged in user needs the secret recover permission as well.</p><h3># Final words</h3><p>Sometimes there are constraints for what Terraform is made for. Especially date handling and more complex (dynamic) conditions can bring the HCL to its limits. Something you would solve in a programming language in a few lines can take in HCL very complex conditions an strange behaviour during execution time.</p><p>It is quite disappointing that the Azure KeyVault offers no option to generate a secret on-demand like it does for instance for certificates.</p><p>We hope that at some point in the future the KeyVault is able to auto create and rotate secrets. But until then we are quite happy with our solution.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d3270cbed371" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From microservices to “many-services” architecture]]></title>
            <link>https://medium.com/@pat-st/from-microservices-to-many-services-architecture-bc52b4a46006?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/bc52b4a46006</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[it]]></category>
            <category><![CDATA[microservices]]></category>
            <category><![CDATA[architecture]]></category>
            <category><![CDATA[comedy]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Wed, 17 Aug 2022 07:44:51 GMT</pubDate>
            <atom:updated>2022-08-17T07:44:51.080Z</atom:updated>
            <content:encoded><![CDATA[<p>During todays walk after lunch we discussed about application sizes, services and architectures. We discussed how to size your applications and services (and how to not do it as you may see in a second) and things got out of control very fast ;-). Our discovery was that teams trying to build a microservice architecture can end up with a different architectural style pretty fast.</p><h3>Discovery of a new architectural pattern</h3><p>This newly discovered architecture was too good (and funny) to be kept secret. We called our observation the “<strong>many-services</strong> <strong>architecture</strong>” it is a revolutionary(?) deviation from the microservice architecture.</p><p>The basic principle was already discovered during the rise of continuous deployment and is pretty simple:</p><blockquote>“If it hurts, do it more frequently, and bring the pain forward.” [Source: <strong>1</strong>]</blockquote><p>For service design that means we don’t deploy only a few services but as many services as possible. (“<em>The more the better!</em>”)</p><h4>Implementation</h4><p>It is important to not worry about service boundaries, domains and other stuff which just confuses your head and is changing all the time anyway but think more like: “<em>Start small, implement fast, test later!</em>”</p><p>You did everything right when there is at least a handful of services per team member. All services need to be cleanly separated in different Git repositories in order to prevent merge conflicts.</p><p>In some agile handbooks this is called <strong>two-pizza teams</strong>. (<em>Source: 2</em>) Think of your services as pizza slices. Each team member owns at least as many services as there are slices. A tasty idea and easy to follow!</p><h4>Communication and Integration</h4><p>Integration patterns are always different to explain and stuff gets usually complicated when you are no longer alone. Thats the reason not to be shy from the beginning:</p><p>The services only communicate asynchronous (best option to start: self-hosted Kafka cluster!) and each service should at least be dependent to three other services (“all good comes in three”). If the last part cannot be fulfilled think about deleting your service, it doesn’t seem to fit very good in your application landscape!</p><p>The holy grail is if every service is calling every other service without running into cycles!</p><p>(If you do: Don’t worry, just implement some <em>sleep(100)</em> command in your code. It is asynchronous, nobody will notice anyway!)</p><p>Important for the service integration is <strong>not to rest on using REST</strong>! If the payload of service calls is human readably it can’t be fast. Use at least gRPC or something more fancy in order to squeeze out every millisecond during service calls in order to make up for the 10 seconds unoptimised database query you have, because nobody sees or reads database logs.</p><h4>Polyglot languages</h4><p>To leverage the full potential of the many-services architecture different languages should be used. This will be attractive for new employees because you have the <strong>perfect programming</strong> language for every job candidate!</p><p>If in doubt if you should use a certain language the rule of thumb is:</p><p><em>If it can be containerized it will work!</em></p><h4><strong>Security</strong></h4><p>Everybody is talking about DevSecOps and security questions must not fall short in the many-services architecture. ( ̶S̶e̶c̶u̶r̶i̶t̶y̶ -f̶i̶r̶s̶t̶ /security-somewhere in-the-top-10-of-important-things)</p><p>For authentication and authorization it is important to mix protocols. (“Security by obscurity!”) Use at least some OAuth2 and SAML to complicate lateral movement and make it not to easy to use your stuff (there is a reason we are all experts, right?).</p><p>Don’t rely on a single identity provider, the more identities the better. For fault tolerance rely on basic auth with application local users for all admin stuff. This way you can administer your applications even if the identity provider is offline and passwords can be recovered from the webserver logs by taking a look at the http headers!</p><p>Don’t get lost in the details of authentication and authorization, a single OAuth2 application client is fine for alle your services! That makes JWT validation easier anyway.</p><h3>Deployment</h3><p>Continuous Deployment got a lot of attention the recent years. Together with the DevOps culture it got even more important.</p><p>Nevertheless it is important for the many-services architecture to rely on battle-proved techniques. Since you have a lot of services the Ops people need to train deploying! Use CLI tools with imperative commands.</p><p>Don’t think about GitOps and all that complexity of deployment tooling. There are just too many tools around, who can really make an informed decision???</p><p><strong>Important</strong>: Don’t spend too much time searching for CVEs and other weaknesses in your applications: <strong>What you don’t know cannot make you worry </strong>(<em>too soon</em>)<strong>! </strong>The later you find a problem the less likely is deployment will be stopped because nobody wants to miss a project milestone!</p><p>To secure your back you should distribute responsibilities between different teams in your organisation. This creates plausible deniability if the customer blames you for errors and problems.</p><h3>Outlook</h3><p>With all this great hints and techniques you should be busy for the near future.</p><p><strong>But(!)</strong> there are already the next new pattern evolving at the horizon.</p><p>Java developers keep an eye on the new “<em>one-deployment-per-class</em>” pattern and the frontend people should start not too late with the “<em>one-web component-per-button</em>” implementation.</p><p>I hope our findings enlightened you a little bit as well. Feel free to add missing architectures and patterns in the comments!</p><p><strong>Disclaimer: Don’t take this too serious ;-)</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/256/1*pI5iocHbWTn_DM5espcfPw.png" /><figcaption>Try to be the service in the middle! (source: wikipedia)</figcaption></figure><h4>Sources and References:</h4><p>[1] <a href="https://www.goodreads.com/quotes/1314241-if-it-hurts-do-it-more-frequently-and-bring-the#:~:text=Learn%20more)-,%E2%80%9CIf%20it%20hurts%2C%20do%20it%20more%20frequently%2C,and%20bring%20the%20pain%20forward.%E2%80%9D">Jez Humble, Continuous Delivery: Reliable Software Releases Through Build, Test, and Deployment Automation</a></p><p>[2] <a href="https://docs.aws.amazon.com/whitepapers/latest/introduction-devops-aws/two-pizza-teams.html">Two-Pizza teams, Introduction to DevOps in AWS</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bc52b4a46006" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[OpenCost + Azure Cost Export for out-of-cluster-costs]]></title>
            <link>https://medium.com/@pat-st/opencost-azure-cost-export-for-out-of-cluster-costs-bbf684f1d7f7?source=rss-26e3253ec3d1------2</link>
            <guid isPermaLink="false">https://medium.com/p/bbf684f1d7f7</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[azure]]></category>
            <category><![CDATA[cncf]]></category>
            <category><![CDATA[azure-kubernetes-service]]></category>
            <category><![CDATA[kubecost]]></category>
            <dc:creator><![CDATA[patst]]></dc:creator>
            <pubDate>Tue, 09 Aug 2022 18:43:47 GMT</pubDate>
            <atom:updated>2022-08-09T18:43:47.718Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>OpenCost + Azure Cost Export for out-of-cluster-costs</strong></p><p>OpenCost enables you to visualize and measure the costs produced by kubernetes workloads:<br><br><em>OpenCost is a vendor-neutral open source project for measuring and allocating infrastructure and container costs in real time. (source: OpenCost.io)</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UN0PMWTLjFkHM0vhFMV2ug.png" /><figcaption>Source: <a href="https://github.com/opencost/opencost">https://github.com/opencost/opencost</a></figcaption></figure><p>It is nice to see what costs a deployment or a persistent volume causes in the Kubernetes cluster. But apart from these compute and storage costs there might be cost outside your cluster which should be included in the cost report.<br><br>Examples for this type of costs can be PaaS instances like databases (e.g. AWS RDS or Azure PostgreSQL), container registries and other services which are not running inside your clusters.</p><p>In my use case this was from special interest because I am part of a platform team providing a product to development teams. The development team needs to know what cost they produce in order to be able to run their applications in a cost efficient manner.<br><br><em>**Sidenote**: During my tests it wasn&#39;t really clear to me what is part of the CNCF Project OpenCost and what is part of KubeCost (and furthermore what is a paid feature of KubeCost). Being a sandbox CNCF project I guess there is still some work left on the documentation side. Nevertheless it is already an awesome product.</em></p><h3>Configuring the Azure cost export</h3><p>While installing OpenCost inside an Azure Kubernetes Service (AKS) cluster I followed the documentation on the KubeCost Page for <a href="https://guide.kubecost.com/hc/en-us/articles/4407595936023-Adding-Azure-Out-of-Cluster-Cluster-Costs-into-Kubecost">Azure Cloud Integration</a>.</p><p>Having the cost export configured as documented results in this flow:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/676/1*-4xaRsvVD4ZFR5MFQr2K_A.png" /><figcaption>Azure Cost Export for OpenCost</figcaption></figure><p>On Azure subscription level there is a cost export configured which creates a CSV file on a daily schedule. The file is exported to a blob container inside an Azure storage account. The export contains the monthly costs up to the current day.</p><p>This works probably fine for most environments. But if you are working in an environment which requires to use private endpoints for all your Azure resources their is a problem:</p><p>The CSV exports upload is coming from a IP address range located in the <a href="https://docs.microsoft.com/en-us/azure/virtual-network/service-tags-overview#available-service-tags">Azure Cloud Service Tag</a> which contains nearly half the Azure-Internet ;-).</p><p>Using a private endpoint for the storage account where the costs exports should be exported to results in an error message because the blob container is not reachable. Configuring firewall rules for the storage account was no option in our case.</p><p>If the cost export cannot be delivered to us by the exporter then we have to get it ourselves. Going through the list of available Azure REST APIs we found the <a href="https://docs.microsoft.com/en-us/rest/api/cost-management/generate-cost-details-report/create-operation">Generate Cost Report API</a> . The API is generating exactly the CSV data structure the Azure Exporter does (which btw can be configured via API using the <a href="https://docs.microsoft.com/en-us/rest/api/cost-management/exports">Export API</a>). We just need to call the API on a daily basis ourselves.</p><h3>Kubernetes cron job to the rescue</h3><p>Since we already have a Kubernetes cluster the solution was quite obvious. We create a Kubernetes <a href="https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/">cron job</a> with a daily schedule and create a shell script to call the Cost Report API.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/602/1*s9Ok4jbGx-krlSQOKwi0aw.png" /></figure><p>Creating the cost report has these steps:</p><ol><li><em>Generate Cost Report</em> API is called to get the report for an Azure subscription</li><li>the APIs response has an header named location which contains the URL to check the reports creation result. We extract the header’s value</li><li>Call the <em>Get Operation Results</em> API (that is the link from above)</li><li>This time we get a JSON response containing a blobLink attribute with the URL to download the CSV file (located in some Azure managed storage account). The url contains a SAS token and can just be used to download the file.</li><li>Download the CSV file with above link</li><li>next we upload the report to our own storage account which gets crawled by the KubeCost installation (<strong>Gotcha</strong>: The path structure after uploading must match what the default Azure Export is creating. The schema is <em>&lt;exportname&gt;/&lt;time-from&gt;-&lt;time-to&gt;/&lt;exportname&gt;-&lt;random-uuid&gt;</em>.csv , e.g. <em>test/20220801–20220831/test_74ddd963–1d30–427f-90fc-f0a960aa5a80.csv</em>)</li></ol><p>To get this working we need:</p><ul><li>Azure Service Principal with required permissions to create the cost report (“Cost Management Reader” role) for the target subscription. Credentials are stored in an Secret</li><li>ConfigMap with the shell script to generate the report. This makes it easy to adapt the script to your environment</li><li>CronJob definition for starting a Job instance on a daily schedule</li></ul><p>First we define a secret which contains all the information to obtain the cost report. The Secret contains information about the service principal with the permissions to create the cost report, the subscription and tenant ids and the name of the storage account where the reports should be uploaded. This storage account must have a container named cost-analyzer.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/19fd9c3b21ad02034d88f692d1410a54/href">https://medium.com/media/19fd9c3b21ad02034d88f692d1410a54/href</a></iframe><p>Next we define a ConfigMap which contains the shell script to be executed by the CronJob. Alternatively you can build your custom OCI image and copy the script into the image. I like this variant because it allows easier editing and changing the shell script:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/1bb4aa3e02fd0a5e1dd39a6a2ee1471b/href">https://medium.com/media/1bb4aa3e02fd0a5e1dd39a6a2ee1471b/href</a></iframe><p>And last but not least the CronJob ties together the Secret and the ConfigMap. The schedule is to run a Kubernetes Job at 8 o clock (UTC) every day:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b6606587ddaadf7482939a39824b4141/href">https://medium.com/media/b6606587ddaadf7482939a39824b4141/href</a></iframe><p>To test if your setup works you can trigger Job instances for your CronJob manually with</p><pre>kubectl create job --from=cronjob/azure-cost-exporter manualrun1</pre><p>If everything is configured correctly you shouldn’t see any error in the Pod executing the Job and in the target storage account a CSV file should appear containing your subscriptions Azure cost.</p><p>Restart the OpenCost Pod to enforce a scanning of the storage account. Otherwise every 3 hours the storage account is scanned for new cost reports.</p><p>I hoped that helped you to get the export working in a similar network constrained environment like ours ;-)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bbf684f1d7f7" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>