<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Elva's Blog]]></title><description><![CDATA[Elva is a serverless-first consulting company that can help you transform or begin your AWS journey for the future]]></description><link>https://blog.elva-group.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1724051562093/a92b5b02-06ee-45ea-ab78-456027539a34.png</url><title>Elva&apos;s Blog</title><link>https://blog.elva-group.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 19 Apr 2026 15:04:12 GMT</lastBuildDate><atom:link href="https://blog.elva-group.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Centaur Chess App with AgentCore Runtime and Strands Agents]]></title><description><![CDATA[In 1998, Garry Kasparov proposed a new format: human players paired with computer assistance. He called it "Advanced Chess," though the community settled on a better name: centaur chess. The idea was ]]></description><link>https://blog.elva-group.com/building-a-centaur-chess-app-with-agentcore-runtime-and-strands-agents</link><guid isPermaLink="true">https://blog.elva-group.com/building-a-centaur-chess-app-with-agentcore-runtime-and-strands-agents</guid><category><![CDATA[AWS]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Anton Ganhammar]]></dc:creator><pubDate>Fri, 27 Feb 2026 13:25:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69a150aa201718680934447c/b25a8e57-cbf3-4450-abfa-458e054dd432.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In 1998, Garry Kasparov proposed a new format: human players paired with computer assistance. He called it "Advanced Chess," though the community settled on a better name: centaur chess. The idea was simple. Neither human intuition nor raw computation would beat the combination of both.</p>
<p>Nearly three decades later, we have a new version of this pairing. Not human plus chess engine, but human plus language model. The engine calculated perfectly but couldn't explain itself. The LLM can reason about positions but can't search move trees. That contrast gives us something concrete to build with AWS's new agent infrastructure.</p>
<p>We'll use two pieces of that infrastructure. <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html">AgentCore Runtime</a> is a managed container runtime for AI agents, part of Amazon Bedrock. We give it a Docker image, it handles scaling, networking, and session routing.</p>
<p><a href="https://github.com/strands-agents/sdk-typescript">Strands Agents</a> is an open-source TypeScript SDK for building agents with tool use. It supports Amazon Bedrock and OpenAI out of the box, and we can add other providers. We define tools as functions with Zod schemas, wire them to a model, and the SDK manages the reasoning loop. It also supports MCP integration and response streaming, though we only use the tool pattern in this project.</p>
<h2>What We Are Building</h2>
<p>A chess game where a human plays White, assisted by an AI advisor, against an AI opponent playing Black. Three agents, three different designs, one runtime platform.</p>
<p>Here's the architecture:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69a150aa201718680934447c/157cf2c8-7b92-498b-bd34-1eb74150ddbf.png" alt="" style="display:block;margin:0 auto" />

<p>The Next.js API routes call AgentCore Runtime directly using the AWS SDK, which handles authentication automatically via the local credential chain.</p>
<p>The turn flow:</p>
<ol>
<li><p>Player drags a piece on the board, and it appears "ghosted" at the destination</p>
</li>
<li><p>The legality agent validates the move asynchronously</p>
</li>
<li><p>If illegal, the piece flashes red and reverts. If legal, the advisor evaluates it</p>
</li>
<li><p>The advisor responds with a verdict, optionally suggesting an alternative</p>
</li>
<li><p>Player confirms their move, plays the alternative, or tries something else</p>
</li>
<li><p>The opponent picks a move, validates it by calling the legality agent directly (agent-to-agent), and responds with the validated move and resulting position</p>
</li>
<li><p>Repeat</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69a150aa201718680934447c/5a0a7b55-bfda-4feb-a0ef-a92fa561e6b1.gif" alt="" style="display:block;margin:0 auto" />

<h2>AgentCore Runtime</h2>
<p>The container contract is minimal: implement <code>/ping</code> for health checks and <code>/invocations</code> for requests.</p>
<p>Here's why this matters compared to Lambda or Fargate. Lambda forces you into request-response with cold starts. Fargate gives you long-running containers but you manage the ALB, target groups, and scaling policies yourself. AgentCore Runtime provisions isolated microVMs per session, each with its own CPU, memory, and filesystem. Sessions can persist for up to 8 hours and survive multiple requests, but the infrastructure is fully managed. Deployment is a single resource definition.</p>
<p>Our server implementation is exactly the contract and nothing more:</p>
<pre><code class="language-typescript">import express from 'express';
import { advisorAgent } from './advisor.js';
import { opponentAgent } from './opponent.js';
import { legalityAgent } from './legality.js';

const app = express();
app.use(express.raw({ type: '*/*', limit: '10mb' }));

const agent = process.env.AGENT_TYPE === 'opponent'
  ? opponentAgent
  : process.env.AGENT_TYPE === 'legality'
    ? legalityAgent
    : advisorAgent;

// Serialize invocations: Strands SDK agents reject concurrent invoke() calls.
let queue: Promise&lt;void&gt; = Promise.resolve();

app.get('/ping', (_req, res) =&gt; {
  res.json({ status: 'Healthy' });
});

app.post('/invocations', (req, res) =&gt; {
  queue = queue.then(async () =&gt; {
    const raw = Buffer.isBuffer(req.body) ? req.body.toString('utf-8') : String(req.body);
    const body = JSON.parse(raw || '{}');
    const prompt = body.prompt ?? raw;
    const result = await agent.invoke(prompt);
    res.json({ output: { message: result.toString() } });
  }).catch((err) =&gt; {
    console.error('Agent invocation failed:', err);
    res.status(500).json({ error: 'Agent invocation failed' });
  });
});

app.listen(8080, () =&gt; {
  console.log(`${process.env.AGENT_TYPE || 'advisor'} agent listening on 8080`);
});
</code></pre>
<p>Two things to note here. AgentCore forwards the payload without setting a <code>Content-Type</code> header, so we use <code>express.raw()</code> and parse manually. And the Strands SDK agent holds an internal lock during <code>invoke()</code>, rejecting concurrent calls. Since AgentCore may route multiple requests to the same container, we serialize invocations through a simple promise queue.</p>
<p>One Dockerfile, one image. The <code>AGENT_TYPE</code> environment variable selects which agent to run. We push the same image to three ECR repositories and set the variable at the AgentCore Runtime level.</p>
<h2>The Advisor Agent</h2>
<p>The advisor is where things get interesting. We use the Strands Agents SDK to define an agent with a tool, a function the model can call during its reasoning loop.</p>
<p>A quick note on chess notation, since it shows up throughout the code. FEN (Forsyth-Edwards Notation) encodes an entire board position as a single string. SAN (Standard Algebraic Notation) describes a move like <code>Nf3</code> (knight to f3). LAN (Long Algebraic Notation) spells out both squares, like <code>g1f3</code>.</p>
<p>The tool checks whether a proposed move is legal and returns context about it:</p>
<pre><code class="language-typescript">import { Agent, BedrockModel, tool } from '@strands-agents/sdk';
import { Chess } from 'chess.js';
import { z } from 'zod';

const evaluateMove = tool({
  name: 'evaluate_move',
  description: 'Check if a chess move is legal and get basic context',
  inputSchema: z.object({
    fen: z.string().describe('Current board position in FEN'),
    move: z.string().describe('Proposed move in SAN or LAN'),
  }),
  callback: ({ fen, move }) =&gt; {
    const chess = new Chess(fen);
    const result = chess.move(move);
    if (!result) return JSON.stringify({ legal: false });
    return JSON.stringify({
      legal: true,
      san: result.san,
      givesCheck: chess.isCheck(),
      captured: result.captured || null,
    });
  },
});
</code></pre>
<p>We use <code>chess.js</code> for move validation. The Zod schema tells the model what parameters it can pass. This is the pattern for most agent tools: give the model access to something it can't do on its own, and let it incorporate the result into its response.</p>
<p>The agent ties the tool to a model and a system prompt. We want the advisor to suggest better moves when it spots them, so the prompt asks it to prefix every response with a <code>SUGGESTED_MOVE:</code> line:</p>
<pre><code class="language-typescript">export const advisorAgent = new Agent({
  model: new BedrockModel({ modelId: 'eu.amazon.nova-pro-v1:0' }),
  systemPrompt: `You are a chess coach advising a human player.
Use the evaluate_move tool to check the proposed move.

ALWAYS start your response with this exact line:
SUGGESTED_MOVE: &lt;move or NONE&gt;

Then write 1-2 sentences of advice.

Rules:
- If the move is good, write SUGGESTED_MOVE: NONE, then praise briefly.
- If the move is suboptimal or risky, use evaluate_move to find a better legal move,
  then write SUGGESTED_MOVE: &lt;that move in SAN&gt; (e.g. SUGGESTED_MOVE: Nf3).
  Then explain why it is better.
- The SUGGESTED_MOVE line must always be the very first line.
- Keep advice brief and conversational. Never lecture.`,
  tools: [evaluateMove],
  printer: false,
});
</code></pre>
<p>The API route strips the prefix line, extracts the move, and derives the verdict from whether an alternative exists.</p>
<p>On the frontend, the alternative drives the board UI directly. The player's proposed move appears ghosted on the board (semi-transparent highlighted squares), and they can confirm it, play the alternative, or try something else entirely.</p>
<h2>The Opponent Agent</h2>
<p>The opponent demonstrates agent-to-agent communication. Rather than having the frontend orchestrate a separate legality call after each opponent move, the opponent validates its own moves by calling the legality agent directly through AgentCore Runtime.</p>
<p>The key piece is a <code>validate_move</code> tool that invokes the legality runtime:</p>
<pre><code class="language-typescript">import {
  BedrockAgentCoreClient,
  InvokeAgentRuntimeCommand,
} from '@aws-sdk/client-bedrock-agentcore';

const agentCoreClient = new BedrockAgentCoreClient({
  region: process.env.AWS_REGION,
});

const validateMove = tool({
  name: 'validate_move',
  description:
    'Validate a chess move by calling the legality agent. ' +
    'Returns JSON with legal (boolean), san, newFen, isCheck, isCheckmate, isDraw.',
  inputSchema: z.object({
    fen: z.string().describe('Current board position in FEN'),
    move: z.string().describe('Proposed move in SAN notation'),
  }),
  callback: async ({ fen, move }) =&gt; {
    const command = new InvokeAgentRuntimeCommand({
      agentRuntimeArn: process.env.LEGALITY_ARN,
      runtimeSessionId: `opponent-legality-${Date.now()}`,
      payload: new TextEncoder().encode(
        JSON.stringify({
          prompt: `Position (FEN): \({fen}\nMove (SAN): \){move}\nValidate this move.`,
        }),
      ),
    });

    const response = await agentCoreClient.send(command);
    const text = await response.response?.transformToString();
    // ... parse and return the legality result
  },
});
</code></pre>
<p>This is a tool that calls another agent. The opponent's container has the legality agent's ARN as an environment variable, and we grant <code>bedrock-agentcore:InvokeAgentRuntime</code> permission through the IAM role. From the SDK's perspective, it is just another async tool callback. The model calls it, gets the result, and incorporates it into its reasoning.</p>
<p>The system prompt instructs the opponent to always validate before responding, and to retry if a move is illegal:</p>
<pre><code class="language-typescript">export const opponentAgent = new Agent({
  model: new BedrockModel({ modelId: 'eu.amazon.nova-pro-v1:0' }),
  systemPrompt: `You are a chess engine playing Black.

PROCEDURE:
1. Choose a move in standard algebraic notation.
2. ALWAYS call validate_move with the FEN and your chosen move.
3. If the move is illegal, choose a different move and validate again.
4. Repeat until you find a legal move.

Once validated, respond with exactly three lines:
MOVE: &lt;san&gt;
FEN: &lt;newFen from the validation result&gt;
STATUS: &lt;check|checkmate|draw|normal&gt;`,
  tools: [validateMove],
  printer: false,
});
</code></pre>
<p>Compare this with the advisor. The advisor has a local tool (chess.js) that validates moves within the same process. The opponent has a remote tool that calls another agent over the network. The SDK handles both cases identically. We define the tool, the model decides when to call it, and the framework manages the loop. The only difference is that the remote tool is async and adds network latency.</p>
<p>This pattern, one agent calling another through AgentCore, is how we compose agents in this infrastructure. Each agent has a single responsibility, and composition happens through tool calls rather than orchestration code.</p>
<h2>The Legality Agent</h2>
<p>The third agent handles move validation. The frontend has no chess library. Every move, whether from the player or the opponent, goes through this agent before it touches the board state.</p>
<pre><code class="language-typescript">const checkMove = tool({
  name: 'check_move',
  description: 'Check if a chess move is legal given a FEN position using from/to squares',
  inputSchema: z.object({
    fen: z.string(),
    from: z.string(),
    to: z.string(),
    promotion: z.string().optional(),
  }),
  callback: ({ fen, from, to, promotion }) =&gt; {
    const chess = new Chess(fen);
    try {
      const result = chess.move({ from, to, promotion });
      if (!result) return JSON.stringify({ legal: false });
      return JSON.stringify({
        legal: true, san: result.san,
        isCheck: chess.isCheck(),
        isCheckmate: chess.isCheckmate(),
        isDraw: chess.isDraw(),
        captured: result.captured || null,
        newFen: chess.fen(),
      });
    } catch { return JSON.stringify({ legal: false }); }
  },
});

const checkSanMove = tool({
  name: 'check_san_move',
  description: 'Check if a chess move in SAN notation (e.g. Nf3, Bc4, e4) is legal',
  inputSchema: z.object({
    fen: z.string(),
    san: z.string(),
  }),
  callback: ({ fen, san }) =&gt; {
    const chess = new Chess(fen);
    try {
      const result = chess.move(san);
      if (!result) return JSON.stringify({ legal: false });
      return JSON.stringify({
        legal: true, san: result.san,
        isCheck: chess.isCheck(),
        isCheckmate: chess.isCheckmate(),
        isDraw: chess.isDraw(),
        captured: result.captured || null,
        newFen: chess.fen(),
      });
    } catch { return JSON.stringify({ legal: false }); }
  },
});

export const legalityAgent = new Agent({
  model: new BedrockModel({ modelId: 'eu.amazon.nova-pro-v1:0' }),
  systemPrompt:
    'You validate chess moves. Call check_move (for from/to squares) or ' +
    'check_san_move (for SAN like Nf3, Bc4). ' +
    'Your ENTIRE response must be the JSON object returned by the tool. ' +
    'Do NOT add any text, explanation, or formatting. Only the raw JSON.',
  tools: [checkMove, checkSanMove],
  printer: false,
});
</code></pre>
<p>Wrapping chess.js in an LLM agent is inherently roundabout. A direct function call would be faster, cheaper, and more reliable. We chose this design deliberately as a teaching example. It demonstrates how the Strands SDK handles tool-equipped agents, how the model selects the right tool from multiple options, and (via the opponent's validate_move tool) how agents compose through AgentCore Runtime. Think of the legality agent as a stand-in for any validation service you might wrap in an agent interface.</p>
<p>We initially used Nova Micro for this agent, since legality checking is a tool-only operation. The model just needs to call the right tool and return its output. In practice, Nova Micro would call the tool correctly but then rewrite the JSON result as prose, ignoring the system prompt instruction to return raw JSON. Switching to Nova Pro solved this. The Python SDK has a <a href="https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/structured-output/">structured output feature</a> that validates responses against a schema and retries on failure, which could help here, but it's not yet available in the TypeScript SDK.</p>
<p>The agent has two tools because the frontend sends player moves as from/to squares (from the drag-and-drop interaction), while the opponent and advisor produce moves in SAN notation. Rather than converting between formats on the client, we let the model pick the appropriate tool.</p>
<p>The <code>newFen</code> field in the response is the key design choice. It gives the frontend the authoritative next position (with updated castling rights, en passant squares, and move clocks) without needing a chess library on the client. The frontend stores the board as a simple piece map (<code>Record&lt;string, string&gt;</code>, e.g. <code>{ e1: 'wK', d2: 'wP', ... }</code>) and reconstructs it from the FEN after each validated move.</p>
<p>This creates a UX tradeoff. With a client-side chess library, move validation is instant. With an async legality agent, there is a visible delay. We lean into it: the piece appears ghosted at its destination immediately, and a brief "checking move" state gives visual feedback. If the move turns out to be illegal, the destination square flashes red and the piece reverts. This feels responsive even though the validation is happening over the network.</p>
<h2>Deploying to AgentCore</h2>
<p>The CDK stack creates six resources: three ECR repositories, an IAM role, and three AgentCore Runtimes. The IAM role trusts the AgentCore service principal and grants Bedrock model invocation:</p>
<pre><code class="language-typescript">const runtimeRole = new iam.Role(this, 'AgentRuntimeRole', {
  assumedBy: new iam.ServicePrincipal('bedrock-agentcore.amazonaws.com'),
  inlinePolicies: {
    BedrockInvoke: new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          actions: ['bedrock:InvokeModel', 'bedrock:InvokeModelWithResponseStream'],
          resources: ['*'],
        }),
      ],
    }),
  },
});
</code></pre>
<p>The runtimes themselves use the L2 construct from <code>@aws-cdk/aws-bedrock-agentcore-alpha</code>:</p>
<pre><code class="language-typescript">const advisorRuntime = new agentcore.Runtime(this, 'AdvisorRuntime', {
  runtimeName: 'centaur-advisor',
  agentRuntimeArtifact: agentcore.AgentRuntimeArtifact.fromEcrRepository(advisorRepo, 'latest'),
  networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
  environmentVariables: { AGENT_TYPE: 'advisor' },
  executionRole: runtimeRole,
});
</code></pre>
<p>That's the entire deployment for one agent. AgentCore Runtime handles the container lifecycle. The opponent and legality runtimes are identical except for the name and environment variable.</p>
<h2>Calling Agents from Next.js</h2>
<p>The frontend calls AgentCore through Next.js API routes:</p>
<pre><code class="language-typescript">import {
  BedrockAgentCoreClient,
  InvokeAgentRuntimeCommand,
} from '@aws-sdk/client-bedrock-agentcore';
import { pieceMapToFen, type GameMeta } from '../chess-utils';

const client = new BedrockAgentCoreClient({ region: process.env.AWS_REGION });

export async function POST(req: Request) {
  const { pieces, meta, move, sessionId } = await req.json();
  const fen = pieceMapToFen(pieces, meta);

  const command = new InvokeAgentRuntimeCommand({
    agentRuntimeArn: process.env.ADVISOR_ARN,
    runtimeSessionId: sessionId,
    payload: new TextEncoder().encode(
      JSON.stringify({
        prompt: `Position (FEN): \({fen}\nProposed move: \){move}\nEvaluate this move.`,
      }),
    ),
  });

  // ... response parsing unchanged
}
</code></pre>
<p>The API routes accept a piece map and game metadata from the frontend. A shared <code>pieceMapToFen</code> utility converts this to FEN for the agent prompt, keeping the frontend free of any chess library dependency.</p>
<p>The <code>runtimeSessionId</code> is worth noting. We generate one per game and pass it with every request. AgentCore uses this to route requests to the same microVM and maintain conversation context. This gives us session memory without writing any memory management code.</p>
<p>Sessions can persist for up to 8 hours with a configurable idle timeout (default 15 minutes). If a session terminates, the microVM is cleaned up and a new request with the same ID creates a fresh environment. For a chess game that typically lasts minutes, this is plenty.</p>
<h2>Tradeoffs</h2>
<p>We learned a lot building this, but let's be direct about the limitations.</p>
<p><strong>LLM chess quality is poor.</strong> Language models learn chess patterns from training data but don't search move trees. The opponent now self-validates through the legality agent, which eliminates illegal moves, but it chains two LLM calls for every validation (opponent tool call to legality agent, which itself calls a tool via the LLM). The positional judgment still isn't trustworthy. This is a teaching example, not a competitive chess application.</p>
<p><strong>Async validation is a deliberate tradeoff.</strong> Removing the client-side chess library means every move goes through the network for validation. This adds latency compared to instant client-side checks. We traded that speed for a cleaner architecture (no chess logic duplicated between client and server) and a more interesting UX challenge. The ghost-move pattern, where the piece appears immediately and validates asynchronously, keeps the interaction feeling responsive. Whether this tradeoff makes sense depends on your latency budget. For a teaching example, it works well.</p>
<p><strong>The SDK is in preview.</strong> The Strands Agents TypeScript SDK was released in late 2025. The API surface is clean and the tool pattern is well-designed, but breaking changes should be expected. The AgentCore CDK constructs are similarly early. The L2 construct library is alpha, and we found the documentation is still catching up with the implementation.</p>
<p><strong>Cost is worth mentioning.</strong> Each agent session runs in a dedicated microVM that stays alive until the idle timeout (default 15 minutes). For a chess game with pauses between moves, that means we're paying for idle time within each session. For a teaching example this is fine, but for production we'd want to understand the pricing model and compare it against Lambda (pay-per-invocation) or Fargate (pay-per-task). AgentCore is still in preview, so pricing details may evolve.</p>
<p><strong>What's promising.</strong> Going from "agent code in a Docker image" to "running in the cloud with session management" takes one CDK construct. The container contract (<code>/ping</code> + <code>/invocations</code>) is simple, and the Strands tool pattern (Zod schema in, typed callback out, model decides when to call) keeps tool definitions clean. The three agents in this project show the range: a tool-equipped advisor, a self-validating opponent that composes with another agent, and a pure validation agent. Same SDK, same deployment pattern, different designs for different jobs.</p>
<h2>Companion Repository</h2>
<p>The full example is available in the <a href="https://github.com/ganhammar/centaur-chess">GitHub repository</a>. To deploy:</p>
<ol>
<li><p>Clone the repo and run <code>npm install</code></p>
</li>
<li><p>Configure AWS credentials for a region that supports AgentCore Runtime (at the time of writing, availability is limited, check the <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore-supported-regions.html">AWS documentation</a> for supported regions)</p>
</li>
<li><p>Run <code>./deploy.sh</code> to build, push images, and deploy infrastructure</p>
</li>
<li><p>Copy the output ARNs to <code>frontend/.env.local</code> (see <code>.env.local.example</code> for the format)</p>
</li>
<li><p>Run <code>npm run dev -w frontend</code> to start the local dev server</p>
</li>
</ol>
<p>The board renders at <code>localhost:3000</code>. Drag a white piece to get the advisor's take, then confirm or try a different move.</p>
<hr />
<p>Hi there, I'm <a href="https://ganhammar.se"><strong>Anton Ganhammar</strong></a>! If you enjoyed this post make sure to follow me on <a href="https://www.linkedin.com/in/ganhammar/"><strong>LinkedIn</strong></a> 👋</p>
<hr />
<p><a href="https://elva-group.com/"><strong>Elva</strong></a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Safe AI Development with Claude Code Permission Controls]]></title><description><![CDATA[Introduction
You've seen how generative AI can automate boilerplate setup and accelerate project work to 80-90% completion, leaving you to focus on the complex, high-value tasks. The potential productivity gains are significant, and the use case is c...]]></description><link>https://blog.elva-group.com/safe-ai-development-with-claude-code-permission-controls</link><guid isPermaLink="true">https://blog.elva-group.com/safe-ai-development-with-claude-code-permission-controls</guid><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[claude]]></category><dc:creator><![CDATA[Joel Roxell]]></dc:creator><pubDate>Thu, 20 Nov 2025 07:00:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763554812447/14f52486-9c40-4b68-8f6f-4e593d048b04.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>You've seen how generative AI can automate boilerplate setup and accelerate project work to 80-90% completion, leaving you to focus on the complex, high-value tasks. The potential productivity gains are significant, and the use case is clear.</p>
<p>Yet many organizations have legitimate concerns about using AI tools along with proprietary code. Security teams need assurance that sensitive information won't be exposed, and that's entirely reasonable. The good news is that modern AI coding assistants, such as Claude Code, are built with these concerns in mind.</p>
<p>This is a hands-on guide to addressing security requirements when using Claude Code with proprietary code. As generative AI becomes more sophisticated, the question isn't whether to use these tools; it's how to use them responsibly. I'll walk you through practical solutions that protect sensitive information while still enabling the productivity benefits of AI-assisted development.</p>
<h2 id="heading-understanding-claude-codes-security-architecture">Understanding Claude Code's Security Architecture</h2>
<p><strong>Permission-Based System</strong></p>
<p>Claude Code operates on a <strong>strict permission model</strong> as documented in <a target="_blank" href="https://docs.claude.com/en/docs/claude-code/iam">the official IAM guide</a>. Each action the agent takes will be reviewed and allowed or blocked based on your configuration. If you haven't configured anything, the defaults are displayed in the following table.</p>
<p><strong>Tool Permission Tiers:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>ToolType</td><td>Example</td><td>Approval Required</td></tr>
</thead>
<tbody>
<tr>
<td>Read-only</td><td>File reads, LS, Grep</td><td>No</td></tr>
<tr>
<td>Bash Commands</td><td>Shell execution</td><td>Yes</td></tr>
<tr>
<td>File Modification</td><td>Edit/write files</td><td>Yes</td></tr>
</tbody>
</table>
</div><p><strong>What Gets Sent to Anthropic's API?</strong></p>
<p><strong>Sent to Claude API:</strong><br />- Code you explicitly include in prompts<br />- Files Claude Code reads (with your permission)<br />- Command outputs you approve<br />- Conversation history</p>
<p><strong>NOT sent:</strong><br />- Files blocked by permission rules<br />- Environment variables (unless explicitly referenced)<br />- Files outside your working directory<br />- Other projects on your machine</p>
<p><strong>Note</strong> - <strong>According to Anthropic's commercial terms, your code is not used to train models</strong>. See the <a target="_blank" href="https://trust.anthropic.com">Trust Center</a> for more info and <a target="_blank" href="https://trust.anthropic.com/faq">FAQ</a>.</p>
<p><img src="https://codahosted.io/docs/Bc7BhIXsXA/blobs/bl-syU0eHcWY1/a9f553c574d693bc189a96aa1619109e3dee49710b85905e72d3ddfafddcccce74c0d972196860e36333ca03613ad801a8683b69c44dea4d25cc86f0884ed521933fee06cbb4347ea5c74c943a43f261d2f7a37cf149d3c341a1e21333e7dad05cae9230" alt="image.png" /></p>
<p>The following sections outline several strategies you can use to maintain safety while working.</p>
<h2 id="heading-use-permission-settings-to-protect-sensitive-files">Use Permission Settings to Protect Sensitive Files</h2>
<p>The <strong>official way</strong> to exclude sensitive files is to use <strong>permissions.deny</strong> in <code>settings.json</code>.</p>
<p><strong>Create Project Security Settings</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Create .claude directory example</span>
mkdir -p .claude

<span class="hljs-comment"># Create settings.json with deny rules</span>
cat &gt; .claude/settings.json &lt;&lt; <span class="hljs-string">'EOF'</span>
{
  <span class="hljs-string">"permissions"</span>: {
    <span class="hljs-string">"deny"</span>:[
      <span class="hljs-string">"Read(config/production.yml)"</span>
    ],
    <span class="hljs-string">"ask"</span>: [
      <span class="hljs-string">"Bash(git push *)"</span>,
      <span class="hljs-string">"Bash(npm install *)"</span>
    ],
    <span class="hljs-string">"allow"</span>: [
      <span class="hljs-string">"Read(src/public/**)"</span>,
      <span class="hljs-string">"Read(tests/**)"</span>,
      <span class="hljs-string">"Edit(src/public/**)"</span>
    ]
  }
}
EOF

<span class="hljs-comment"># Commit to share with the team</span>
git add .claude/settings.json
git commit -m <span class="hljs-string">"Add Claude Code security settings"</span>
</code></pre>
<p><img src="https://codahosted.io/docs/Bc7BhIXsXA/blobs/bl-GOaDP95zvr/b4e9a880c4b61170ea7867e2dcc05cc619d6cae72e2eea034b408173a725f178c4a02ec321e81aea84a682482322554011f699491e441516ee916de98d4b7979236f37a8fb6b2a24b6d05630b7ff47685e9b2515634b971eb164af88ef5d2e21c89dbd56" alt="Screenshot 2025-11-13 at 15.00.26.png" /></p>
<h3 id="heading-path-pattern-types"><strong>Path Pattern Types</strong></h3>
<p>Claude Code uses the gitignore specification, as explained in the IAM guide:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>//path</td><td>Absolute from filesystem root</td><td>Read(//Users/alice/secrets/**)</td><td>/Users/alice/secrets/**</td></tr>
</thead>
<tbody>
<tr>
<td>~/path</td><td>From home directory</td><td>Read(~/Documents/*.pdf)</td><td>/Users/alice/Documents/*.pdf</td></tr>
<tr>
<td>/path</td><td>Relative to the settings file</td><td>Edit(/src/**/*.ts)</td><td>&lt;settings-file-path&gt;/src/**/*.ts</td></tr>
<tr>
<td>path</td><td>Relative to the current directory</td><td>Read(*.env)</td><td>&lt;cwd&gt;/*.env</td></tr>
</tbody>
</table>
</div><p><strong>Important</strong>: A pattern like /Users/alice/file is NOT absolute - use //Users/alice/file for absolute paths!</p>
<p><strong>Test Your Permissions</strong></p>
<p>Create your settings files and the permission boundaries you want, then run clode and review them.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># View current permissions</span>
claude
/permissions
<span class="hljs-comment"># Try to access blocked file</span>
claude <span class="hljs-string">"Show me the .env file"</span>
<span class="hljs-comment"># Should be denied</span>

<span class="hljs-comment"># Try to access allowed file</span>
claude <span class="hljs-string">"Show me src/api.py"</span>
<span class="hljs-comment"># Should work</span>
</code></pre>
<p><img src="https://codahosted.io/docs/Bc7BhIXsXA/blobs/bl-hdF7LG6aae/e2ec86fda09ca9a260e5c347e28e8df1613d9e00b4352f9e1adcbe3e01f5102cc6ef544f20f883d4a0ed2e9047c2fd33471d1557ad32de1fc700dd66d640b95f376b3b53e5ffb8cc82af0693f1baabc5803efb455f44b29383582764d064cf5ca4898ca5" alt="image.png" /></p>
<h2 id="heading-segment-your-workflow-by-project-structure">Segment Your Workflow by Project Structure</h2>
<p>With this approach, you can split your project into public and proprietary sections. This keeps sensitive files out of reach for Claude while allowing the model to access more generic parts. You can protect your unique business code and still use AI to help with routine tasks, like exposing data from a database through a web API.</p>
<pre><code class="lang-plaintext">my-project/
├── public/                # ✅ Safe for Claude
│   ├── api/               # Public API interfaces
│   └── utils/             # Generic utilities
│
├── proprietary/           # ⚠️ Sensitive
│   ├── algorithms/        # Proprietary business logic
│   └── integrations/      # Third-party secrets
│
└── .claude/
    └── settings.json      # Protection rules
</code></pre>
<p>Following the earlier pattern, edit .claude/settings.json:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"permissions"</span>: {
    <span class="hljs-attr">"deny"</span>: [
      <span class="hljs-string">"Read(proprietary/**)"</span>,
      <span class="hljs-string">"Read(**/proprietary/**)"</span>
    ],
    <span class="hljs-attr">"allow"</span>: [
      <span class="hljs-string">"Read(public/**)"</span>,
      <span class="hljs-string">"Edit(public/**)"</span>
    ]
  }
}
</code></pre>
<h2 id="heading-settings-precedence">Settings Precedence</h2>
<p>Good to know: as documented in <a target="_blank" href="https://code.claude.com/docs/en/iam#settings-precedence">IAM settings precedence</a></p>
<p>1. <strong>Enterprise policies</strong> (highest - cannot be overridden)<br />2. Command line arguments<br />3. Local project settings (`.claude/settings.local.json`)<br />4. Shared project settings (`.claude/settings.json`)<br />5. User settings (`~/.claude/settings.json`)</p>
<p><strong>Summary</strong></p>
<p>If your company or administration doesn’t allow you to use generative AI in your workflow, the company will fall behind. It’s your job to make them understand that there are solutions to work with these tools and still protect your edge.</p>
<p>The permissions are <code>settings.json</code> provided:</p>
<p>1. <strong>Granular Control</strong>: You can do more than hide files. You can control reads, writes, commands, and network access.<br />2. <strong>Team Sharing</strong>: Commit <code>.claude/settings.json</code> to share security rules<br />3. <strong>Enterprise Enforcement</strong>: IT can enforce policies that can't be bypassed<br />4. <strong>Flexibility</strong>: Different rules for different projects and team members<br />5. <strong>Auditability</strong>: All permissions are explicit and version-controlled</p>
<p><strong>Next Steps:</strong></p>
<ol>
<li><p>Tell the person blocking you from using generative AI that it’s possible to set permission boundaries for the models.</p>
</li>
<li><p>Focus on the essential tasks and let the AI handle the routine work.</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Supercharge yourself with your own team of agents]]></title><description><![CDATA[Since entering the world of creating agents, one of the most powerful patterns being used is the supervisor, also commonly referred to as the orchestrator pattern. Very quickly explained: the supervisor pattern consists of a lead agent who delegates ...]]></description><link>https://blog.elva-group.com/supercharge-yourself-with-your-own-team-of-agents</link><guid isPermaLink="true">https://blog.elva-group.com/supercharge-yourself-with-your-own-team-of-agents</guid><category><![CDATA[AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[supervisor]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Strands Agents]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Tobias Edvardsson]]></dc:creator><pubDate>Thu, 23 Oct 2025 11:59:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/fEgt5QRI-rA/upload/b99a73a74f339f2d227f9e926fd5b416.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Since entering the world of creating agents, one of the most powerful patterns being used is the supervisor, also commonly referred to as the orchestrator pattern. Very quickly explained: the supervisor pattern consists of a lead agent who delegates the work to specialized agents who actually perform the task being requested.</p>
<p>This article will go through the concepts related to a supervisor pattern, and we will also build our own agentic team using this pattern. Given I have started to dive into the AWS open-source framework Strands (check out <a target="_blank" href="https://blog.elva-group.com/lets-build-an-agent-on-aws">Let's Build an Agent on AWS</a> to get rolling here), the article will cover their approach to this, which they call "Agents as Tools."</p>
<h2 id="heading-what-makes-this-pattern-so-powerful">What Makes This Pattern So Powerful?</h2>
<p>Getting meaningful answers from an LLM comes down to two things: the right instructions and the right context. Generalize too much, and you'll quickly hit issues with how current LLMs behave. This might not be a problem forever, as we're already seeing major providers adopt specialization patterns behind the scenes to improve both performance and results.</p>
<p>Being a software engineer by trade, from my point of view, this simplifies a lot since you can create much more modular parts of your overall solutions instead of having a large monolithic approach for your agent. Small modular replaceable parts that you can develop in isolation and attach when complete. Plays in nicely with the classical modular approaches with low coupling.</p>
<h2 id="heading-lets-go-through-the-pattern">Let's Go Through the Pattern</h2>
<p>From this point, I will only refer to the pattern as the supervisor pattern to keep it consistent.</p>
<p>On a high level, the supervisor pattern consists of one supervisor agent whose only purpose is to delegate incoming tasks to the available specialized agents. So, in practice, that means the instructions for the agent are pretty much only details about how it should behave, as well as which agents it has access to.</p>
<p><strong>Pseudo prompt example:</strong></p>
<pre><code class="lang-plaintext">Available agents:
- mathematician (solves all your math problems)
- weather guru (can give you insights of the weather)
- aws nerd (knows all the AWS docs)

Instructions:
You are a supervisor agent.

Your only task is to delegate incoming requests to the correct agent,
check "Available agents" to understand which agents you can delegate
tasks to.

If you do not have an agent suited for an incoming task,
reply that you are not able to help with the request.
</code></pre>
<p>There are plenty of diagrams that represent this setup. This one from LangGraph is easy enough to get an understanding of how it would look.</p>
<p><img src="https://langchain-ai.github.io/langgraph/tutorials/multi_agent/assets/diagram.png" alt="diagram" /></p>
<p>The supervisor takes the input from the users and delegates the task to the most relevant agent.</p>
<h2 id="heading-lets-get-building">Let's Get Building</h2>
<p>The full code is available at: <a target="_blank" href="https://github.com/elva-labs/strands-multi-agent-blog-example">https://github.com/elva-labs/strands-multi-agent-blog-example</a></p>
<h3 id="heading-stack">Stack</h3>
<p>We will continue using the Strands Agents framework to set this up. If you have not used this before, check out their docs at: <a target="_blank" href="https://strandsagents.com/latest/documentation/docs/user-guide/quickstart/">https://strandsagents.com/latest/documentation/docs/user-guide/quickstart/</a> or my previous article <a target="_blank" href="https://blog.elva-group.com/lets-build-an-agent-on-aws">https://blog.elva-group.com/lets-build-an-agent-on-aws</a> on how to get running.</p>
<h3 id="heading-first-steps">First Steps</h3>
<p>Let's get our supervisor agent up and just make sure all works as expected.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> strands <span class="hljs-keyword">import</span> Agent

agent = Agent(
    system_prompt=<span class="hljs-string">"""
You are a supervisor agent, overseeing multiple specialized agents.

Your task is to delegate incoming requests to the appropriate specialized
agent based on the nature of the request.

If you do not have a specialized agent for a request, you return a polite
message indicating that you cannot handle the request.
"""</span>,
)

message = <span class="hljs-string">"""
Please help me research the effects of climate change on polar bear populations.
"""</span>
agent(message)
</code></pre>
<p>We do get an expected response, something like:</p>
<pre><code class="lang-plaintext">git:(main) ✗ uv run src/main.py
I appreciate your interest in researching the effects of
climate change on polar bear populations. However, I don't currently have
a specialized research agent available to handle this type of scientific
research request.

For this important topic, I'd recommend consulting:
- Peer-reviewed scientific journals focusing on climate science and
wildlife biology
- Reports from organizations like the IUCN Polar Bear Specialist Group
- Research from institutions like the U.S. Geological Survey or Polar
Bears International
- Academic databases such as Google Scholar, PubMed, or Web of Science

These sources will provide you with the most current and authoritative
information on how climate change is impacting polar bear populations,
including data on habitat loss, hunting success rates, reproduction,
and population trends.

I apologize that I cannot provide direct research assistance on this
topic at this time.
</code></pre>
<p>From what we can see, the supervisor does not have any available "tool/sub-agent" to solve this, hence it will just return that it cannot help you out.</p>
<h3 id="heading-adding-a-specialized-agent">Adding a Specialized Agent</h3>
<p>Let's add a simulated weather specialized agent.</p>
<p>One of the nice things when creating a sub-agent is that you can fully create a standalone agent in the first steps, and try that out in isolation until you are happy with the result. Let's do that.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> random
<span class="hljs-keyword">from</span> strands <span class="hljs-keyword">import</span> Agent, tool


<span class="hljs-meta">@tool</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_weather_data</span>(<span class="hljs-params">location: str, unit: str = <span class="hljs-string">"celsius"</span></span>) -&gt; str:</span>
    <span class="hljs-string">"""
    Simulate an external API call to fetch weather data for a given location.

    This tool simulates calling a weather service API (like OpenWeatherMap)
    to retrieve current weather conditions.

    Args:
        location: The city or location name to get weather for (e.g., "London", "New York")
        unit: Temperature unit, either "celsius" or "fahrenheit" (default: "celsius")

    Returns:
        A JSON-formatted string containing simulated weather data
    """</span>
    <span class="hljs-comment"># Simulate API call delay and response</span>
    <span class="hljs-comment"># In a real implementation, this would be: requests.get(f"https://api.weather.com/data?location={location}")</span>

    <span class="hljs-comment"># Generate simulated weather data</span>
    temperature = random.randint(<span class="hljs-number">-10</span>, <span class="hljs-number">35</span>) <span class="hljs-keyword">if</span> unit == <span class="hljs-string">"celsius"</span> <span class="hljs-keyword">else</span> random.randint(<span class="hljs-number">14</span>, <span class="hljs-number">95</span>)
    conditions = random.choice([<span class="hljs-string">"Sunny"</span>, <span class="hljs-string">"Cloudy"</span>, <span class="hljs-string">"Partly Cloudy"</span>, <span class="hljs-string">"Rainy"</span>, <span class="hljs-string">"Stormy"</span>, <span class="hljs-string">"Snowy"</span>])
    humidity = random.randint(<span class="hljs-number">30</span>, <span class="hljs-number">90</span>)
    wind_speed = random.randint(<span class="hljs-number">5</span>, <span class="hljs-number">40</span>)

    weather_data = {
        <span class="hljs-string">"location"</span>: location,
        <span class="hljs-string">"temperature"</span>: temperature,
        <span class="hljs-string">"unit"</span>: unit,
        <span class="hljs-string">"conditions"</span>: conditions,
        <span class="hljs-string">"humidity"</span>: humidity,
        <span class="hljs-string">"wind_speed"</span>: wind_speed,
        <span class="hljs-string">"wind_unit"</span>: <span class="hljs-string">"km/h"</span>
    }

    <span class="hljs-comment"># Return formatted string (simulating API JSON response)</span>
    <span class="hljs-keyword">return</span> str(weather_data)


<span class="hljs-comment"># Define a specialized system prompt for the weather agent</span>
WEATHER_AGENT_PROMPT = <span class="hljs-string">"""
You are a specialized weather assistant. You can provide current weather
information and forecasts for any location. Use the weather API tool to
fetch weather data and present it in a clear, user-friendly format.
"""</span>

weather_agent = Agent(
    system_prompt=WEATHER_AGENT_PROMPT,
    tools=[get_weather_data],
)

weather_agent(<span class="hljs-string">"What's the weather like in London?"</span>)
</code></pre>
<p>Let's try it out.</p>
<pre><code class="lang-plaintext">git:(main) ✗ uv run src/weather_agent.py 
I'll get the current weather information for London for you.
Tool #1: get_weather_data
Here's the current weather in London:

**🌤️ London Weather**
- **Temperature:** 24°C
- **Conditions:** Sunny
- **Humidity:** 85%
- **Wind Speed:** 34 km/h

It's a lovely sunny day in London with pleasant temperatures!
The humidity is quite high at 85%, and there's a moderate breeze with
winds at 34 km/h.
</code></pre>
<p>We are now in a state where we can run both agents in isolation. To tie them together, Strands uses the concept of making a tool of the actual agent. This is done in the same way you work with regular tools for your agents.</p>
<h4 id="heading-preparing-the-agent-for-our-supervisor">Preparing the Agent For Our Supervisor</h4>
<p>Strands uses the concept of "agents as tools." Since tools in Strands are just decorated functions, we need to wrap our agent's initialization and call in a function, then mark it with <code>@tool</code>.</p>
<p>Let's follow that pattern, and to make it possible to still directly trigger the file to test the agent in isolation, let's add a <code>if __name__ == "__main__":</code> which basically means, if this file is the file being triggered, run this code.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> random
<span class="hljs-keyword">from</span> strands <span class="hljs-keyword">import</span> Agent, tool

<span class="hljs-comment"># Define a specialized system prompt</span>
WEATHER_AGENT_PROMPT = <span class="hljs-string">"""
You are a specialized weather assistant. You can provide current weather
information and forecasts for any location. Use the weather API tool to
fetch weather data and present it in a clear, user-friendly format.
"""</span>


<span class="hljs-meta">@tool</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_weather_data</span>(<span class="hljs-params">location: str, unit: str = <span class="hljs-string">"celsius"</span></span>) -&gt; str:</span>
    <span class="hljs-string">"""
    Simulate an external API call to fetch weather data for a given location.

    This tool simulates calling a weather service API (like OpenWeatherMap)
    to retrieve current weather conditions.

    Args:
        location: The city or location name to get weather for (e.g., "London", "New York")
        unit: Temperature unit, either "celsius" or "fahrenheit" (default: "celsius")

    Returns:
        A JSON-formatted string containing simulated weather data
    """</span>
    <span class="hljs-keyword">try</span>:
        <span class="hljs-comment"># Simulate API call delay and response</span>
        <span class="hljs-comment"># In a real implementation, this would be: requests.get(f"https://api.weather.com/data?location={location}")</span>

        <span class="hljs-comment"># Generate simulated weather data</span>
        temperature = (
            random.randint(<span class="hljs-number">-10</span>, <span class="hljs-number">35</span>) <span class="hljs-keyword">if</span> unit == <span class="hljs-string">"celsius"</span> <span class="hljs-keyword">else</span> random.randint(<span class="hljs-number">14</span>, <span class="hljs-number">95</span>)
        )
        conditions = random.choice(
            [<span class="hljs-string">"Sunny"</span>, <span class="hljs-string">"Cloudy"</span>, <span class="hljs-string">"Partly Cloudy"</span>, <span class="hljs-string">"Rainy"</span>, <span class="hljs-string">"Stormy"</span>, <span class="hljs-string">"Snowy"</span>]
        )
        humidity = random.randint(<span class="hljs-number">30</span>, <span class="hljs-number">90</span>)
        wind_speed = random.randint(<span class="hljs-number">5</span>, <span class="hljs-number">40</span>)

        weather_data = {
            <span class="hljs-string">"location"</span>: location,
            <span class="hljs-string">"temperature"</span>: temperature,
            <span class="hljs-string">"unit"</span>: unit,
            <span class="hljs-string">"conditions"</span>: conditions,
            <span class="hljs-string">"humidity"</span>: humidity,
            <span class="hljs-string">"wind_speed"</span>: wind_speed,
            <span class="hljs-string">"wind_unit"</span>: <span class="hljs-string">"km/h"</span>,
        }

        <span class="hljs-comment"># Return formatted string (simulating API JSON response)</span>
        <span class="hljs-keyword">return</span> str(weather_data)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"Error fetching weather data: <span class="hljs-subst">{str(e)}</span>"</span>


<span class="hljs-meta">@tool</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">weather_assistant</span>(<span class="hljs-params">query: str</span>) -&gt; str:</span>
    <span class="hljs-string">"""
    Process and respond to weather-related queries.

    This agent can answer questions about current weather conditions,
    forecasts, and provide weather information for specific locations.

    Args:
        query: A weather-related question (e.g., "What's the weather in Paris?")

    Returns:
        A detailed weather response with current conditions
    """</span>
    <span class="hljs-keyword">try</span>:
        weather_agent = Agent(
            system_prompt=WEATHER_AGENT_PROMPT,
            tools=[get_weather_data],
        )

        response = weather_agent(query)
        <span class="hljs-keyword">return</span> str(response)
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        <span class="hljs-keyword">return</span> <span class="hljs-string">f"Error in weather assistant: <span class="hljs-subst">{str(e)}</span>"</span>


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    result = weather_assistant(<span class="hljs-string">"What's the weather like in London?"</span>)
    print(result)
</code></pre>
<p>We can still run the file directly, but this opens up the possibility to actually use the agent function as a tool in another agent, which is the full goal here.</p>
<h3 id="heading-the-final-steps-add-it-to-our-supervisor">The Final Steps: Add It to Our Supervisor</h3>
<p>The last steps are straightforward. Just add the weather assistant tool to our supervisor agent. Strands will automatically update the supervisor's prompt to let it know the weather agent is available.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> strands <span class="hljs-keyword">import</span> Agent
<span class="hljs-keyword">from</span> weather_agent <span class="hljs-keyword">import</span> weather_assistant

agent = Agent(
    system_prompt=<span class="hljs-string">"""
You are a supervisor agent, overseeing multiple specialized agents.

Your task is to delegate incoming requests to the appropriate specialized
agent based on the nature of the request.

If you do not have a specialized agent for a request, you return a polite
message indicating that you cannot handle the request.
"""</span>,
    tools=[weather_assistant],  <span class="hljs-comment"># Add other specialized agents here as needed</span>
)

<span class="hljs-comment"># Ask the agent a question that uses the available tools</span>
message = <span class="hljs-string">"""
What can you assist me with?
"""</span>
agent(message)
</code></pre>
<p>When we run the agent, we will now receive what it can help us with:</p>
<pre><code class="lang-plaintext">git:(main) ✗ uv run src/main.py
Hello! I'm a supervisor agent that can help coordinate different types of
requests.
Currently, I have access to a specialized weather assistant that can help
with:

**Weather-related queries:**
- Current weather conditions for any location
- Weather forecasts
- Temperature, humidity, precipitation information
- Weather-related questions and advice

For example, you could ask:
- "What's the weather like in New York today?"
- "Will it rain tomorrow in London?"
- "What's the current temperature in Tokyo?"

If you have any weather-related questions, I'd be happy to connect you
with the weather assistant. For other types of requests outside of
weather information, I may not have the appropriate specialized
agent available, but feel free to ask and I'll let you know if I
can help!

What would you like assistance with?%
</code></pre>
<h3 id="heading-done">Done!</h3>
<p>We have successfully added a new specialized agent to our team. The supervisor pattern is very powerful in this way, as you can easily develop new agents in isolation, add the specialized tools needed for certain tasks to only that agent, without the need to worry about the overall solution. Making it modular and nicely decoupled.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>At this moment, the supervisor pattern provides a nice modular approach to building larger agentic solutions. The biggest positives are that you can very easily add new functionality with new agents, extending the current solution without worrying about the other implementations, which works very well in teams and companies. Need a new agent? Create that in isolation to achieve the goal, and just expose it later via the supervisor.</p>
<p>Outside of the supervisor pattern, there are plenty of other ideas floating around on how to orchestrate larger agentic systems. Strands has a nice write-up of a few of them on their docs. Check that out at <a target="_blank" href="https://strandsagents.com/latest/documentation/docs/user-guide/concepts/multi-agent/multi-agent-patterns/">https://strandsagents.com/latest/documentation/docs/user-guide/concepts/multi-agent/multi-agent-patterns/</a>.</p>
<p>Ready to create your own team of specialized agents?</p>
<hr />
<p>If you enjoyed this post, want to know more about me, working at Elva, or just want to reach out, you can find me on <a target="_blank" href="https://www.linkedin.com/in/tedvardsson/">LinkedIn</a>.</p>
<hr />
<p><a target="_blank" href="https://www.elva-group.com"><strong>Elva</strong></a> is an AWS specialized consulting company that can help you transform or begin your AWS journey.</p>
]]></content:encoded></item><item><title><![CDATA[Let's Build an Agent on AWS!]]></title><description><![CDATA[AWS is going all-in on AI, and they're making it easier than ever to build agents in their ecosystem. They've recently released two key pieces: Amazon Bedrock AgentCore (a managed runtime for deploying agents) and Strands (an open-source Python frame...]]></description><link>https://blog.elva-group.com/lets-build-an-agent-on-aws</link><guid isPermaLink="true">https://blog.elva-group.com/lets-build-an-agent-on-aws</guid><category><![CDATA[agentic AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Strands Agents]]></category><category><![CDATA[bedrock]]></category><category><![CDATA[bedrock agentcore]]></category><dc:creator><![CDATA[Tobias Edvardsson]]></dc:creator><pubDate>Fri, 17 Oct 2025 09:34:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/jMDtJtFs8EQ/upload/ab175b0a76fa63590f63b4b230d25224.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS is going all-in on AI, and they're making it easier than ever to build agents in their ecosystem. They've recently released two key pieces: Amazon Bedrock AgentCore (a managed runtime for deploying agents) and Strands (an open-source Python framework for building them).</p>
<p>This article is aimed toward you, developers, or those who have some familiarity with code. If you want to check out articles talking about the general concepts surrounding AI, check out these posts from me and my colleagues:</p>
<ul>
<li><p><a target="_blank" href="https://blog.elva-group.com/robots-are-taking-our-jobs">Robots Are Taking Our Jobs</a></p>
</li>
<li><p><a target="_blank" href="https://blog.elva-group.com/solving-ai-context-with-mcp-servers">Solving AI Context With MCP Servers</a></p>
</li>
<li><p><a target="_blank" href="https://blog.elva-group.com/moving-past-the-ai-hype-introducing-mcp">Moving Past The AI Hype MCP</a></p>
</li>
</ul>
<h2 id="heading-a-quick-agent-recap">A Quick Agent Recap</h2>
<p>Let's quickly walk through what an agent is from a technical perspective.</p>
<h3 id="heading-tldr">TLDR;</h3>
<p>An agent is a loop that, with the help of different tools, will enrich the query being sent to the LLM and continue to query the LLM with new information until a task is complete or a question is answered.</p>
<h3 id="heading-but-what-does-this-mean-in-practice">But What Does This Mean in Practice?</h3>
<p>When you ask an agent a question or give an agent a task to perform, it will try to answer your question based on the instructions it's been given. These instructions can be just plain text with something like "Answer all questions in Swedish", which would make the agent only return answers in Swedish. But when you dive into the more complex setups, the instructions are often complemented with details that make agents so powerful, such as:</p>
<ul>
<li><p>The ability to query specific internal data (Data retrieval/RAG)</p>
</li>
<li><p>Read history from previous conversations (Memory)</p>
</li>
<li><p>Have ways to perform tasks or retrieve external information (Tools &amp; MCP Servers)</p>
</li>
<li><p>Be aware of who is asking the question (User context)</p>
</li>
</ul>
<p>A pseudo example of a prompt inside an agentic loop could look something like:</p>
<pre><code class="lang-plaintext">History:
USER: Question
AGENT: Answer

User context:
UserID: 123
UserName: Name

Context:
&lt;Information from previous tool calls&gt;

Instructions:
You are a seasoned software engineer who answers questions about best practices on AWS. ALWAYS answer the questions in Swedish.

Tools Available:
You have the following tools available. Answer with a TOOL_CALL when you need to use a tool.
- Search the web (parameters: question)
- AWS Documentation MCP (parameters: service)

Definitions:
A TOOL_CALL should always be returned as JSON following the format:
{"tool_id": "id", "parameters": {"example_param": "abc"}}

Question:
USER: How much memory can I configure a Lambda to have?
</code></pre>
<p>This prompt will continuously be reused, added to, and sometimes redacted from until a goal is reached, and sent multiple times to the LLM. Ask a follow-up question? History will be extended, context will be increased with potential new information using tools, and sent to the LLM to generate a potential answer.</p>
<p>And to make it clear, the LLM itself doesn't do anything here—all the extending and actual work with tools and the prompt comes from the agentic frameworks themselves.</p>
<p>As an example, if a TOOL_CALL is requested from the LLM, an agentic framework will parse this, and by regular software engineering, figure out that it's time to use an actual piece of code with the parameters that the LLM wanted to use and pass that information back to the next prompt being sent to the LLM.</p>
<p>This concept and logic are what an agentic framework solves for you.</p>
<h2 id="heading-lets-get-building">Let's Get Building</h2>
<p>Given the concepts above, the industry is slowly standardizing around concepts on how to achieve these steps needed for an effective agentic loop. Most frameworks similarly approach this, and nowadays it's usually the developer experience or choice of language that makes the difference.</p>
<h3 id="heading-whats-needed-to-get-going">What's Needed to Get Going</h3>
<h4 id="heading-runtime">Runtime</h4>
<p>When we actually want to deploy our agent, AWS approaches this with AWS Bedrock AgentCore, which enables you to quickly set up the infrastructure you need to actually run an agent on the web. This includes, for example, the runtime for the actual agent (container-based), as well as the infrastructure behind the tools an agent needs, such as memory, search, and code execution.</p>
<p>Check out their page: <a target="_blank" href="https://aws.amazon.com/bedrock/agentcore/">https://aws.amazon.com/bedrock/agentcore/</a> for more details on what the runtime offers.</p>
<h4 id="heading-framework">Framework</h4>
<p>AWS has also released Strands, which is an open-source Python framework for agents. Strands is extremely quick to get started with and follows a model-agnostic approach, and can be run anywhere—which is a nice surprise coming from AWS.</p>
<p>You can find all details about Strands at: <a target="_blank" href="https://strandsagents.com/">https://strandsagents.com/</a></p>
<h3 id="heading-what-are-we-building">What Are We Building?</h3>
<p>Now that we have both a framework (Strands) and a deployment platform (AgentCore), let's build something practical: an agent that can search the web and query AWS documentation. By the end, we'll deploy it to AWS so other services can use it.</p>
<h4 id="heading-requirements">Requirements</h4>
<p><a target="_blank" href="https://strandsagents.com/latest/documentation/docs/">https://strandsagents.com/latest/documentation/docs/</a> includes a straightforward getting-started guide. To set up the example, we will use the following Python and AWS setup:</p>
<ul>
<li><p>Python 3.13.5</p>
</li>
<li><p>uv (<a target="_blank" href="https://docs.astral.sh/uv/">https://docs.astral.sh/uv/</a>)</p>
</li>
<li><p>An AWS account with access to Claude Sonnet 4 via Bedrock, and environment variables for the secrets (<a target="_blank" href="https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-envvars.html">https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-envvars.html</a>)</p>
</li>
<li><p>(optional) sesh to quickly manage our AWS credentials (<a target="_blank" href="https://github.com/elva-labs/awsesh">https://github.com/elva-labs/awsesh</a>)</p>
</li>
</ul>
<h4 id="heading-first-steps">First Steps</h4>
<p>Let's get started (the steps are written from a Mac perspective):</p>
<ol>
<li><p>Create a new project folder</p>
</li>
<li><p>Using your terminal, go to the folder and run <code>uv init</code> to set up a new project</p>
</li>
<li><p>Activate your virtual environment via <code>source .venv/bin/activate</code></p>
</li>
<li><p>Add the Strands library via <code>uv add strands-agents</code></p>
</li>
<li><p>In the terminal, set your AWS credentials via environment variables</p>
</li>
</ol>
<p>We now should have everything we need to get going to test out their example.</p>
<p>Copy-paste this code into the <a target="_blank" href="http://main.py"><code>main.py</code></a> file (taken from the Strands docs):</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> strands <span class="hljs-keyword">import</span> Agent

<span class="hljs-comment"># Create an agent with default settings</span>
agent = Agent()

<span class="hljs-comment"># Ask the agent a question</span>
agent(<span class="hljs-string">"Tell me about agentic AI in one sentence"</span>)
</code></pre>
<p>And let's run it via <a target="_blank" href="http://main.py"><code>python main.py</code></a>. If all works as expected, you should have an output similar to:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760683751836/03d32c63-cf24-4fcc-9eaf-80a925652d4a.png" alt class="image--center mx-auto" /></p>
<p>Perfect! We are now up and running and can start adding functionality.</p>
<h4 id="heading-adding-search">Adding Search</h4>
<p>We want the agent to be able to search the web if it doesn't know the answer. Strands comes with a bunch of community-built tools which you can just install. Let's add the tool built for the AgentCore browser. (All community-built tools can be found at <a target="_blank" href="https://github.com/strands-agents/tools">https://github.com/strands-agents/tools</a>)</p>
<p>Let's run: <code>uv add 'strands-agents-tools[agent_core_browser]'</code> to add the package and add the tool to our agent.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> strands <span class="hljs-keyword">import</span> Agent
<span class="hljs-keyword">from</span> strands_tools.browser <span class="hljs-keyword">import</span> AgentCoreBrowser

browser_tool = AgentCoreBrowser(
    region=<span class="hljs-string">"eu-west-1"</span>
) <span class="hljs-comment"># Initialize the browser in a supported region</span>

agent = Agent(tools=[browser_tool.browser]) <span class="hljs-comment"># Add the browser tool to the agent</span>

<span class="hljs-comment"># Ask the agent to search the internet</span>
agent(<span class="hljs-string">"Use the browser to get the title of the latest AWS News Blog post."</span>)
</code></pre>
<p>When we run it, we can now see that the agent is using the tool to search for the latest blog post from AWS.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760683785999/3eb8b9e4-f095-4654-8351-35fc865c4831.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-what-about-mcp-servers">What About MCP Servers?</h4>
<p>Since MCP servers are a very powerful addition to add more abilities to your agents, let's also try adding one.</p>
<p>AWS has a lot of ready-to-use MCP servers. Let's choose one of their remote servers that is ready to consume. <a target="_blank" href="https://github.com/awslabs/mcp/tree/main/src/aws-knowledge-mcp-server">https://github.com/awslabs/mcp/tree/main/src/aws-knowledge-mcp-server</a> is a good example.</p>
<p>Add the MCP library via <code>uv add mcp</code>, and let's update the code to include an MCP server:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> strands <span class="hljs-keyword">import</span> Agent
<span class="hljs-keyword">from</span> strands_tools.browser <span class="hljs-keyword">import</span> AgentCoreBrowser
<span class="hljs-keyword">from</span> mcp.client.streamable_http <span class="hljs-keyword">import</span> streamablehttp_client
<span class="hljs-keyword">from</span> strands.tools.mcp.mcp_client <span class="hljs-keyword">import</span> MCPClient

aws_knowledge_mcp = MCPClient(
    <span class="hljs-keyword">lambda</span>: streamablehttp_client(<span class="hljs-string">"https://knowledge-mcp.global.api.aws"</span>)
)
browser_tool = AgentCoreBrowser(region=<span class="hljs-string">"eu-west-1"</span>)

<span class="hljs-keyword">with</span> aws_knowledge_mcp:
    <span class="hljs-comment"># Get the tools available from the MCP server</span>
    aws_knowledge_tools = aws_knowledge_mcp.list_tools_sync()
    <span class="hljs-comment"># Combine the browser tool with the MCP tools</span>
    tools = [browser_tool.browser] + aws_knowledge_tools

    agent = Agent(tools=tools)
    agent(<span class="hljs-string">"What tools do you have available?"</span>)
</code></pre>
<p>After running the agent again, you can now see that we have enabled more tools for the agent, which will give it the ability to answer more questions or perform more of the tasks you want it to be able to do.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760683821020/77046fc8-b7f3-4890-93be-5934ba4409c4.png" alt class="image--center mx-auto" /></p>
<p>We now have an agent who can both search the internet and use MCP servers using Strands. In many ways, it is this simple to create the backend for your own ChatGPT clone. It's becoming very trivial to enable this type of functionality based on an LLM.</p>
<p>But how can I share this with my colleagues and friends? Let's move on to deploying this.</p>
<h3 id="heading-deployment">Deployment</h3>
<p>At this point, we have a working agent that runs locally. You could stop here and integrate this agent directly into your existing applications—just instantiate it in your API endpoints, Lambda functions, or any other service where you need AI-powered automation</p>
<p>But since we are exploring AWS Bedrock AgentCore, let's check out their runtime, which is a way to enable you to run your agent very quickly on AWS.</p>
<p>We'll follow their SDK guide, which makes this very quick and easy: <a target="_blank" href="https://strandsagents.com/latest/documentation/docs/user-guide/deploy/deploy_to_bedrock_agentcore/#option-a-sdk-integration">https://strandsagents.com/latest/documentation/docs/user-guide/deploy/deploy_to_bedrock_agentcore/#option-a-sdk-integration</a></p>
<h4 id="heading-amazon-bedrock-agentcore-sdk">Amazon Bedrock AgentCore SDK</h4>
<p>Start by adding the SDK library <code>uv add bedrock-agentcore</code> and let's update the code based on their example:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> strands <span class="hljs-keyword">import</span> Agent
<span class="hljs-keyword">from</span> strands_tools.browser <span class="hljs-keyword">import</span> AgentCoreBrowser
<span class="hljs-keyword">from</span> mcp.client.streamable_http <span class="hljs-keyword">import</span> streamablehttp_client
<span class="hljs-keyword">from</span> strands.tools.mcp.mcp_client <span class="hljs-keyword">import</span> MCPClient
<span class="hljs-keyword">from</span> bedrock_agentcore <span class="hljs-keyword">import</span> BedrockAgentCoreApp

app = BedrockAgentCoreApp()
aws_knowledge_mcp = MCPClient(
    <span class="hljs-keyword">lambda</span>: streamablehttp_client(<span class="hljs-string">"https://knowledge-mcp.global.api.aws"</span>)
)

browser_tool = AgentCoreBrowser(region=<span class="hljs-string">"eu-west-1"</span>)

<span class="hljs-comment"># Create which function acts as the entrypoint for the agent</span>
<span class="hljs-meta">@app.entrypoint</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">agent_invocation</span>(<span class="hljs-params">payload</span>):</span>
    <span class="hljs-string">"""Handler for agent invocation"""</span>

    user_message = payload.get(
        <span class="hljs-string">"prompt"</span>, <span class="hljs-string">"No prompt found in input, please guide customer to create a JSON payload with prompt key"</span>,
    )
    <span class="hljs-comment"># Create an agent with MCP tools</span>
    <span class="hljs-keyword">with</span> aws_knowledge_mcp:
        <span class="hljs-comment"># Get the tools from the MCP server</span>
        aws_knowledge_tools = aws_knowledge_mcp.list_tools_sync()
        <span class="hljs-comment"># Combine the browser tool with the MCP tools</span>
        tools = [browser_tool.browser] + aws_knowledge_tools

        agent = Agent(tools=tools)
        stream = agent.stream_async(user_message)
        <span class="hljs-keyword">async</span> <span class="hljs-keyword">for</span> event <span class="hljs-keyword">in</span> stream:
            print(event)
            <span class="hljs-keyword">yield</span> (event)

<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    app.run()
</code></pre>
<p>You can now run this locally with <code>uv run</code> <a target="_blank" href="http://main.py"><code>main.py</code></a>, which will start a web server that hosts the agent behind an endpoint. When it's running, you can test it via, for example, curl using the default endpoints exposed by AgentCore.</p>
<pre><code class="lang-plaintext">curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"prompt": "Hello world!"}'
</code></pre>
<p>Since we opted to use the streaming response pattern, this would be a useful way of making it feel alive when, for example, building a chat interface.</p>
<h4 id="heading-prepare-for-deployment">Prepare for Deployment</h4>
<p>The AgentCore SDK comes with a CLI for easy deployment and testing without the need to configure any infrastructure.</p>
<p>Run: <code>uv run agentcore configure --entrypoint</code> <a target="_blank" href="http://main.py"><code>main.py</code></a>, which will run you through a few steps to create a <code>.bedrock_agentcore.yaml</code> document that includes the details related to your deployment.</p>
<p>To try out the agent via AgentCore locally, you can then run <code>uv run agentcore launch --local</code>. This will build the Docker image that will be used when deployed and make sure everything is working as expected. Once again, use curl to invoke <a target="_blank" href="http://localhost">localhost</a> to try out the endpoints—but this time via the Docker container which will be used for the actual deployment.</p>
<h4 id="heading-lets-deploy-it">Let's Deploy It</h4>
<p>When you are ready to actually deploy this to AWS, just run <code>uv run agentcore launch</code>. Given the specifications in <code>.bedrock_agentcore.yaml</code>, it will now deploy the required roles, push the Dockerfile to AWS, and make the instance available to be invoked from the AWS ecosystem.</p>
<p><strong>Deploying Updates:</strong> Added a new tool or updated your agent logic? Just run <code>uv run agentcore launch</code> again. The CLI handles the rebuild, pushes the new container image, and updates your deployed agent—no manual infrastructure changes needed. Your agent ARN stays the same, so any existing integrations continue working seamlessly.</p>
<p>You can find the full code at: <a target="_blank" href="https://github.com/elva-labs/strands-agentcore-blog-example">https://github.com/elva-labs/strands-agentcore-blog-example</a></p>
<h4 id="heading-but-how-do-i-actually-use-it">But How Do I Actually Use It?</h4>
<p>If you are familiar with the AWS ecosystem, especially services such as AWS Lambda, a deployed agent works very much the same way. Meaning that you can, via the regular AWS SDK, invoke the agent and use the response in any manner you see fit. Check out <a target="_blank" href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-invoke-agent.html">https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-invoke-agent.html</a> for details related to this.</p>
<p>Here is an example of how you could invoke your deployed agent using the boto3 framework:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> boto3
<span class="hljs-keyword">import</span> json

<span class="hljs-comment"># Initialize the Bedrock AgentCore client</span>
agent_core_client = boto3.client(<span class="hljs-string">'bedrock-agentcore'</span>)

<span class="hljs-comment"># Prepare the payload</span>
payload = json.dumps({<span class="hljs-string">"prompt"</span>: prompt}).encode()

<span class="hljs-comment"># Invoke the agent</span>
response = agent_core_client.invoke_agent_runtime(
    agentRuntimeArn=agent_arn,
    runtimeSessionId=session_id,
    payload=payload
)

<span class="hljs-comment"># Process and print the response</span>
<span class="hljs-keyword">if</span> <span class="hljs-string">"text/event-stream"</span> <span class="hljs-keyword">in</span> response.get(<span class="hljs-string">"contentType"</span>, <span class="hljs-string">""</span>):
    <span class="hljs-comment"># Handle streaming response</span>
    content = []
    <span class="hljs-keyword">for</span> line <span class="hljs-keyword">in</span> response[<span class="hljs-string">"response"</span>].iter_lines(chunk_size=<span class="hljs-number">10</span>):
        <span class="hljs-keyword">if</span> line:
            line = line.decode(<span class="hljs-string">"utf-8"</span>)
            <span class="hljs-keyword">if</span> line.startswith(<span class="hljs-string">"data: "</span>):
                line = line[<span class="hljs-number">6</span>:]
                print(line)
                content.append(line)
    print(<span class="hljs-string">"\nComplete response:"</span>, <span class="hljs-string">"\n"</span>.join(content))

<span class="hljs-keyword">elif</span> response.get(<span class="hljs-string">"contentType"</span>) == <span class="hljs-string">"application/json"</span>:
    <span class="hljs-comment"># Handle standard JSON response</span>
    content = []
    <span class="hljs-keyword">for</span> chunk <span class="hljs-keyword">in</span> response.get(<span class="hljs-string">"response"</span>, []):
        content.append(chunk.decode(<span class="hljs-string">'utf-8'</span>))
    print(json.loads(<span class="hljs-string">''</span>.join(content)))

<span class="hljs-keyword">else</span>:
    <span class="hljs-comment"># Print raw response for other content types</span>
    print(response)
</code></pre>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>AWS has, in my opinion, made the correct strategic move here toward a fully open-source agent framework that is very quick to get started with and has out-of-the-box support for the AgentCore runtime.</p>
<p>It also seems like AWS Bedrock AgentCore is their best bet for simpler ways of deploying infrastructure compared to the rest of AWS. As an experienced power user of AWS, I'm very comfortable with their ecosystem, as it enables me to use advanced patterns to solve almost any issue at any scale. But for many people entering the AI scene, much of this is unknown territory. By just having a simple CLI and a few commands to get running, they're actually becoming a challenger in the consumer space when it comes to agents, since the steep learning curve AWS traditionally comes with won't really work for this new wave of users.</p>
<p>Overall, I think the combination of AgentCore and Strands is a really nice concept that's getting close to becoming something I'd personally actually use in production. It still lacks some developer experience compared to more popular and larger frameworks, and is still a bit clunky in how you integrate it with your services. The AWS-specific flavor makes it a bit harder to integrate out of the box with, for example, the AI SDK, which powers a lot of chat frontends today.</p>
<p>All that said, I'm very excited by this step and looking forward to following the progress AWS will be making in this space over the next year.</p>
<h2 id="heading-whats-next">What's Next?</h2>
<p>Ready to take this further? Here are some next steps:</p>
<p><strong>Explore More MCP Servers:</strong></p>
<ul>
<li><p>Check out the <a target="_blank" href="https://github.com/modelcontextprotocol/servers">MCP Server Registry</a> for community tools</p>
</li>
<li><p>AWS Labs has several production-ready servers for different AWS services at <a target="_blank" href="https://github.com/awslabs/mcp">awslabs/mcp</a></p>
</li>
</ul>
<p><strong>Production Considerations:</strong></p>
<ul>
<li><p>Set up CloudWatch monitoring for your deployed agent via AgentCore Observability</p>
</li>
<li><p>Review the <a target="_blank" href="https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore-security.html">AgentCore security best practices</a></p>
</li>
<li><p>Remember: AgentCore pricing is consumption-based, so you only pay for what you use</p>
</li>
</ul>
<p>What's the one repetitive task in your workflow you'd automate first with an agent? I'd love to hear what you're building!</p>
<hr />
<p>If you enjoyed this post, want to know more about me, working at Elva, or just want to reach out, you can find me on <a target="_blank" href="https://www.linkedin.com/in/tedvardsson/">LinkedIn</a>.</p>
<hr />
<p><strong>Elva</strong> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future.</p>
]]></content:encoded></item><item><title><![CDATA[AWS AI Tooling: The Cheat Sheet Nobody Asked For (But Everyone Needs)]]></title><description><![CDATA[TL;DR: AWS offers numerous AI services, making it challenging to keep them all straight. This is your no-nonsense reference for actually figuring out which one solves your problem, without having to read a near-unlimited number of pages of documentat...]]></description><link>https://blog.elva-group.com/aws-ai-tooling-the-cheat-sheet-nobody-asked-for-but-everyone-needs</link><guid isPermaLink="true">https://blog.elva-group.com/aws-ai-tooling-the-cheat-sheet-nobody-asked-for-but-everyone-needs</guid><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Developer]]></category><dc:creator><![CDATA[Joel Roxell]]></dc:creator><pubDate>Thu, 09 Oct 2025 07:48:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759993024785/b58099b7-10ae-4fa3-92c7-0c5cac1c007a.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>TL;DR: AWS offers numerous AI services, making it challenging to keep them all straight. This is your no-nonsense reference for actually figuring out which one solves your problem, without having to read a near-unlimited number of pages of documentation first. Note that your system can be built using one or more of these AWS components.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>What are you trying to do?</td><td>Start here</td><td>Notes</td></tr>
</thead>
<tbody>
<tr>
<td>Add conversational AI fast</td><td><a target="_blank" href="https://aws.amazon.com/bedrock/">Bedrock</a></td><td>Foundation models, managed infrastructure</td></tr>
<tr>
<td>Build custom ML models</td><td><a target="_blank" href="https://aws.amazon.com/sagemaker/">SageMaker</a></td><td>When Bedrock's models don't fit your needs</td></tr>
<tr>
<td>Analyze images/video</td><td><a target="_blank" href="https://docs.aws.amazon.com/rekognition/latest/dg/what-is.html">Rekognition</a></td><td>Face detection, object recognition, and content moderation</td></tr>
<tr>
<td>Extract data from documents</td><td><a target="_blank" href="https://docs.aws.amazon.com/textract/latest/dg/what-is.html">Textract</a></td><td>Forms, tables, handwriting, beyond basic OCR</td></tr>
<tr>
<td>Convert speech to text</td><td><a target="_blank" href="http://aws.amazon.com/transcribe/">Transcribe</a></td><td>100+ languages, real-time or batch</td></tr>
<tr>
<td>Analyze text (sentiment, entities)</td><td><a target="_blank" href="https://docs.aws.amazon.com/comprehend/latest/dg/what-is.html">Comprehend</a></td><td>When regex isn't enough</td></tr>
<tr>
<td>Translate text</td><td><a target="_blank" href="https://aws.amazon.com/translate/">Translate</a></td><td>75+ languages, customizable</td></tr>
<tr>
<td>Convert text to speech</td><td><a target="_blank" href="https://aws.amazon.com/polly/">Polly</a></td><td>Multiple voices, sounds actually human</td></tr>
<tr>
<td>Build recommendation engines</td><td><a target="_blank" href="https://aws.amazon.com/pm/personalize">Personalize</a></td><td>Netflix-style "you might also like"</td></tr>
<tr>
<td>Forecast time-series data</td><td><a target="_blank" href="https://docs.aws.amazon.com/forecast/latest/dg/what-is-forecast.html">Forecast</a></td><td>Demand prediction, capacity planning</td></tr>
<tr>
<td>Make internal search actually work</td><td><a target="_blank" href="https://aws.amazon.com/pm/kendra/">Kendra</a></td><td>Natural language search across repositories</td></tr>
<tr>
<td>Connect AI to existing systems</td><td><a target="_blank" href="https://modelcontextprotocol.io/docs/getting-started/intro">MCP</a></td><td>The integration layer and use with any AI service above</td></tr>
</tbody>
</table>
</div><p><em>Note: This document will evolve as AWS releases new services and I discover more effective ways to explain (and utilize) the existing ones.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759931677866/cc41630c-9285-4570-a15b-5e3a7c56a9b5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-generative-ai">Generative AI</h2>
<p><strong>Bedrock - Your "Just Make It Work" Solution</strong></p>
<p>Remember when deploying ML models meant provisioning infrastructure, managing versions, and dedicating your weekends and paying tribute to &lt;insert-divinity&gt;, who might grant you success? Bedrock is the opposite of that. It provides plug-and-play access to foundation models via the SDK. Think of it as the AWS-ification of AI; they handle the hard parts, you handle the business logic.</p>
<h3 id="heading-the-stuff-you-actually-care-about">The stuff you actually care about</h3>
<p><strong>Bedrock AgentCore</strong> - Managing AI agents at enterprise scale without losing your mind. Covers most use cases, handles security, and won't wake you up at 03:00 due to deployment-related issues. If you're running AI for a big organization, this is your toolkit.</p>
<p><strong>Bedrock Guardrails</strong> - Use this when "move fast and break things" doesn't work and when your AI is customer-facing. This is your safety net for content moderation. Nobody wants their chatbot to become a PR disaster on a Friday afternoon.</p>
<p><strong>Cross-region inference</strong> - Routes requests based on current load. It's like having an intelligent load balancer that actually understands your models aren't just stateless functions. Your models become truly plug-and-play across regions. You can use this service when you want AI capabilities without needing to become an ML infrastructure expert, which is, let's be honest, most of the time.</p>
<h2 id="heading-machine-learning">Machine Learning</h2>
<p><strong>SageMaker - The Swiss Army Knife</strong></p>
<p>SageMaker is the answer to "what if we made machine learning... manageable?" It's comprehensive, fully managed, and eliminates most of the operational headaches.</p>
<p>The usual workflow:</p>
<p><strong>Build</strong> - Spin up a Jupyter notebook, load your data, and start experimenting. You know, the fun part.</p>
<p><strong>Train</strong> - Run training jobs with built-in algorithms or your custom code. SageMaker handles infrastructure scaling, so you can focus on whether your model actually works.</p>
<p><strong>Deploy</strong> - Push your model live with no adjustments required. Integration with the rest of AWS happens when you're ready, not when AWS decides it's time.</p>
<p>Use it when Bedrock's pre-trained models aren't enough and you need something tailored. It's more effort, but you get precisely what you want.</p>
<h2 id="heading-computer-vision-amp-language">Computer Vision &amp; Language</h2>
<p><strong>The "AI But Make It Useful" Services</strong></p>
<p>AWS offers a suite of services that are basically "we trained the models so you don't have to." They're surprisingly good, and you can implement them without a PhD.</p>
<h3 id="heading-the-lineup">The lineup</h3>
<p><strong>Rekognition</strong> - Deep learning for images and video. Detects faces, objects, text, and that weird thing in the corner of your security footage. It just works, and you don't need to understand convolutional neural networks to use it.</p>
<p><strong>Textract</strong> - OCR on steroids. Pulls text, handwriting, layout elements, and structured data from documents. Goes way beyond "here's some text" into "here's the actual data you wanted."</p>
<p><strong>Transcribe</strong> - Speech-to-text that handles 100+ languages and both real-time streaming and batch processing. Great for when you have audio and need text, without manually setting up inference pipelines.</p>
<p><strong>Comprehend</strong> - NLP service that finds entities, key phrases, language, sentiment, and other insights in text. Uses ML to understand documents so you don't have to write regex for the 47th time.</p>
<p><strong>Translate</strong> - Real-time translation using deep learning. Fast, affordable, and customizable. Beats the old "translate via Google Sheets" workaround most teams start with.</p>
<p><strong>Polly</strong> - Text-to-speech with actual personality. Uses deep learning to sound less robotic and more human. Perfect for accessibility features or voice interfaces that don't make users want to throw their devices.</p>
<p>Use one or more of these tools when you have a specific, well-defined problem (transcribe this, translate that, find entities in documents) and don't want to become a specialist in that domain.</p>
<h2 id="heading-bi-amp-recommendations">BI &amp; Recommendations</h2>
<p><strong>Personalize - Netflix-Style Recommendations</strong></p>
<p>Helps you build custom recommendation engines with real-time personalization and user segmentation. It's the same technology that powers the "customers who bought this also bought..." feature everywhere on the internet. Getting recommendations right is more challenging than it appears. Personalize handles the ML complexity so you can focus on the business logic of what to recommend.</p>
<p><strong>Forecast - Time-Series Predictions</strong></p>
<p>Statistical and ML algorithms for forecasting. Built on the same tech Amazon uses internally for demand prediction, which means it's battle-tested at scale. Use this when you need to predict future values based on historical data and don't want to spend hours developing a forecasting model.</p>
<p><strong>Kendra - Enterprise Search That Doesn't Suck</strong></p>
<p>Intelligent search using NLP and ML to help people find content across your organization's repositories. Think Google, but for your internal systems. Keeps employees from spending half their day searching for documents. Kendra searches actually work using natural language instead of boolean operators nobody remembers.</p>
<h2 id="heading-model-context-protocol-mcp">Model Context Protocol (MCP)</h2>
<p><strong>The Integration Layer You Didn't Know You Needed</strong></p>
<p>What is MCP?</p>
<p>MCP is the connective tissue between your LLM and your actual data. It lets AI access external data and trigger actions. AWS has built several MCP servers that provide deep access to AWS APIs, enabling natural language input and outputting AWS actions.</p>
<p>Why this matters: Most AI implementations fail not because the models are bad, but because they can't access the correct data. MCP solves the "last mile" problem of AI integration.</p>
<p><strong>Useful MCP Servers</strong></p>
<p>If you're working with AWS, these MCPs are worth exploring. At a minimum, check out the repo https://awslabs.github.io/mcp/.</p>
<p><strong>Documentation MCP</strong> - Real-time access to official AWS docs, API references, What's New posts, Getting Started guides, Builder Library, blog posts, and architectural references. No more having 30+ tabs open.</p>
<p><strong>Infrastructure &amp; Deployment</strong> - Build, deploy, and manage cloud infrastructure through conversation instead of clicking through consoles.</p>
<p><strong>AI &amp; Machine Learning</strong> - Enhance AI applications with knowledge retrieval and ML capabilities. No custom integration code required.</p>
<p><strong>Data &amp; Analytics</strong> - Work with databases, caching systems, and data processing through natural interfaces.</p>
<p>MCP servers turn your systems from "things you have to learn" into "things you can just ask." That's a big win.</p>
<h2 id="heading-the-strategy-view">The Strategy View</h2>
<p>The AWS catalog is overwhelming (probably by design). AWS builds tools for every use case, which means you need to know which tool fits the problem you’re trying to solve.</p>
<p>The decision tree:</p>
<ul>
<li><p>Need AI capabilities fast with minimal setup? → <strong>Bedrock</strong></p>
</li>
<li><p>Need custom models for specific problems? → <strong>SageMaker</strong></p>
</li>
<li><p>Have a well-defined AI task (transcribe, translate, etc.)? → <strong>Specialized services</strong> (Rekognition, Textract, etc.)</p>
</li>
<li><p>Do you need to connect AI to your existing systems? → <strong>MCP</strong></p>
</li>
</ul>
<hr />
<p><strong>Ready to Transform Your AI Infrastructure?</strong></p>
<p>At Elva, we help organizations move past AI PoC and MVP theater. Our team's expertise with the Model Context Protocol and AWS lets companies:</p>
<ul>
<li><p>Implement robust infrastructure for integrating Generative AI.</p>
</li>
<li><p>Transform legacy systems into AI-accessible resources without costly replacements.</p>
</li>
<li><p>Build once, deploy AI everywhere across your entire ecosystem.</p>
</li>
</ul>
<p>Make your business easy to talk to. <a target="_blank" href="https://claude.ai/chat/link">Contact Elva</a> to implement MCP and gain a competitive edge.</p>
<p><a target="_blank" href="https://claude.ai/chat/link">LinkedIn</a> / <a target="_blank" href="https://claude.ai/chat/link">Hashnode</a> / <a target="_blank" href="https://claude.ai/chat/link">Contact</a></p>
<hr />
<p><em>Note: This document will evolve as AWS releases new services and I discover more effective ways to explain (and utilize) the existing ones.</em></p>
]]></content:encoded></item><item><title><![CDATA[Robots Are Taking Our Jobs: Understanding AI Agents]]></title><description><![CDATA[TLDR: Agents are AI that don't stop at giving advice - they actually do the work. They try things, use tools, check results, and keep going until done. The gap between 'AI assistant' and 'AI colleague' is closing fast.
From Human Loop to AI Loop
You ...]]></description><link>https://blog.elva-group.com/robots-are-taking-our-jobs</link><guid isPermaLink="true">https://blog.elva-group.com/robots-are-taking-our-jobs</guid><category><![CDATA[voltagent]]></category><category><![CDATA[AI]]></category><category><![CDATA[AWS]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[mcp]]></category><dc:creator><![CDATA[Tobias Edvardsson]]></dc:creator><pubDate>Thu, 02 Oct 2025 11:01:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/R4WCbazrD1g/upload/cdea791f51fb134e4a24a481b848f327.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TLDR:</strong> Agents are AI that don't stop at giving advice - they actually do the work. They try things, use tools, check results, and keep going until done. The gap between 'AI assistant' and 'AI colleague' is closing fast.</p>
<h2 id="heading-from-human-loop-to-ai-loop">From Human Loop to AI Loop</h2>
<p>You know this dance: You need to schedule a meeting with your team to discuss the Q1 roadmap.</p>
<h3 id="heading-todays-reality"><strong>Today's reality</strong></h3>
<ol>
<li><p>Check your calendar for free slots</p>
</li>
<li><p>Message teammates: "When are you free next week?"</p>
</li>
<li><p>Wait for responses (refresh Slack obsessively)</p>
</li>
<li><p>Cross-reference everyone's availability</p>
</li>
<li><p>Find a room that's actually free</p>
</li>
<li><p>Send calendar invites</p>
</li>
<li><p>Realize someone can't make it</p>
</li>
<li><p>Start over</p>
</li>
</ol>
<p>Time: Anywhere from 20 minutes to 3 days, depending on how responsive your team is.</p>
<h3 id="heading-whats-coming">What's coming</h3>
<p>"Book a meeting with the team early next week when everyone's available."</p>
<p>Done. The agent checks calendars, finds the overlap, books the room, and sends invites. Time: 10 seconds.</p>
<p>The difference? You're not the one doing the loop anymore. The AI is.</p>
<h2 id="heading-what-is-an-agent-actually">What Is an Agent, Actually?</h2>
<p>When an agent tackles a task, it's running what's called an agentic loop. Don't let the jargon fool you - it's straightforward:</p>
<p><strong>Step 1: Reasoning.</strong> The agent breaks down what you asked for. "To find backend development experts, I need to search employee profiles and recent project work."</p>
<p><strong>Step 2: Action.</strong> It uses tools to get information. Query the employee database, check project repositories, and scan Slack channels.</p>
<p><strong>Step 3: Observation.</strong> It looks at what came back. "I found 12 people with backend development listed as a skill."</p>
<p><strong>Step 4: Validation.</strong> It checks if that's enough. "Did I answer the question completely? Do I need API design experience, too?"</p>
<p><strong>Step 5: Loop or Finish.</strong> If done, return results. If not, go back to Step 1 with new information.</p>
<p>A Regular LLM gives you a single answer and stops, while an agent keeps going until the job is actually finished.</p>
<p>When an agent tackles a task, it's running through this cycle over and over. The agent reasons about what to do next, takes an action using available tools, observes what came back, validates if it's done, and either finishes or loops again. This is why dedicated agents feel different - they're built to run longer, more complex tasks without giving up. While tools like ChatGPT and Claude are becoming increasingly agentic with their own tool use, purpose-built agents can handle workflows that span minutes or hours, not just seconds.</p>
<p>This is the first time LLMs can "do" instead of just "say." And yeah, that's a big deal.</p>
<h2 id="heading-why-this-changes-everything">Why This Changes Everything</h2>
<p>So why does this simple loop pattern matter so much?</p>
<p>Here's what makes agents different: they can use tools and MCP servers to actually do things. (Want the full story on context and MCP? Check out <a target="_blank" href="https://blog.elva-group.com/moving-past-the-ai-hype-introducing-mcp">Moving Past the AI Hype</a> and <a target="_blank" href="https://blog.elva-group.com/solving-ai-context-with-mcp-servers">Solving Context with MCP Servers</a>.)</p>
<p>The short version: agents don't wait for you to gather information. They access it themselves, remember what they've done, and keep working until the task is complete.</p>
<p><strong>Monday morning:</strong> "What meetings do I have this week and what prep do I need for each?" The agent checks your calendar, pulls relevant documents, summarizes action items from previous meetings, and gives you a briefing.</p>
<p><strong>Strategic reporting:</strong> "Create a report comparing our cloud costs versus our main competitor's public pricing." The agent pulls your AWS billing data, researches competitor pricing, analyzes the differences, and generates a formatted report with recommendations.</p>
<p><strong>Feature development:</strong> "Add dark mode to our dashboard using the latest React patterns." The agent reviews your current codebase, checks React 19 documentation, implements the feature following your project's conventions, and opens a PR with tests.</p>
<p>You're not just saving time. You're eliminating entire categories of work that used to require a human to be the glue between systems.</p>
<h2 id="heading-whats-available-right-now">What's Available Right Now</h2>
<p>Here's what the landscape looks like today:</p>
<p><strong>You're Already Using Agents</strong> (Probably)</p>
<ul>
<li><p>ChatGPT with plugins? That's an agent.</p>
</li>
<li><p>Claude with MCP servers? Agent.</p>
</li>
<li><p>GitHub Copilot? Specialized agent for code.</p>
</li>
</ul>
<p>They're purpose-built for specific tasks, but they're all running the same basic pattern.</p>
<p>Agents are popping up everywhere today, and it’s only the beginning. I really think we are heading into a new technical era where agents will replace much of our daily work, especially the repetitive, slow-going work that many of us spend hours daily doing at our desk jobs. Is it only the carpenters and plumbers who have safe jobs for now?</p>
<p>Make sure to take the opportunity to stay ahead of the curve and utilize the advantage agents currently give you, parallelize yourself, and create leverage in your knowledge by automating the skills and knowledge that make you valuable within your job.</p>
<h2 id="heading-ready-to-build-your-own">Ready to Build Your Own?</h2>
<p><strong>No-Code Agent Builders.</strong> If you're not a coder, platforms like <a target="_blank" href="https://n8n.io/">n8n</a> let you build agents visually. Connect your tools, define workflows, and let the agent handle the execution. It's surprisingly powerful for common business processes and has become one of the most popular low-code frameworks for agentic AI work.</p>
<p><strong>Developer Frameworks:</strong> If you write code, things get interesting fast. Frameworks like <a target="_blank" href="https://voltagent.dev/"><strong>VoltAgent</strong></a> (my personal favorite) let you build custom agents in minutes, not days. AWS is also pushing hard here with Agent Core Runtime and their Strands Python framework.</p>
<p>Building agents is a deep topic that deserves its own article - we'll cover that in detail soon.</p>
<p>The community is moving fast. What used to require a team of ML engineers now takes a developer an afternoon.</p>
<p><strong>Need help getting started?</strong> At <a target="_blank" href="https://elva-group.com/">Elva</a>, we help teams design and build custom agents that fit their specific workflows. Whether you're looking to automate internal processes or build customer-facing AI products, we'd love to chat about your use case.</p>
<p><em>For you developers out there, check out this repo with a quick, simple agent starter using</em> <a target="_blank" href="https://voltagent.dev"><em>VoltAgent</em></a><em>, ready to be deployed as an API on AWS using</em> <a target="_blank" href="https://sst.dev"><em>SST</em></a><em>:</em> <a target="_blank" href="https://github.com/elva-labs/voltagent-blog-start"><em>https://github.com/elva-labs/voltagent-blog-start</em></a></p>
<h2 id="heading-the-shift-is-here">The Shift Is Here</h2>
<p>I truly believe that things are changing, and they are changing fast. We're not talking about a gradual evolution - we're looking at a fundamental shift in how technology works.</p>
<p>Within a few years, the idea of manually moving data between systems will feel as outdated as using a fax machine. Specialized autonomous agents will handle the tedious work: booking meetings, comparing quotas against contracts, generating reports, monitoring systems, and flagging anomalies.</p>
<p>This is what makes the current state of LLM technology fundamentally different from previous AI hypes. We're not just getting better predictions or recommendations. We're creating software that can reason about tasks, use tools autonomously, and persist until the job is done.</p>
<p>The robots aren't coming for your job in the dramatic sci-fi sense. But the boring parts? The context-switching, the data gathering, the "glue work" that takes up half your day? Yeah, those are getting automated. Fast.</p>
<p>The question isn't whether this will happen. It's whether you're going to be building these agents or just watching while others do.</p>
<p>Time to start experimenting.</p>
<hr />
<p>If you enjoyed this post, want to know more about me, working at Elva, or just want to reach out, you can find me on <a target="_blank" href="https://www.linkedin.com/in/tedvardsson/"><strong>LinkedIn</strong></a>.</p>
<hr />
<p><a target="_blank" href="https://elva-group.com/">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Stop Copy-Pasting Into ChatGPT: How MCP Servers Actually Solve the Context Problem]]></title><description><![CDATA[TLDR: MCP servers let your AI agents directly access your company's data and tools. No more copy-paste, no more context switching. Just ask questions, get answers that actually know your business. Here's how to actually use them.
If you have not read...]]></description><link>https://blog.elva-group.com/solving-ai-context-with-mcp-servers</link><guid isPermaLink="true">https://blog.elva-group.com/solving-ai-context-with-mcp-servers</guid><category><![CDATA[AI]]></category><category><![CDATA[mcp]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[context engineering]]></category><dc:creator><![CDATA[Tobias Edvardsson]]></dc:creator><pubDate>Wed, 24 Sep 2025 09:37:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Ejcuhcdfwrs/upload/bb11f6754438b8e28080a39d907e1f3b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>TLDR:</strong> MCP servers let your AI agents directly access your company's data and tools. No more copy-paste, no more context switching. Just ask questions, get answers that actually know your business. Here's how to actually use them.</p>
<p><em>If you have not read our first article in this series, check out:</em> <a target="_blank" href="https://blog.elva-group.com/moving-past-the-ai-hype-introducing-mcp"><em>https://blog.elva-group.com/moving-past-the-ai-hype-introducing-mcp</em></a></p>
<h2 id="heading-the-copy-paste-marathon-we-all-know">The Copy-Paste Marathon We All Know</h2>
<p>Picture this: You need to know who in your 500-person company has experience with Kubernetes and AWS.</p>
<p>Old way: Open HR system, export to Excel, search LinkedIn, check Slack, compile a list, hope it's current. Time: 45 minutes.</p>
<p>New way: "Who in our company knows Kubernetes and has AWS experience?" Time: 5 seconds.</p>
<p>The difference? MCP servers - and here's exactly how to use them.</p>
<h2 id="heading-context-is-everything-why-your-ai-needs-direct-access">Context Is Everything: Why Your AI Needs Direct Access</h2>
<p>Here's the fundamental problem with AI today: it only knows what you tell it. Every conversation starts from zero. You're the human middleware, copying and pasting context that should already be obvious.</p>
<p>Everything about working with LLMs is moving toward what's becoming known as context engineering. Prepare the context for the question being asked, regardless of whether this is to produce an answer about personnel, create code, or write you an article about your favorite animal.</p>
<p>MCP (Model Context Protocol) servers solve this by creating secure bridges between AI agents and your actual tools. Your AI can directly:</p>
<ul>
<li><p>Query your employee database</p>
</li>
<li><p>Check project documentation</p>
</li>
<li><p>Pull analytics from your dashboards</p>
</li>
<li><p>Search through your knowledge base</p>
</li>
<li><p>Access your CRM data</p>
</li>
<li><p>Read from your project management tools</p>
</li>
</ul>
<p>Technically, MCP servers are JSON-RPC servers that expose your internal tools as functions AI agents can call. They provide a standardized way for AI to interact with any data source or API you want to expose - with built-in discovery, typing, and documentation. Once you connect a tool via MCP, any AI agent that supports the protocol can use it.</p>
<p>When AI already knows your employee skills, project history, and team structures, you can skip the copy-paste marathon and just ask: "Who should I talk to about Python performance issues in our data platform?" - turning hours of context-gathering into seconds of conversation.</p>
<h2 id="heading-local-vs-remote-mcp-servers-choose-your-approach">Local vs Remote MCP Servers: Choose Your Approach</h2>
<p>Before diving into setup, it's important to understand the two types of MCP servers:</p>
<p><strong>Local MCP Servers:</strong></p>
<ul>
<li><p>Execute directly on your machine as a process</p>
</li>
<li><p>Use stdio (standard input/output) for communication</p>
</li>
<li><p>Direct access to your local files and databases</p>
</li>
<li><p>Data never leaves your machine</p>
</li>
<li><p>Example: Node.js script accessing your local SQLite database</p>
</li>
</ul>
<p><strong>Remote MCP Servers:</strong></p>
<ul>
<li><p>Run on external infrastructure (cloud servers, managed services)</p>
</li>
<li><p>Communicate over HTTP/HTTPS protocols</p>
</li>
<li><p>No local execution needed - just network access</p>
</li>
<li><p>Maintained and scaled by the provider</p>
</li>
<li><p>Example: AWS Knowledge Server running on AWS infrastructure</p>
</li>
</ul>
<p>We're seeing a clear shift toward remote MCP servers, and for good reason. While today most people start with local servers running on their machines, the future is organizations hosting their own internal libraries of MCP servers - accessible to all employees but secured within company boundaries.</p>
<p>Imagine your company's private MCP ecosystem: HR data servers, documentation servers, analytics servers - all running in your cloud, accessible to your AI agents. Employees connect their AI tools to these internal servers, just as they do with the AWS Knowledge Server today.</p>
<h2 id="heading-so-how-do-i-start-using-this">So, How Do I Start Using This?</h2>
<p>Let's get practical with a real example. AWS Labs provides a remote MCP server that gives AI agents direct access to AWS documentation, API references, and best practices. Perfect for our AWS-focused teams. For these examples, we'll be using the desktop client from Anthropic, Claude Desktop.</p>
<h3 id="heading-step-1-install-claude-desktop">Step 1: Install Claude Desktop</h3>
<p>Download <a target="_blank" href="https://claude.ai/download">Claude Desktop</a> - it's Anthropic's native app with built-in MCP support. Other clients like Cursor work too, but we'll use Claude Desktop for this example.</p>
<h3 id="heading-step-2-configure-the-aws-knowledge-mcp-server">Step 2: Configure the AWS Knowledge MCP Server</h3>
<p>To add a remote server in Claude Desktop, they have a concept called Connectors. Go to Settings &gt; Connectors and Add custom connector.</p>
<p>Enter the URL to the MCP server as defined at <a target="_blank" href="https://awslabs.github.io/mcp/servers/aws-knowledge-mcp-server">https://awslabs.github.io/mcp/servers/aws-knowledge-mcp-server</a>: <a target="_blank" href="https://knowledge-mcp.global.api.aws"><code>https://knowledge-mcp.global.api.aws</code></a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758198989924/582d5738-8694-4968-8e16-0d12b5b2b82a.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-3-try-it-out">Step 3: Try it out</h3>
<p>After restarting, try asking AWS-specific questions:</p>
<ul>
<li><p>"What's the best practice for setting up multi-account AWS organizations?"</p>
</li>
<li><p>"Show me how to configure S3 bucket policies for cross-account access."</p>
</li>
</ul>
<p>Now the magic happens. When your AI identifies a question where it could benefit from using an MCP server to get you a better answer, it will use the MCP server, query it for more information based on what you asked, and then return an answer.</p>
<p>This approach gives you an enhanced answer that includes up-to-date and question-specific data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758199012456/86639345-b661-4087-80a1-65cfc46a89e2.png" alt class="image--center mx-auto" /></p>
<p><em>If you're a developer like many of us are, creating your MCP server is very simple. We've created a sample repo with a local test MCP that you can get started with to build your own server on our GitHub. Check out the repo at:</em> <a target="_blank" href="https://github.com/elva-labs/mcp-blog-demo">https://github.com/elva-labs/mcp-blog-demo</a></p>
<h2 id="heading-ready-to-enrich-your-ai-workflows-with-your-own-data">Ready to Enrich Your AI Workflows With Your Own Data?</h2>
<p>Whether you're looking to connect your internal databases, documentation systems, or analytics platforms, the path forward is clear: give your AI the context it needs to actually help you.</p>
<p>Need help implementing MCP servers in your organization or want to discuss your use case? Don't hesitate to reach out - we're helping teams navigate this transition every day and would love to hear about your challenges.</p>
<hr />
<p>If you enjoyed this post, want to know more about me, working at Elva, or just want to reach out, you can find me on <a target="_blank" href="https://www.linkedin.com/in/tedvardsson/"><strong>LinkedIn</strong></a>.</p>
<hr />
<p><a target="_blank" href="https://elva-group.com">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Moving Past the AI Hype (Introducing MCP)]]></title><description><![CDATA[In this post, I’ll cut through the noise surrounding Generative AI and the Model Context Protocol (MCP), explain the practical changes on the horizon, and show you why understanding these topics matters if you want to stay relevant in the industry.
W...]]></description><link>https://blog.elva-group.com/moving-past-the-ai-hype-introducing-mcp</link><guid isPermaLink="true">https://blog.elva-group.com/moving-past-the-ai-hype-introducing-mcp</guid><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[mcp]]></category><category><![CDATA[mcp server]]></category><category><![CDATA[MCP Client]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Joel Roxell]]></dc:creator><pubDate>Wed, 17 Sep 2025 06:24:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758030061982/94406b58-4386-4ba6-aba1-054733fe73c9.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, I’ll cut through the noise surrounding Generative AI and the Model Context Protocol (MCP), explain the practical changes on the horizon, and show you why understanding these topics matters if you want to stay relevant in the industry.</p>
<p>With Generative AI dominating headlines, it's easy to feel overwhelmed by bold claims of job replacement and rapid change. I understand this uncertainty because I share it. While you hear projections about programmers being replaced by AI in a matter of months, the real story is that our profession is changing, and what that means for you right now. In this post, I’ll share insights and actionable steps to leverage and adapt to the technologies shaping our future.</p>
<p>Follow us on <a target="_blank" href="https://www.linkedin.com/company/elva-group/">LinkedIn</a> / <a target="_blank" href="https://blog.elva-group.com/">Hashnode</a> / <a target="_blank" href="https://elva-group.com/contact/">Contact</a>.</p>
<p>We’re launching a new series on AI &amp; MCPs, exploring how you can adapt to stay ahead of your competitors and unlock new experiences for your customers. In the coming weeks, we’ll introduce practical examples on how to create and use both AI and MCP in the AWS cloud.</p>
<h2 id="heading-the-expensive-ai-experiment">The Expensive AI Experiment</h2>
<p>Today's hype in AI investment mirrors the early days of the internet, where substantial spending often lacked focus on tangible outcomes. Senior leadership is increasingly demanding a direct connection between AI initiatives and strategic deliverables, without a clear and/or reasonable goal.</p>
<p>A <a target="_blank" href="https://tech.co/news/mit-enterprise-ai-pilots-fail-revenues">study</a> conducted at MIT found that only 5% of AI pilots significantly boost revenue, with most showing little to no effect on profits. Similarly, <a target="_blank" href="https://observer.com/2025/06/mckinsey-study-business-ai-productivity/">McKinsey reports</a> that over 80% of companies see no clear impact on earnings from generative AI. It may be time to acknowledge the red flags.</p>
<p>From our experience, these failures often occur because teams and projects are siloed, in addition to a failing integration strategy. Running a few proof-of-concept projects in isolation will increase costs across the company. Isolated solutions can't share information or build the operational context an AI requires, so they end up providing little value to you or your customers.</p>
<p>Enterprise systems hold decades of refined business logic, data, and tested processes. Your ERP system covers supply chain complexity and your CRM knows your customers. Companies have invested a significant amount of effort into these systems and now rely on them to maintain their competitive edge. The problem is that these essential systems were not designed for natural conversation. They require strict commands instead of everyday language.</p>
<p>In effect, the systems with the most valuable business information are the most challenging for AI to access. Companies face a choice: either build costly middleware for each new integration, or limit AI to new, context-free applications. The second option often results in weak AI projects that people are reluctant to use.</p>
<h2 id="heading-the-overlooked-infrastructure-crisis">The Overlooked Infrastructure Crisis</h2>
<p>Despite the excitement and investment, most AI pilot programs fail to produce lasting results. <a target="_blank" href="https://www.gartner.com/en/newsroom/press-releases/2024-07-29-gartner-predicts-30-percent-of-generative-ai-projects-will-be-abandoned-after-proof-of-concept-by-end-of-2025">Gartner</a> says nearly a third of generative AI projects are quickly abandoned. The reason is not that models are too complex, but that they aren't integrated into the core business systems.</p>
<p>These challenges demonstrate that the real obstacle to successful AI is not the complexity of the models, but rather the lack of integration with the core business. The Model Context Protocol, or MCP, addresses this by embedding AI directly into existing business systems. It offers a standard way to connect everything, making integration easier and helping AI deliver real, lasting value.</p>
<p>Instead of treating AI as something extra, companies need to focus on integration as the main challenge. By building infrastructure that makes AI easy to use as a core business tool, not just in isolated pilot projects, organizations can unlock long-term value.</p>
<h2 id="heading-the-missing-infrastructure-layer-most-overlooks-model-context-protocol">The Missing Infrastructure Layer Most Overlooks (Model Context Protocol )</h2>
<p>MCP serves as the crucial link between AI and business software, resolving longstanding integration challenges. Standardization allows access across diverse systems while maintaining security. This makes conversational AI broadly usable and enables seamless integration with both legacy and modern core systems.</p>
<p>Many people focus on bigger AI models, but the real benefit comes from better integration. The Model Context Protocol offers three main advantages. Firstly, it securely connects AI to business systems at scale, thereby reducing the need for custom integration. Next, it enables AI to access essential business data using standardized methods. Finally, you can update or swap the AI model without modifying MCP or the API, ensuring your setup is future-ready.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758010351208/2ace790b-b8f1-4ea2-ba59-ba160a9bdea4.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-nobody-wants-to-learn-your-software"><strong>Nobody Wants to Learn Your Software</strong></h2>
<p>Most people don't want to spend their time clicking through menus or filling out forms. <a target="_blank" href="https://www.cnbc.com/2022/04/06/people-spend-more-than-half-of-the-day-on-busy-work-says-asana-survey.html">Asana reports</a> that employees spend a lot of time on repetitive administrative tasks. These hours could be used to solve real problems.</p>
<p>Most daily tasks are straightforward, such as finding a number, checking a status, or approving a request. Instead of working through complicated software, it would be easier to ask for what you need. With AI powered by MCP and in turn your core systems, routine work can be handled automatically, allowing people to focus on what matters most.</p>
<p>However, this doesn’t just apply to simple tasks. Imagine handling expense approvals, budget plans, or performance reviews through simple discussion. AI can orchestrate the details and keep everything on track, letting you focus on the big picture.</p>
<p>MCP enables the use of a single, simple conversational interface, such as chat or voice, for all your systems. This enhances customer experiences and facilitates the development of new business solutions. Integration becomes easier, and once you have MCP in place, any AI can connect. This 'build once, use everywhere' method reduces costs and complexity, making it affordable to grow your use of AI.</p>
<h2 id="heading-the-window-for-a-head-start-is-closing">The Window for a Head Start is Closing</h2>
<p>Currently, the adoption of AI shares many similarities with the dawn of the internet. At that time, companies that built strong online systems stood out and stayed ahead for years. Today, there is a similar chance to be among the first to create an AI infrastructure. Companies that move quickly will be ready for future advances in AI, while others may find it hard to keep up or fall short.</p>
<p>As more companies realize the value of easier integration and AI, MCP is becoming the standard. Leaders need to decide whether to continue spending on expensive, isolated AI projects or to invest in infrastructure that enables AI to deliver value at scale.</p>
<p>Choosing the proper infrastructure is almost always more cost-effective and impactful over time than funding scattered initiatives. Additionally, AI will only succeed if it is fully integrated into the core business infrastructure. Companies that make this integration a priority, rather than treating AI as an add-on, will move ahead of their competitors as conversation-based systems become the norm.</p>
<p>The era of AI pilot projects and struggling AI startups is coming to an end. Now is the time to invest in robust AI infrastructure and prepare your business for the next decade.</p>
<p><strong>Ready to Transform Your AI Infrastructure?</strong></p>
<p>At Elva, we help organizations move past AI PoC and MVP.  Our team’s expertise with the Model Context Protocol and AWS allows companies to:</p>
<ul>
<li><p>Implement a robust infrastructure that is suitable for integrating Generative AI.</p>
</li>
<li><p>Transform legacy systems into AI-accessible resources without costly replacements.</p>
</li>
<li><p>Build once, deploy AI everywhere across your entire ecosystem.</p>
</li>
</ul>
<p>Make your business easy to talk to. Contact Elva now to implement MCP and gain a competitive edge.</p>
<p><a target="_blank" href="https://www.linkedin.com/company/elva-group/">LinkedIn</a> / <a target="_blank" href="https://blog.elva-group.com/">Hashnode</a> / <a target="_blank" href="https://elva-group.com/contact/">Contact</a></p>
]]></content:encoded></item><item><title><![CDATA[Radical Simplicity in Cloud Architecture]]></title><description><![CDATA[For a long time, both professionally with clients in my consulting work and in the AWS community, I've been evangelizing the "serverless" paradigm shift. To me, it's crystal clear that you should focus on building what differentiates your business, a...]]></description><link>https://blog.elva-group.com/radical-simplicity-in-cloud-architecture</link><guid isPermaLink="true">https://blog.elva-group.com/radical-simplicity-in-cloud-architecture</guid><category><![CDATA[AWS]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[serverless]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Sebastian Bille]]></dc:creator><pubDate>Tue, 04 Mar 2025 07:18:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741072434107/52cc2a7c-d04c-4549-b9c3-3f1c1d215655.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For a long time, both professionally with clients in my consulting work and in the AWS community, I've been evangelizing the "serverless" paradigm shift. To me, it's crystal clear that you should focus on building what differentiates your business, and not on managing infrastructure or solving problems that are already solved by others. The term "serverless" may have been watered down by marketing in recent years, but the core idea of utilizing managed services and focusing on business logic remains.</p>
<p>People argue about cold starts, vendor lock-in, or learning curves, and while they are valid concerns, I believe that the benefits of serverless far outweigh the drawbacks - especially when you consider the total cost of ownership over the lifetime of a project. But I'm not here to argue about these concerns today, I'm here to talk about something else.</p>
<blockquote>
<p>Luc van Donkersgoed covers some of these concerns and others in my favorite blog post of all time <a target="_blank" href="https://lucvandonkersgoed.com/2023/10/13/if-the-shoulders-of-giants-are-offered-youd-do-well-to-stand-on-them/">here</a>. Go ahead and read it but pinky promise you'll come back!</p>
</blockquote>
<p>Today I want to talk about an aspect of serverless that I think is often overlooked: the developer experience and the unexpected use cases of "boring" technology that emerge when it's exposed in the form of a radically simple to consume and configure managed service.</p>
<h2 id="heading-your-personal-production-environment">Your personal production environment</h2>
<p>Do you know how many lines of code or clicks in the console you need to set up a DynamoDB table? An S3 Bucket? A Lambda function?</p>
<p>The answer is about 2.</p>
<p>This sets up a service that will scale to virtually any load, without further configuration. Perhaps more importantly though, it also scales to 0, and you literally pay nothing when it's not being used.</p>
<p>People tend to focus on the scaling up part, not realizing what the scaling down part actually entails. It's not just about saving money, it's about the freedom to experiment and build things in a way that you wouldn't have otherwise.</p>
<p>Want to have a personal carbon-copy production environment? Sure, hit deploy and have it ready in 30 seconds and it will always be free.</p>
<p>When you merge code to main, you can be absolutely certain that it will work exactly the same in production as it did during development, because you've been testing in "production" all along.</p>
<p>Want to spin up an ephemeral environment on every pull request, run integration tests against it, and tear it down when the pull request is merged? Sure, it's literally a few lines of code and it will cost you nothing.</p>
<p>And once these resources are deployed, you will never have to worry about them again. They will scale up and down to meet demand, there is nothing to patch or maintain, and the availability and security of the services will be the responsibility of one of the most experienced and capable companies in running cloud infrastructure in the world - so that you can spend your time where it actually sets you apart.</p>
<p>Gone are the days of sharing a database in the development environments to keep cost down. Gone are the days tinkering with scaling groups and load balancers. Gone are the days of waking up in the middle of the night to patch a vulnerability.</p>
<p>When the log4net vulnerability was announced, I was working in the platform services team at a major automotive company running hundreds of workloads in AWS. Do you know what we did to patch it? Not a damn thing.</p>
<p>The same company at one point ran a superbowl ad and traffic was naturally expected to explode. Do you know what we did to prepare for it? Not a damn thing - we sat back to enjoy the show.</p>
<p>The idea that serverless compute is more expensive than traditional alternatives is less and less true the more you look at the full picture and lifecycle of your applications. Is there a break-even point where it becomes more cost-efficient to move away from it? Sure. But I'd argue that that point is way (way) higher than you'd think if you consider the <strong>total</strong> cost of ownership. That is, not only the compute layer itself, but also:</p>
<ul>
<li><p>development environments and feedback loops</p>
</li>
<li><p>labor cost of maintenance, configuration management</p>
</li>
<li><p>overprovisioning to meet demand, or underprovisioning and risking business objectives, and the labor of finding the right balance</p>
</li>
<li><p>the opportunity cost of not spending time on what actually matters to the business</p>
</li>
</ul>
<p>This isn't meant to be a blanket statement claiming that serverless is always cheaper. But it is cheaper in more cases than you might think at first glance. It's also not a binary choice. Employing a serverless-first strategy is wise, where you default to “more serverless” and move further back in the abstraction spectrum as needed, with parts of your application (or application landscape) being serverful where it's more fitting.</p>
<h2 id="heading-unexpected-use-cases-from-radically-simple-services">Unexpected Use Cases from Radically Simple Services</h2>
<p>Traditionally, setting up a globally distributed, highly available database required intricate configurations: manual sharding, complex replication setups, and constant vigilance to maintain consistency across regions. This complexity often deterred teams from pursuing global distribution, limiting application performance and resilience.​</p>
<p>Enter services like DSQL and DynamoDB Global Tables. With the latter as an example, you can select the regions where you need your data replicated, and DynamoDB handles the rest, eliminating the complexity and operational burden of deploying and managing multi-region replication. This means that updates performed on a replica table in one region are automatically replicated to the replica tables in other regions, ensuring low-latency access for users worldwide. By abstracting away traditional pain points, fully managed services make many problems that were previously very hard and costly to solve completely disappear.</p>
<p>And this pattern repeats itself across many other services and problem domains. Whether it's APIs with API Gateway, Cron jobs and scheduled tasks with EventBridge or workflow orchestration with StepFunctions - what once required dedicated infrastructure, maintenance, and tuning is now an API call away.</p>
<p>When you have managed services that are so simple to consume and configure, you start to see unexpected use cases for technology that you wouldn't have considered before.</p>
<p>My friend and colleague <a target="_blank" href="https://www.linkedin.com/in/eliasbrange/">Elias Brange</a> has two perfect examples of this.</p>
<p>The first one is covered in his blog post "<a target="_blank" href="https://www.eliasbrange.dev/posts/eventbridge-testing-with-appsync-events/">Test Event-Driven Architectures with EventBridge and AppSync Events</a>" where he explores the idea of using AppSync Events (a managed Websockets API) to test event-driven architectures end-to-end. Spinning up a websockets API as part of your test suite and streaming events through it to test your system's behavior is a brilliant idea, and it's a fantastic solution to a problem that's hard to solve with traditional tools.</p>
<p>The other one is about <a target="_blank" href="https://www.eliasbrange.dev/posts/lambda-wiremock-stubr-extension/">spinning up a Wiremock service as a Lambda Layer</a> to mock external services during early development or as part of integration tests without the need to change any application code.</p>
<p>While it's obviously not a new idea to mock external services, the simplicity of setting up a Wiremock service in a few lines of code, deploying it as a "sidecar" to your dev stack, and not having to pay a dime for it is, honestly, a game changer.</p>
<p>If you talk to developers who've been working with serverless for a while, you'll find that it's not uncommon for many of them to have tricks like this up their sleeve.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>When you really start to lean into the serverless paradigm shift, you realize that perhaps the biggest benefit isn't just avoiding infrastructure management or infinite scalability. It's the ability to focus entirely on solving business problems instead of wrangling things that "should just work. It may even unlock new ways of thinking about how you can use technology to solve problems that you wouldn't have considered before, and in, dare I say, the vast majority of cases, it will be cheaper in the long run.</p>
<hr />
<p>Hi there, I'm Sebastian Bille! If you enjoyed this post or just want a constant feed of memes, AWS &amp; serverless talk, and the occasional new blog post, make sure to follow me on 𝕏 at <a target="_blank" href="https://twitter.com/TastefulElk"><strong>@TastefulElk</strong></a> or on <a target="_blank" href="https://www.linkedin.com/in/sebastianbille/"><strong>LinkedIn</strong></a> 👋</p>
<hr />
<p><a target="_blank" href="https://elva-group.com/"><strong>Elva</strong></a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[The honeymoon is over]]></title><description><![CDATA[So our first year as a company went well. Great actually. Fantastic numbers, high spirits, and an everlasting startup feeling. That all changed early in year two.

Act and react
I already wrote some words in my “first-year review” regarding the then ...]]></description><link>https://blog.elva-group.com/the-honeymoon-is-over</link><guid isPermaLink="true">https://blog.elva-group.com/the-honeymoon-is-over</guid><category><![CDATA[Company]]></category><category><![CDATA[Recession]]></category><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[business]]></category><category><![CDATA[Business and Finance ]]></category><dc:creator><![CDATA[Andreas Persson]]></dc:creator><pubDate>Mon, 16 Dec 2024 11:25:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/vnpTRdmtQ30/upload/cf1e78709146115992d2aa609dd49c2b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So our first year as a company went well. Great actually. Fantastic numbers, high spirits, and an everlasting startup feeling. That all changed early in year two.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734295879722/37e07d6b-7e8f-4db1-ba60-55fb1accc22d.png" alt="Down and down we go" class="image--center mx-auto" /></p>
<h2 id="heading-act-and-react">Act and react</h2>
<p>I already wrote some words in my “<a target="_blank" href="https://blog.elva-group.com/daddy-look-what-i-made">first-year review</a>” regarding the then ongoing recession. If we knew then what we know now, right?… This year we saw early signs at our clients as well as in the general public market that the uncertain situation in the world was taking its toll and that the situation would spiral further. Thanks to the senior experience within the group we acted quickly and precisely. Working actively with forecasting and keeping a tight feedback loop we managed to take actions, on a group level as well as an individual company level, cushioning the impact of inevitable customer cutbacks and losses. We developed a contingency plan identifying a list of actions to improve the outcome (on a monetary level). A list growing in severity and level of unpleasantness. The two most extreme being bringing on external capital and laying off colleagues. Our focus shifted from growth to profitability, cutting unnecessary costs and “nonprofit driving activities”. An important thing for us was to have full transparency and regular communication amongst each other. It wasn't fun, but everyone agreed on the precautions and understood the seriousness of the situation.</p>
<p>Looking back there were especially two months that I will remember. I will not remember them fondly mind you but I will take with me into the continued journey. During this period I experienced changes in my physical wellbeing. Most noticeable discomfort in the chest, trouble sleeping, and general fatigue. All this stemmed from the utter disgust I felt about even thinking of layoffs and knowing how we all suffered. Lingering in not knowing and to a large extent not having any power over the situation. Thankfully we managed to turn this around rather quickly and we could, with quite high certainty, say that we would close the year on black numbers. The relief was imminent.</p>
<h2 id="heading-is-it-really-worth-it">Is it really worth it?</h2>
<p>I’ve gotten the question <em>“Is it really worth it?”</em> a few times during the year. And not to take lightly on the health aspects of it all, but YES. It absolutely is worth it. I thoroughly enjoy my day-to-day work. I love my client. I love the interaction with brokers and colleagues, trying to achieve a common goal and doing it for myself, as well as for them. I’ve learned a lot during this year; regarding myself, my colleagues, and perhaps even more about running a business. I believe the people of Elva have become tighter with each other and more prepared than before. In the end, I’d say we are a better company and group for it. I don’t intend to rest on my laurels once things stabilise. I’ve come to terms with what bad stress is to me. When to avoid it and when to listen to my body. I’ve also come to appreciate and somewhat thrive during periods of high workload, making me feel appreciated and with a sense of accomplishment.</p>
<h2 id="heading-yes-because">Yes, because:</h2>
<p>In hindsight, we’ve accomplished quite a lot as a group.</p>
<ul>
<li><p>Grew in numbers despite an extremely tough market, and not only in our existing locations. We launched Malmö AND Stockholm.</p>
</li>
<li><p>We lost two clients; but gained five.</p>
</li>
<li><p>We finally got an AWS User Group and Community in Örebro.</p>
</li>
<li><p>Won the various AWS prospecting competitions we set out to participate in</p>
</li>
<li><p>We lost a year; but gained experience that will benefit us for years to come.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734342122456/c776c647-2dac-4280-9124-275827acd42f.png?width=600" alt="Check, check, check" class="image--center mx-auto" /></p>
<p>The first year was amazing. The second one not so much. I hope in year three we can at least find a middle ground and some stability. No doubt things are still tough; at least now we have built both resilience and processes to handle it.</p>
<hr />
<p>If you enjoyed this post, want to know more about me, working at Elva or just want to reach out you can find me on <a target="_blank" href="https://www.linkedin.com/in/andreas-persson-1a38a16a/"><strong>LinkedIn</strong></a>.  </p>
<hr />
<p><a target="_blank" href="https://elva-group.com/"><strong>Elva</strong></a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[How I built a Multiplayer App in 3 days]]></title><description><![CDATA[I thought it was time to create a follow up of my previous post Get your idea deployed to prod already. In the post I detail how you can quickly bootstrap an app and get it deployed and ready in no-time. This is a loose retelling of how I followed my...]]></description><link>https://blog.elva-group.com/how-i-built-a-multiplayer-app-in-3-days</link><guid isPermaLink="true">https://blog.elva-group.com/how-i-built-a-multiplayer-app-in-3-days</guid><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[React]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[observability]]></category><category><![CDATA[sst]]></category><dc:creator><![CDATA[Alvin Johansson]]></dc:creator><pubDate>Tue, 14 May 2024 06:35:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1715602391943/6595793b-91a8-480a-b202-b83cc26b5365.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I thought it was time to create a follow up of my previous post <a target="_blank" href="https://blog.elva-group.com/deploy-to-prod-already">Get your idea deployed to prod already</a>. In the post I detail how you can quickly bootstrap an app and get it deployed and ready in no-time. This is a loose retelling of how I followed my own advice and got an idea to prod.</p>
<h2 id="heading-the-idea">The idea</h2>
<p>Just before I went to sleep last Sunday I thought it would be fun to create an app for my friends and I where we can rate the Eurovision Song Contest participants live together. The idea was to have everyones votes being tallied up live as we make adjustments to the ratings. This led me into looking at <a target="_blank" href="https://replicache.dev/">Replicache</a> as the solution. I've been interested in trying it out for a while now and this was the perfect project for it. I quickly threw together a sketch on <a target="_blank" href="https://excalidraw.com/">Excalidraw</a> so I would have something to go on for tomorrow.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715594730994/b84609b1-358c-402d-9375-29fd1fb1e9c3.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-building">Building</h2>
<p>As the Monday began I started reading documentation and looking into the tech I wanted to use. For most of the stack I went with things I know. But for the multiplayer part I had to read up and understand the core concepts behind Replicache. While reading I came across their hosted service <a target="_blank" href="https://reflect.net/">Reflect</a>. This was a perfect fit as time was of the essence and I would not have to build my own Replicache backend! It's completely free for 1000 user hours every month. As this project is supposed to be used during the 5 hours of Eurovision every year we can calculate the maximum amount of users we can have online for the full duration by simple division.</p>
<p>$$\frac{1000hu}{5h} = 200u$$</p><p>200 users? We're totally good.</p>
<h3 id="heading-bootstrapping-the-project">Bootstrapping the project</h3>
<p>For the project bootstrapping I chose to base my project on Reflect's own React template. This decision came from the idea that I wanted to play around with it a bit first and get a proper understanding of it before I try to implement my idea. While playing around with the template I made new mutators and subscribers and got a feeling of how the service works. Great start!</p>
<h3 id="heading-skeleton-structure">Skeleton structure</h3>
<p>The next step was setting up basic React and HTML elements to be the skeleton of my idea. I got all the fields I wanted up and a general structure of the main feature. Key here is not to put too much time into styling. I mostly used the css styles already included in the scaffolded project and added just a little bit for the rounded borders and layout to be possible.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715590183950/dc4546a8-2154-4494-a090-df05bf565c07.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-implementing-the-main-feature">Implementing the main feature</h3>
<p>Now with the basic ui elements in place we can start implementing the features we care about. For me it was getting live updates when someone changes a rating. At first I built it without taking into consideration what song was being rated just to get something up. and running. And when I got that working I updated the data structure to handle the mutations based on id's and boom the main part of the application was working! I could now switch between different songs and add individual ratings to them while also seeing the updates in real time from another browser!</p>
<h3 id="heading-adding-the-other-bare-necessities">Adding the other bare necessities</h3>
<p>Now that the main feature had been implemented I had two things left to do in regards to the bare necessities. I wanted some sort of "login" page and a scoreboard.</p>
<h4 id="heading-login-page">"Login" page</h4>
<p>The login page would serve as a way for the user to identify themselves with a name and then join a specific room ID. I purposefully made this super simple, letting users choose whatever room ID they want and Reflect would handle creating the room gracefully with the users not noticing anything at all. For another user to join the same room they simply had to type the same room ID. This is possible since Reflect is using <a target="_blank" href="https://developers.cloudflare.com/durable-objects/">CloudFlares Durable Objects</a> in the background, serverlessly handling the client states for me and I don't have to be worried about any cost as I'm within the limits of Reflect's free plan.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715592262303/63d382e5-7afd-4729-9ab2-a0ee5179b64f.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-scoreboard">Scoreboard</h4>
<p>The scoreboard should show the aggregate total of all the scores from all the users, ranked after either order in the show or the max average. This could surely be built in a more sophisticated way with a table where you could sort based on any attribute but I kept it simple. I render the list based on how many contributions have been rated and calculate the average for each then sort it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715592373948/759d5714-39d7-411b-bd9b-91677c03d3be.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-styling">Styling</h3>
<p>All the functionality I intended is in place now! Time to make it look at least a little better. As I am not the best css:er nor very imaginative I gave a lot of the stylistic choices to ChatGPT. In essence I gave it the layout of the page with the current styles I had in place and then asked it to "pimp my ride"-it in the style of Eurovision. Color wise that seems to mean "add gradients". This works well enough for me!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715594921258/35959d21-1826-4dec-aa29-819c98cfdb81.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715595197752/a2324ede-7c3b-46b0-a946-efbbb76417e7.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-deploying">Deploying</h3>
<p>I was happy with this! Everything seems to work well while developing and it seems ready enough. First I created a production environment for my Reflect service by running <code>npx reflect publish --app lagom-euro</code> and added the generated url to my .env. Then I ran <code>sst init</code> to add <a target="_blank" href="https://ion.sst.dev/">sst ion</a> to the project. I added a StaticSite resource and ran a deployment to my prod AWS account. <code>sst deploy --stage production</code>. A production environment was now up and running!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715595850686/fbcc5b48-d919-44d2-a8e7-46d3aebe4853.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-testing-and-fixing">Testing and fixing</h3>
<p>The site was up and available via CloudFront. I started to show people and tested it with a few devices. Found some unintended behaviors and things that could be clarified. I added som help text and a button to show and hide the help.</p>
<h3 id="heading-domain">Domain</h3>
<p>Finally I bought a domain for it. I continue to name my projects after my favorite Swedish word "Lagom" which translates to "Just right". I attached the domain to my sst resource and boom the site was now available from <a target="_blank" href="https://lagomeurovision.com/">lagomeurovision.com</a>!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715596507849/7e929fd4-bdb1-419e-bdd8-b17909442ce0.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-observability">Observability</h3>
<p>This is a bit late in the game I admit, but better late than never! I figured it could be cool to know how many visitors and rooms I have on this small little project. I went ahead and created an environment for this project on my <a target="_blank" href="https://baselime.io/">Baselime</a> account. Baselime is a serverless observability service, recently acquired by CloudFlare! I've used Baselime in a few of my smaller projects now and it's super nice to use. For this project I installed their React SDK and added a custom event that emits when a room is joined.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715597185796/58559720-0cba-4d00-b8c0-1585170fb7fb.png" alt class="image--center mx-auto" /></p>
<p>I then set up a dashboard where I'd have an overview of the page stats. During the night I had 8 unique rooms and people joined a room 29 times. Very reasonably the site was mostly used on phones with the iPhone hogging the spotlight at 21 counts and Android following on 9. Stats are cool.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1715597372852/83db9b32-6440-4ea1-890d-df80834416ad.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-final-take-aways">Final take-aways</h2>
<p>These are my personal key take-aways from this project:</p>
<ul>
<li><p>Bootstrap your project to get up and running quickly</p>
<ul>
<li>In my case it meant using a Reflect scaffold to have the project structured from the start</li>
</ul>
</li>
<li><p>Read the documentation</p>
<ul>
<li>It's way easier to build things if you understand how the technology works</li>
</ul>
</li>
<li><p>Start with the main feature</p>
</li>
<li><p>Functionality first, style later</p>
</li>
<li><p>Ask for help if you have a question or get stuck</p>
<ul>
<li>I asked for help in both the Replicache Discord and Baselime Slack servers. In the Replicache server I got help from a community member. In the Baselime server I got help from one of the developers (Thanks Thomas!)</li>
</ul>
</li>
<li><p>Leverage ChatGPT for things you don't necessarily care for (my case, a lot of css)</p>
</li>
<li><p>I like building for my friends</p>
</li>
</ul>
<h2 id="heading-thanks">Thanks!</h2>
<p>Thanks for taking the time to read or at least scroll through all of this. This post is both meant as an example of me eating my own dog food regarding my previous post but also as an exercise in writing. As a newer content creator/tech blogger/aws influencer I've yet to really find my voice. I think I enjoy this type of content quite a bit. I know I like reading about people's own little side projects or how someone built something very cool. If there is something you liked/disagreed/have a question about please write to me! A comment on the post or a direct message on LinkedIn or X are both welcome.</p>
<p><a target="_blank" href="https://lagomeurovision.com/">Here's the final site.</a></p>
<p><a target="_blank" href="https://github.com/Paliago/lagom-eurovision">Here's the repo if you want to check it out.</a></p>
<hr />
<p>If you enjoyed this post you could follow me on 𝕏 at <a target="_blank" href="https://twitter.com/PaliagoAlvin">@Paliago</a>. I mostly engage with the serverless community and post pictures of my pets.</p>
<hr />
<p><a target="_blank" href="https://elva-group.com">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[A great place to quit]]></title><description><![CDATA[Ok the title might be alarming but hear me out. There are a plethora of different awards for “Swedens best employer” or best/good/great place to work etc. You could argue that they are a bit watered down because of the amount of them. Or at least har...]]></description><link>https://blog.elva-group.com/a-great-place-to-quit</link><guid isPermaLink="true">https://blog.elva-group.com/a-great-place-to-quit</guid><category><![CDATA[Company]]></category><category><![CDATA[values]]></category><dc:creator><![CDATA[Andreas Persson]]></dc:creator><pubDate>Thu, 11 Apr 2024 12:02:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712638142353/bf0699d9-a109-4593-830b-51e9444daf41.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Ok the title might be alarming but hear me out. There are a plethora of different awards for “Swedens best employer” or best/good/great place to work etc. You could argue that they are a bit watered down because of the amount of them. Or at least hard to keep track of. I’m not saying they don’t mean anything. I would think that they hold some merit, and what company would not want to be crowned a great place to work. I’d love to have that for Elva but perhaps equally as important I’d like to make Elva a <strong>Great place to quit</strong>. Here’s what I mean by that:</p>
<h2 id="heading-some-background">Some background</h2>
<p>I’ve seen some horrific examples of bad treatment when people have left a company. People have been met with the cold shoulder, not being welcomed at the office during the notice period, with their colleagues kept in the dark, and even a few that’s been threatened with legal actions. For completely no valid reasons other than trying to scare the person into acting differently. Imagine being such a daft and horrible person. I myself have been subject to a subtle but rather unpleasant threat, via email, of being sued when I left a previous work place. I took it for what it was, which was scare tactics that did not hold any ground (it was over a LinkedIn-post I’d shared and written expressing my excitement over Elva). What however hurt me and made me disappointed and mad was the things what was said behind my back once I had left. This after some 7 years together and me leaving to follow my dream to start a company. Not that well handled.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712324139562/ce4c19a6-a188-4d6d-9a69-d3ca23b790de.png?height=400" alt="Some of my friends have faced far worse threats from their former employers. None of us would ever dream of working for that company again. I would go so far as to say we actively would advice people against working there. And (bad) word tend to spread like a wildfire. And I'm here with a bunch of marshmallows on a stick." class="image--center mx-auto" /></p>
<p>Some of my friends have faced far worse threats and treatments from their former employers. None of us would ever dream of working for that company again. I would go so far as to say we actively will advice people against working there. And (bad) word tend to spread like a wildfire. I'll be standing here with a bunch of marshmallows and a stick.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1712821667917/df5389aa-eca6-4a2d-95da-b0b2340f9c96.webp?height=600" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-conclusion">The conclusion</h2>
<p>As mentioned in my <a target="_blank" href="https://blog.elva-group.com/daddy-look-what-i-made">previous blog post</a> I’m not naive enough to think that no one will ever leave Elva. Hopefully it’s for a good and ”unavoidable” reason. Even more so we can hopefully find a way to work together within Elva Group, as we’ve actually intended it to be. If you have a great idea and like to try it out within the confines of a new company, Elva will back you up with resources and expertise. If not I’d like to be the first one to give you a hug and my sincerest good luck wishes. I’m not saying that you should hold a parade in the persons honor, or declare three days of company wide mourning, but you need to treat your soon to be former colleague with upmost respect and dignity. It’s seldom an easy decision to resign from a place you like. Leaving friends and memories. Leaving the comfort of the known. In realising that and remembering that this is a human being we’re talking about, what good does it do you to treat them like garbage? We have to believe that the good prevails. That being a bully only gets you so far. And rest assured. I will do everything I can to make it so.</p>
<hr />
<p>If you enjoyed this post, want to know more about me, working at Elva or just want to reach out you can find me on <a target="_blank" href="https://www.linkedin.com/in/andreas-persson-1a38a16a/"><strong>LinkedIn</strong></a>.</p>
<hr />
<div class="hn-embed-widget" id="leadfeeder"></div><p><a target="_blank" href="https://elva-group.com/"><strong>Elva</strong></a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Shared CloudFront Distribution Cache Policy with SST]]></title><description><![CDATA[If you're sitting in a growing team and working with SST it's highly likely that you're working with personal staging environments. You might even use PR-deployments to test your sites before merging (smart of you). You might also have run into the p...]]></description><link>https://blog.elva-group.com/shared-cloudfront-distribution-cache-policy-with-sst</link><guid isPermaLink="true">https://blog.elva-group.com/shared-cloudfront-distribution-cache-policy-with-sst</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[sst]]></category><dc:creator><![CDATA[Alvin Johansson]]></dc:creator><pubDate>Mon, 15 Jan 2024 14:48:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704458453257/d5e80779-72b3-43af-9c32-f2ec6ac5ee6f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you're sitting in a growing team and working with <a target="_blank" href="https://sst.dev/">SST</a> it's highly likely that you're working with personal staging environments. You might even use PR-deployments to test your sites before merging (smart of you). You might also have run into the problem of having too many CloudFront Distribution Cache Policies. The <a target="_blank" href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-policies">maximum number of cache policies per account is 20</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704273264190/e66a1cac-a196-45af-8e56-8e1c851b5d43.png" alt class="image--center mx-auto" /></p>
<p>The error occurs because every deployment on a unique stage deploys an SST server response cache policy.</p>
<blockquote>
<p>By default, the cache policy is configured to cache all responses from <em>the server rendering Lambda based on the query-key only. If you're using</em> cookie or header based authentication, you'll need to override the * cache policy to cache based on those values as well. - <a target="_blank" href="https://github.com/sst/sst/blob/7d62f222df54f1a911589f0fedd32d19d518eb59/packages/sst/src/constructs/SsrSite.ts#L443">from the SST documentation</a></p>
</blockquote>
<p>One of the possible remedies here is to create a shared cache policy for your main development and production environment. You'd then re-use the policy for your personal stages and PR-deployments. Let's look at how to set this up.</p>
<h3 id="heading-the-plan">The Plan</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704458240192/3c8abf92-4845-4f49-b46b-41c0130bc9bf.png" alt class="image--center mx-auto" /></p>
<p>You're going to need at least two stacks, one for your site and one for the policy. When deploying the main distribution for the stage (as in <code>dev</code> and not <code>pr-1</code>) we'd deploy the policy first together with the id of it as a value in SSM Parameter Store. We'd then fetch the id of the policy during deployment and use it for the CloudFront Distribution in the new stack. For every other stage in the account we'd only have to deploy our site stack. Let's look at some code.</p>
<h3 id="heading-distribution-policy-stack">Distribution Policy Stack</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
  CachePolicy,
  CacheQueryStringBehavior,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-cloudfront"</span>;
<span class="hljs-keyword">import</span> { StringParameter } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-ssm"</span>;
<span class="hljs-keyword">import</span> { Duration } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/core"</span>;
<span class="hljs-keyword">import</span> { StackContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"sst/constructs"</span>;

<span class="hljs-comment">// the ssm param name we will share with the other stack</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> cacheParamName = <span class="hljs-string">"/cool-site/cache-policy-id"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DistCachePolicy</span>(<span class="hljs-params">{ stack }: StackContext</span>) </span>{
  <span class="hljs-comment">// the cache policy, configure it to your liking</span>
  <span class="hljs-keyword">const</span> serverCachePolicy = <span class="hljs-keyword">new</span> CachePolicy(stack, <span class="hljs-string">"ServerCache"</span>, {
    queryStringBehavior: CacheQueryStringBehavior.all(),
    headerBehavior: CacheHeaderBehavior.none(),
    cookieBehavior: CacheCookieBehavior.none(),
    defaultTtl: Duration.days(<span class="hljs-number">0</span>),
    maxTtl: Duration.days(<span class="hljs-number">365</span>),
    minTtl: Duration.days(<span class="hljs-number">0</span>),
  });

  <span class="hljs-comment">// the ssm param</span>
  <span class="hljs-keyword">new</span> StringParameter(stack, <span class="hljs-string">"CachePolicyIdParameter"</span>, {
    parameterName: cacheParamName,
    stringValue: serverCachePolicy.cachePolicyId,
  });

  stack.addOutputs({
    CachePolicyId: serverCachePolicy.cachePolicyId,
    ParameterName: cacheParamName,
  });
}
</code></pre>
<p>Notice here how we set up the cache policy as well as a parameter. With the name of the parameter exported as a const. This constant will be imported and used in the Site stack.</p>
<h3 id="heading-site-stack">Site Stack</h3>
<p>The site stack fetches the id of the cache policy and uses that as the server cache policy when deploying the CloudFront Distribution.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { CachePolicy } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-cloudfront"</span>;
<span class="hljs-keyword">import</span> { StringParameter } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-cdk-lib/aws-ssm"</span>;
<span class="hljs-keyword">import</span> { RemixSite, StackContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"sst/constructs"</span>;
<span class="hljs-comment">// the cache param we exported in the dist stack</span>
<span class="hljs-keyword">import</span> { cacheParamName } <span class="hljs-keyword">from</span> <span class="hljs-string">"./DistCachePolicy"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Site</span>(<span class="hljs-params">{ stack }: StackContext</span>) </span>{
  <span class="hljs-comment">// read the cache policy id from SSM</span>
  <span class="hljs-keyword">const</span> cachePolicyId = StringParameter.valueForStringParameter(
    stack,
    cacheParamName
  );

  <span class="hljs-keyword">const</span> serverCachePolicy = CachePolicy.fromCachePolicyId(
    stack,
    <span class="hljs-string">"CachePolicy"</span>,
    cachePolicyId
  );

  <span class="hljs-comment">// works with any high-level site construct which extends SsrSite</span>
  <span class="hljs-keyword">const</span> site = <span class="hljs-keyword">new</span> RemixSite(stack, <span class="hljs-string">"site"</span>, {
    path: <span class="hljs-string">"./apps/web"</span>,
    cdk: {
      serverCachePolicy,
    },
  });

  stack.addOutputs({
    DocumentationSiteUrl: site.url,
  });
}
</code></pre>
<p>In this example I deploy a site based on the Remix SST construct but any high-level construct based on the <code>SsrSite</code> construct will work (Astro, Remix, Nextjs, SolidStart, SvelteKit).</p>
<h3 id="heading-the-sst-config">The SST Config</h3>
<p>We should also take a look at the <code>sst.config.ts</code> for how we then deploy these stacks.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { SSTConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"sst"</span>;
<span class="hljs-keyword">import</span> Site <span class="hljs-keyword">from</span> <span class="hljs-string">"./stacks/Site"</span>;
<span class="hljs-keyword">import</span> DistCachePolicy <span class="hljs-keyword">from</span> <span class="hljs-string">"./stacks/DistCachePolicy"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  config(_input) {
    <span class="hljs-keyword">return</span> {
      name: <span class="hljs-string">"cool-site"</span>,
      region: <span class="hljs-string">"eu-north-1"</span>,
    };
  },
  stacks(app) {
    <span class="hljs-comment">// only deploy cache policy in prod and dev, reuse them in PRs</span>
    <span class="hljs-keyword">if</span> (app.stage === <span class="hljs-string">"prod"</span> || app.stage === <span class="hljs-string">"dev"</span>) {
      app.stack(DistCachePolicy);
    }
    app.stack(Site);
  },
} satisfies SSTConfig;
</code></pre>
<p>We set the <code>DistCache</code> stack within an <code>if</code> statement that checks if the stage is either dev or prod as those are the only stages we want to create policies for.</p>
<h3 id="heading-deployment">Deployment</h3>
<p>To deploy and start using this shared cache we need to first deploy the dev stage.</p>
<pre><code class="lang-bash">sst deploy --stage dev
</code></pre>
<p>After that is ready we should be good to deploy our personal stage.</p>
<pre><code class="lang-bash">sst dev
</code></pre>
<p>Now we can deploy upward to a 100 individual distributions using this same cache policy. Hope this helped you out!</p>
<hr />
<p>If you enjoyed this post you could follow me on 𝕏 at <a target="_blank" href="https://twitter.com/PaliagoAlvin">@Paliago</a>. I mostly engage with the serverless community and post pictures of my pets.</p>
<hr />
<p><a target="_blank" href="https://elva-group.com">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Testing Individual StepFunction States]]></title><description><![CDATA[AWS StepFunctions is a fantastic service for orchestrating complex workflows. But testing StepFunctions has always been... tricky.
You could execute the entire state machine, observing side effects and final outcomes, but this approach often feels li...]]></description><link>https://blog.elva-group.com/testing-individual-stepfunction-states</link><guid isPermaLink="true">https://blog.elva-group.com/testing-individual-stepfunction-states</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Sebastian Bille]]></dc:creator><pubDate>Thu, 07 Dec 2023 07:28:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701897405962/02345e48-d51e-4137-9850-597407b70079.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS StepFunctions is a fantastic service for orchestrating complex workflows. But testing StepFunctions has always been... tricky.</p>
<p>You could execute the entire state machine, observing side effects and final outcomes, but this approach often feels like using a sledgehammer for a task that needs a scalpel. It's effective for smaller workflows but quickly becomes unwieldy with more complex ones.</p>
<p>Unit testing Lambda functions or other compute components can cover the core logic of a task, but this doesn't cover data transformations or control flows between states.</p>
<p><a target="_blank" href="https://docs.aws.amazon.com/step-functions/latest/dg/sfn-local.html">Local emulation of StepFunctions</a> is another approach but it often leads to IAM access inconsistencies and other headaches common with emulating cloud services.</p>
<p>While these methods are useful, they each come with drawbacks that risk that the beautiful state machine you wrote to handle that one complicated payment workflow becomes just another mess of logic that is hard to maintain or evolve. This is particularly ironic considering that many state machines are built explicitly to untangle complex logic.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Did you know? While<strong> StepFunctions</strong> is the name of the AWS Service, a workflow in StepFunctions is called a <strong>state machine</strong>. Each step in a workflow is called a <strong>state</strong>, and a <strong>task state</strong> represents a state where another AWS service performs the work.</div>
</div>

<p>Right before re:Invent 2023, however, AWS <a target="_blank" href="https://aws.amazon.com/blogs/aws/external-endpoints-and-testing-of-task-states-now-available-in-aws-step-functions/">released</a> a new capability to test individual states in a state machine.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701888985072/1c7bb8c3-6c3f-4b27-a73a-b7496d3df44d.png" alt="Printscreen from the test feature in StepFunction console" class="image--center mx-auto" /></p>
<p>This feature is available in the StepFunction console, but, more importantly, it also allows us to write isolated integration tests in code. These can test output, data transformations, and control flow for each individual state in our state machines - all without the hassle of trying to force the entire state machine to go down a particular route to validate the same thing.</p>
<p>Let's take a look at how we can use it.</p>
<h2 id="heading-integration-tests">Integration Tests</h2>
<p>First, assume we have a small state machine that upgrades a user account, but only if they have enough credits to cover the cost.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1701898064399/9c9ba7e3-a966-4bae-97a9-6466e0ec318d.png" alt class="image--center mx-auto" /></p>
<p>Using the Node AWS SDK, there's a new <code>TestStateCommand</code>. This command takes:</p>
<ul>
<li><p>A task definition</p>
</li>
<li><p>A task input payload</p>
</li>
<li><p>An IAM role that the StepFunction service will assume to execute the state</p>
</li>
</ul>
<p>It executes the state, and returns:</p>
<ul>
<li><p>The status of the execution - whether or not it errored, succeeded, or caught an error</p>
</li>
<li><p>The output of the task</p>
</li>
<li><p>The state that would be next in line</p>
</li>
<li><p>Metadata about the execution</p>
</li>
</ul>
<p>An example response could look like this:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-string">'$metadata'</span>: {
    <span class="hljs-attr">httpStatusCode</span>: <span class="hljs-number">200</span>,
    <span class="hljs-attr">requestId</span>: <span class="hljs-string">'2916bb74-418d-402b-a903-18c0a9c3e9c9'</span>,
    <span class="hljs-attr">extendedRequestId</span>: <span class="hljs-literal">undefined</span>,
    <span class="hljs-attr">cfId</span>: <span class="hljs-literal">undefined</span>,
    <span class="hljs-attr">attempts</span>: <span class="hljs-number">1</span>,
    <span class="hljs-attr">totalRetryDelay</span>: <span class="hljs-number">0</span>
  },
  <span class="hljs-attr">nextState</span>: <span class="hljs-string">'Choice'</span>,
  <span class="hljs-attr">output</span>: <span class="hljs-string">'{"ExecutedVersion":"$LATEST","Payload":{"cost":75,"credits":100},"SdkHttpMetadata":{"AllHttpHeaders":{"X-Amz-Executed-Version":["$LATEST"],"x-amzn-Remapped-Content-Length":["0"],"Connection":["keep-alive"],"x-amzn-RequestId":["256f8768-f84e-46b0-ba79-b48dbfd250fa"],"Content-Length":["25"],"Date":["Wed, 06 Dec 2023 19:48:24 GMT"],"X-Amzn-Trace-Id":["root=1-6570d008-34fc67a96baf085e418d0745;sampled=1;lineage=055c6c5a:0"],"Content-Type":["application/json"]},"HttpHeaders":{"Connection":"keep-alive","Content-Length":"25","Content-Type":"application/json","Date":"Wed, 06 Dec 2023 19:48:24 GMT","X-Amz-Executed-Version":"$LATEST","x-amzn-Remapped-Content-Length":"0","x-amzn-RequestId":"256f8768-f84e-46b0-ba79-b48dbfd250fa","X-Amzn-Trace-Id":"root=1-6570d008-34fc67a96baf085e418d0745;sampled=1;lineage=055c6c5a:0"},"HttpStatusCode":200},"SdkResponseMetadata":{"RequestId":"256f8768-f84e-46b0-ba79-b48dbfd250fa"},"StatusCode":200}'</span>,
  <span class="hljs-attr">status</span>: <span class="hljs-string">'SUCCEEDED'</span>
}
</code></pre>
<p>Let's put together a couple of helper functions that can help us fetch a deployed state machine's definition, and one that executes a given state in the state machine definition.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// util.ts</span>

<span class="hljs-keyword">import</span> {
  DescribeStateMachineCommand,
  SFNClient,
  TestStateCommand,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-sdk/client-sfn'</span>;

<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> SFNClient({});

<span class="hljs-keyword">interface</span> TestStateInput {
  stateMachineDefinition: <span class="hljs-built_in">string</span>;
  roleArn: <span class="hljs-built_in">string</span>;
  taskName: <span class="hljs-built_in">string</span>;
  input?: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> fetchStateMachine = <span class="hljs-keyword">async</span> (stateMachineArn: <span class="hljs-built_in">string</span>) =&gt; {
  <span class="hljs-keyword">const</span> stateMachine = <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> DescribeStateMachineCommand({
      stateMachineArn: stateMachineArn,
    })
  );

  <span class="hljs-keyword">if</span> (!stateMachine.definition) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'State machine definition not found'</span>);
  }

  <span class="hljs-keyword">if</span> (!stateMachine.roleArn) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'State machine roleArn not found'</span>);
  }

  <span class="hljs-keyword">return</span> {
    definition: stateMachine.definition,
    roleArn: stateMachine.roleArn,
  };
};

<span class="hljs-keyword">const</span> getTask = <span class="hljs-function">(<span class="hljs-params">taskName: <span class="hljs-built_in">string</span>, definition: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
  <span class="hljs-comment">// parse the definition and return the given state</span>
  <span class="hljs-keyword">const</span> task = <span class="hljs-built_in">JSON</span>.parse(definition).States[taskName];
  <span class="hljs-keyword">if</span> (!task) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Task <span class="hljs-subst">${taskName}</span> not found`</span>);
  }

  <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.stringify(task);
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> testState = <span class="hljs-keyword">async</span> ({
  roleArn,
  stateMachineDefinition,
  taskName,
  input,
}: TestStateInput) =&gt; {
  <span class="hljs-keyword">const</span> task = getTask(taskName, stateMachineDefinition);

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> TestStateCommand({
      definition: task,
      roleArn: roleArn,
      input: input,
    })
  );
};
</code></pre>
<p><code>fetchStateMachine</code> takes a state machine ARN and returns the deployed state machine's definition and the IAM role it's configured to use.</p>
<p><code>testState</code> takes that definition, role, and the name of the state that we want to test. It executes the state using the StepFunction service, and it returns the execution information.</p>
<p>We can now use these to start writing our tests:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// account-upgrade.test.ts</span>
<span class="hljs-keyword">import</span> { describe, it, expect } <span class="hljs-keyword">from</span> <span class="hljs-string">'vitest'</span>;
<span class="hljs-keyword">import</span> { fetchStateMachine, testState } <span class="hljs-keyword">from</span> <span class="hljs-string">'./util'</span>;

describe(<span class="hljs-string">'[accountUpgrade]'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> stateMachine = <span class="hljs-keyword">await</span> fetchStateMachine(
    <span class="hljs-string">'arn:aws:states:us-east-1:123456789012:stateMachine:accountUpgrade'</span>
  );

  describe(<span class="hljs-string">'[CheckCreditBalance]'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    it(<span class="hljs-string">'returns credit balance'</span>, <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> testState({
        stateMachineDefinition: stateMachine.definition,
        roleArn: stateMachine.roleArn,
        taskName: <span class="hljs-string">'CheckCreditBalance'</span>,
        input: <span class="hljs-built_in">JSON</span>.stringify({
          cost: <span class="hljs-number">75</span>,
        }),
      });

      expect(res.status).toBe(<span class="hljs-string">'SUCCEEDED'</span>);
      expect(res.nextState).toBe(<span class="hljs-string">'Choice'</span>);

      <span class="hljs-keyword">const</span> output = <span class="hljs-built_in">JSON</span>.parse(res.output || <span class="hljs-string">'{}'</span>);
      expect(output.Payload).toEqual({
        balance: expect.any(<span class="hljs-built_in">Number</span>),
        cost: <span class="hljs-number">75</span>,
      });
    });
  });

  describe(<span class="hljs-string">'[Choice]'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    it(<span class="hljs-string">'flows to account upgrade if credit balance more than cost'</span>, <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> testState({
        stateMachineDefinition: stateMachine.definition,
        roleArn: stateMachine.roleArn,
        taskName: <span class="hljs-string">'Choice'</span>,
        input: <span class="hljs-built_in">JSON</span>.stringify({
          cost: <span class="hljs-number">75</span>,
          balance: <span class="hljs-number">100</span>,
        }),
      });

      expect(res.status).toBe(<span class="hljs-string">'SUCCEEDED'</span>);
      expect(res.nextState).toBe(<span class="hljs-string">'UpgradeAccount'</span>);
    });
  });
});
</code></pre>
<p>With this, we have everything we need to start to fully cover the full configuration and logic of our state machines.</p>
<p>For a more complete demo, which gives us type safety for the resource ARN and the state names, I've published a demo SST project <a target="_blank" href="https://github.com/TastefulElk/testing-stepfunctions-demo">here</a>.</p>
<p>Additionally, Lars Jacobsson has <a target="_blank" href="https://dev.to/aws-builders/take-stepfunctions-teststate-api-one-step-further-with-samp-cli-4klm">added support</a> to his <code>samp-cli</code> project for interactively running the individual state tests - it even lets you re-use recent live execution payloads. Incredible!</p>
<p>If you want to dig further into more strategies for testing StepFunctions, Yan Cui has some excellent writing on it <a target="_blank" href="https://medium.com/theburningmonk-com/testing-strategies-for-step-functions-19cd087eae19">here</a>.</p>
<p>In conclusion, testing StepFunctions is hard, but it did just get a whole lot easier. As we've seen, the ability to test individual StepFunction states marks a leap forward in how we can build more robust and well-tested state machines. This feature isn't just a neat trick; it addresses some of the core frustrations we've faced in complex workflow orchestration. It doesn't replace end-to-end testing and unit testing of state machines, but it's a well-appreciated complement!</p>
<hr />
<p>Hi there, I'm Sebastian Bille! If you enjoyed this post or just want a constant feed of memes, AWS &amp; serverless talk, and the occasional new blog post, make sure to follow me on 𝕏 at <a target="_blank" href="https://twitter.com/TastefulElk">@TastefulElk</a> or on <a target="_blank" href="https://www.linkedin.com/in/sebastianbille/"><strong>LinkedIn</strong></a> 👋</p>
<hr />
<p><a target="_blank" href="https://elva-group.com">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Daddy, look what I made]]></title><description><![CDATA[I thought I’d mix it up a bit and add another flavour to the otherwise technical content that is presented here. This blog post will focus on my experiences and thoughts surrounding the first year at Elva, from the excitement of starting a company to...]]></description><link>https://blog.elva-group.com/daddy-look-what-i-made</link><guid isPermaLink="true">https://blog.elva-group.com/daddy-look-what-i-made</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[community]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Andreas Persson]]></dc:creator><pubDate>Tue, 05 Dec 2023 13:03:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700728025241/0c692d98-e11c-4b22-aa02-871ddaef4058.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I thought I’d mix it up a bit and add another flavour to the otherwise technical content that is presented here. This blog post will focus on my experiences and thoughts surrounding the first year at Elva, from the excitement of starting a company to the excitement of actually running it.</p>
<h2 id="heading-the-beginning">The beginning</h2>
<p>To be honest I’ve always “dreamt” of starting my own company but never really had the guts or “the idea” or even the know-how. Or I might just have been too comfortable or lazy, I don’t know. All I know is that I didn't want to do it alone. When I was approached with the opportunity and idea of the company-soon-to-be-called-Elva, it felt like a no-brainer. The idea of Sagewei (now Elva Group) acting as a platform company for our company struck a chord with me. A platform company consisting of people with a proven track record. With a view on both hard and soft values aligning with mine taking responsibility for handling the legalities, HR, writing contracts, and teaching us the ins and outs of building a successful company. Enabling us to focus on what we do best: Making silly jokes at, and photoshops of, each other. And serverless stuff on AWS of course. The stars aligned and all that. We just needed to assemble the right people.</p>
<p><img src="https://media.tenor.com/PitLC368CS0AAAAd/anchorman-team.gif" alt /></p>
<h3 id="heading-building-the-brand">Building the brand</h3>
<p>Before you officially can start there are a few things that are nice to have in place. For instance a company name. It’s quite central but perhaps not something that makes or breaks a company. For us though the name was something to be proud of and that we all could get behind. The naming process was quite extensive and tedious, perhaps because <em>too many cooks spoil the broth</em> or simply because it's very hard to come up with names for a company. Looking back some of the names were... equally brilliant and ridiculous. Imagine if we had settled for "Wolf Tech"… When the name Elva came to light I think we all sort of just knew, and the voting was unanimous. It checked all the boxes...</p>
<p>☑️ Short and concise<br />☑️ Nordic sounding<br />☑️ Works in English<br />☑️ ... not Wolf Tech</p>
<p>... but the main idea is that the Swedish word Elva translates to 'eleven' in English, which is the position of the letter <strong>λ</strong> (lambda) in the Greek alphabet. Our homage to the AWS service that started the serverless revolution.</p>
<p>From there designing the logo went fairly smoothly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697477626127/2b49d572-4ec8-41d3-ba2d-881bdff81f31.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697478082970/ec9542c9-d99d-4039-ae93-83193d28526a.png" alt /></p>
<p>Every Elva company is designed with very low overhead (at the time of writing we are below 6%). This enables us to maintain high net margins of profit (goal is 30%) while at the same time offering competitive salaries and making investments for the future. To reach and ensure the sustainability of these numbers our strategy has been to make sure that each of us had assignments from day one. Each Elva office is by design its own company with its respective founders, budget and board, free to make our own decisions. What we do however share is the devotion and focus on AWS and serverless. Not only is this a decision based on the fact that serverless is a force to be reckoned with for the foreseeable future it also makes it easier to stay relevant and updated. Another thing we agreed upon early was our intent to build a strong relationship with AWS. We also wanted to contribute to the community and share whatever knowledge we have and acquire along the way.</p>
<h2 id="heading-the-ups-and-downs">The ups and downs</h2>
<p>Every journey is bound to have its highs and lows. I'll try to list some of those who have impacted me the most and in some cases even the company.</p>
<h2 id="heading-the-ups">The ups 🔝</h2>
<h3 id="heading-clyde">Clyde</h3>
<p>Many of us have had a long working relationship with a bunch of really great people from Cebu in the Philippines, who also happen to be super-talented developers. I contacted <a target="_blank" href="https://www.linkedin.com/feed/update/urn:li:activity:7115954170028785664/">one of them</a> on the off chance of him being interested in joining Elva. Lo and behold, he was very into the idea and decided to join us. I strongly believe in the concept of distributed teams and we will continue the search for talent, regardless of their physical whereabouts.</p>
<h3 id="heading-advanced-partner">Advanced partner</h3>
<p>If we want to become the leading AWS brand in the Nordics we also believe that we should have a strong partnership with AWS. Therefore we put a lot of time and effort into trying to reach Advanced Partner and <a target="_blank" href="https://www.linkedin.com/feed/update/urn:li:activity:7081875111179362304">did so</a> in less than a year.</p>
<h3 id="heading-prospecting-league-x2">Prospecting league x2</h3>
<p>We entered the AWS Partner Prospecting League for the Nordics on two occasions and managed to become opportunity count champions <a target="_blank" href="https://www.linkedin.com/feed/update/urn:li:activity:7112708355604770816">both</a> those times. Let’s see if we can bag a third one.</p>
<h3 id="heading-sandvik-startup-challenge">Sandvik Startup Challenge</h3>
<p>I didn’t participate or contribute to <a target="_blank" href="https://www.linkedin.com/posts/sandvik_sandvik-startup-challenge-activity-7121067509268537344-TIw7">this</a> but it was exciting to follow from the sidelines. I’m extremely proud of the effort and outcome that the team put together. To be selected as one of the few companies to compete in the final of the Sandvik Startup Challenge was a huge honour and accomplishment.</p>
<h3 id="heading-kickoffs-and-get-togethers">Kickoffs and get-togethers</h3>
<p>We’ve had a few kickoffs and get-togethers. Serverless meetups and dinners. Conferences and after works. All of which left me with a smile on my face, a head full of ideas and insights and energy in abundance.</p>
<h3 id="heading-recognition">Recognition</h3>
<p>Whenever you put in hard work, it's nice to get recognised. I'm trying to give credit where credit is due, hopefully inspiring others to do the same. That way we hopefully create an uplifting and positive work environment internally. When the recognition comes from outside it's sometimes even more encouraging. To hear what people are saying about Elva is truly inspiring and humbling.</p>
<h3 id="heading-last-but-not-least-the-office">Last but not least... the office</h3>
<p>I almost forgot but we do, in my opinion, have a very nice office. It took quite some time to find the right spot and even more time to make it what it is today. But we are all very pleased with the way it turned out.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700814697631/eaa1722e-816d-46f7-aa76-18fba44364e7.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-the-downs">The downs 🪫</h2>
<h3 id="heading-recession">Recession</h3>
<p>The ongoing recession is impacting large portions of the world's population as well as companies. This has made us rethink our strategy in a way, and perhaps slow down the growth rate of the company in terms of new colleagues.</p>
<h3 id="heading-loss-of-assignments-and-lower-fees">Loss of assignments and lower fees</h3>
<p>With recession comes hard times and with that, it’s not uncommon in our line of business to face lower fees and even loss of assignments. We are not exempt from this. It’s sad to have to leave a place that you like and care about. Even more so the people who've become your close colleagues and friends.</p>
<h2 id="heading-the-important-stuff">The important stuff</h2>
<p>The way I went into this was that I wanted to build a company where I myself would love to work. A place with warmth and laughter. Where I am inspired to learn and have the opportunity to do so. Every day. (I’m saying ‘I’ but I should really be saying ‘We’). In as little as one year I can comfortably say that we’ve achieved exactly that. We are for sure still working out some kinks and will continue to fine-tune every aspect of the business but we are well on the way. By maintaining a flat organisation where everyone is invited and encouraged to contribute and influence the direction of the company Elva will hopefully be a place where more people love to work. From my previous job, the majority of my closest <s>colleagues</s> friends have left said job. One is well on the way to becoming a police officer, another one left for Stockholm and a third one is moving abroad to save and care for seals. Have you heard anything so <s>noble</s> <s>quirky</s> lovely? One thankfully didn’t evade me and works side by side with me at Elva. And as far as the other founders… well we all used to work together at one time.</p>
<p>The point I’m trying to make is this: The most important thing, for me, is the people around you. Your colleagues. After all, you spend the lion part of the time you are awake together with them. At least now I can have a say in who those people are. They will come and go, and for various reasons as the examples above. <em>All we have to decide is what to do with the time that is given us.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697478098910/b5406f68-e6e7-4db7-85a4-d45d44b790bd.png" alt /></p>
<h2 id="heading-key-takeaways">Key takeaways</h2>
<ul>
<li><p>Build capital (if possible without anybody else’s money). You never know when that rainy day comes.</p>
</li>
<li><p>Build the team. It’s nice to know that you can count on the people around you when that rainy day turns into a storm.</p>
</li>
<li><p>Build your brand. You don’t have to become a household name. But things tend to come easier if potential customers know your name.</p>
</li>
<li><p>Build the culture. And talk, talk, talk. Make sure that everyone can make their voice heard. We are all different but should be comfortable enough to speak our minds.</p>
</li>
</ul>
<p>I can honest-to-God, hand on heart, scout's honour and I kid you not say that I love every day of working at Elva. And that's largely due to the remarkable people I share my days with. Through open and honest communication, we aim to retain the amazing talent we have and, by staying curious and updated, we hope to attract even more.</p>
<hr />
<p>If you enjoyed this post, want to know more about me, working at Elva or just want to reach out you can find me on <a target="_blank" href="https://www.linkedin.com/in/andreas-persson-1a38a16a/"><strong>LinkedIn</strong></a>.</p>
<hr />
<div class="hn-embed-widget" id="leadfeeder"></div><p> <a target="_blank" href="https://elva-group.com/"><strong>Elva</strong></a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Building an IoT-powered Fridge]]></title><description><![CDATA[Imagine the scenario: guests are about to arrive, and you realize your fridge is running low on beer. Your BoA (Beverage on Arrival) SLA is in danger, and panic sets in, but fear not! In this blog post, we'll walk you through a solution we've devised...]]></description><link>https://blog.elva-group.com/building-an-iot-powered-fridge</link><guid isPermaLink="true">https://blog.elva-group.com/building-an-iot-powered-fridge</guid><category><![CDATA[serverless]]></category><category><![CDATA[iot]]></category><category><![CDATA[AWS]]></category><category><![CDATA[aws lambda]]></category><dc:creator><![CDATA[Joel Roxell]]></dc:creator><pubDate>Thu, 23 Nov 2023 12:21:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700732992559/6cc0cb09-6f1d-4e64-8166-3375eadd177d.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine the scenario: guests are about to arrive, and you realize your fridge is running low on beer. Your <strong>BoA (Beverage on Arrival)</strong> SLA is in danger, and panic sets in, but fear not! In this blog post, we'll walk you through a solution we've devised to never end up in this situation again. By integrating AWS services, IoT technology, Lambda functions, and even a touch of Rust programming, <strong>Elva</strong> created a system that not only alerts us when our beverage stock is running low but also has the potential to automatically place orders to replenish our supplies in the future.</p>
<blockquote>
<p>Using AWS, IoT core, Lambda, Cloudwatch, TS, Rust</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700580328879/802556e4-7174-40c4-ba31-8daad15f42c1.png" alt /></p>
</blockquote>
<p>Requirements (total cost: ~31$)</p>
<ul>
<li><p>4 <a target="_blank" href="https://www.sparkfun.com/products/10245">Weight sensors</a> &amp; 1 <a target="_blank" href="https://www.sparkfun.com/products/13879">Amplifier/ADC</a></p>
</li>
<li><p>1 <a target="_blank" href="https://www.raspberrypi.com/products/raspberry-pi-zero-w/">RPI zero w</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/free">AWS account</a></p>
</li>
<li><p><em>A few hours of your lifetime</em></p>
</li>
</ul>
<p>TLDR; Here are the projects</p>
<ol>
<li><p><a target="_blank" href="https://github.com/elva-labs/byra/tree/main/byra-watcher">Cloud implementation</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/elva-labs/byra/tree/main/rpi">On-board processes</a></p>
</li>
</ol>
<h2 id="heading-the-setup">The Setup</h2>
<p>The overall setup looks like this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700668459569/27082876-1648-4687-a679-5342bbc27225.png" alt class="image--center mx-auto" /></p>
<p>The image displays an abstract view of the data flow in the finalized system.</p>
<ol>
<li><p>The sensors output an analog value depending on the applied pressure (weight). The reading is transformed into a digital output using an analog-to-digital converter (<strong>ADC</strong>).</p>
</li>
<li><p>That resulting data is sent to the scale-reading-process using serial communication (via Raspberry’s <strong>GPIO</strong>).</p>
</li>
<li><p>Next, the first (scale worker) process collects and transforms the digital values into something more understandable to us. The second (IoT worker) pushes that data to AWS IoT core and, in turn, other upstream services like Slack.</p>
</li>
</ol>
<h3 id="heading-the-cloud">The Cloud</h3>
<blockquote>
<p>AWS IoT Core, Lambda, and Cloudwatch Metrics</p>
</blockquote>
<p>As the infrastructure diagram notes, the IoT worker running on the Raspberry is pushing the translated scale readings to IoT core (using MQTT). Once we're in the cloud, we can do whatever we like. The main gist is that we have two handlers and some certificates.</p>
<p>We pass the final readings to Cloudwatch using lambda in this specific setup.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ~/byra-watcher/src/lambda.ts</span>
<span class="hljs-keyword">const</span> metrics = <span class="hljs-keyword">new</span> Metrics({ <span class="hljs-keyword">namespace</span>: <span class="hljs-string">"elva-labs"</span>, serviceName: <span class="hljs-string">"byra"</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = middy(<span class="hljs-keyword">async</span> (
  event: {
    grams: <span class="hljs-built_in">number</span>
  }) =&gt; {
  metrics.addMetric(<span class="hljs-string">"beerWeight"</span>, MetricUnits.Count, event.grams);
}).use(logMetrics(metrics));
</code></pre>
<p>In Cloudwach, we can follow the readings and create alarms if the weight is below a specific threshold value. If this threshold value is breached, another lambda is triggered, which sends a message to Slack so that everyone can see that we have a critical problem to resolve.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ~/byra-watcher/src/slack.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler: EventBridgeHandler&lt;<span class="hljs-string">'_'</span>, CloudWatchAlarmDetail, <span class="hljs-built_in">void</span>&gt; = <span class="hljs-keyword">async</span> (event) =&gt; {
  <span class="hljs-built_in">console</span>.info(<span class="hljs-string">`Received event: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(event)}</span>`</span>);

  <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> IncomingWebhook(Config.SLACK_URL).send({
    text:
      event.detail.state.value === <span class="hljs-string">"ALARM"</span>
        ? <span class="hljs-string">`<span class="hljs-subst">${WARN_EMOJI}</span> CRITICAL: <span class="hljs-subst">${BEER_EMOJI}</span> Beer count is low`</span>
        : <span class="hljs-string">`<span class="hljs-subst">${HAPPY_EMOJI}</span> ALL GOOD: <span class="hljs-subst">${BEER_EMOJI}</span> We have beer!`</span>,
  });
};
</code></pre>
<p>It's time to deploy our handlers and acquire our "thing"-certificates so the RPI can push data to the cloud.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ~/byra-watcher/stacks/ByraStack.ts</span>
<span class="hljs-comment">// ...</span>
<span class="hljs-keyword">const</span> { thingArn, certId, certPem, privKey } = <span class="hljs-keyword">new</span> ThingWithCert(stack, <span class="hljs-string">'ByraScale01'</span>, {
  thingName: <span class="hljs-string">'byra-01'</span>,
  saveToParamStore: <span class="hljs-literal">true</span>,
  paramPrefix: <span class="hljs-string">'devices'</span>,
});
<span class="hljs-comment">// ...</span>
</code></pre>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> ~/byra-watcher &amp;&amp; npm run deploy

SST v2.36.1

➜  App:     byra-watcher
   Stage:   dev
   Region:  eu-north-1
   Account: ...

|  ByraStack PUBLISH_ASSETS_COMPLETE 

✔  Deployed:
   ByraStack
   Byra01Thing: arn:aws:iot:eu-north-1:...:thing/byra-01
   CertId: ...
   CertPem: -----BEGIN CERTIFICATE-----
            ...
            -----END CERTIFICATE-----    

   PrivKey: -----BEGIN RSA PRIVATE KEY-----
            ...
            -----END RSA PRIVATE KEY-----
</code></pre>
<p>Who could have guessed? <strong>Super simple</strong>. Now, we have our infrastructure deployed and our certificates generated.</p>
<h3 id="heading-the-hardware">The Hardware</h3>
<p>The objective of the Raspberry is to read data from the sensors, transform the reading into something we can understand, and then push that data to the cloud for further action.</p>
<ul>
<li><p>The scale worker code can be found <a target="_blank" href="https://github.com/elva-labs/byra/tree/main/rpi/elva-byra-scale">here</a>.</p>
</li>
<li><p>The IoT worker code can be found <a target="_blank" href="https://github.com/elva-labs/byra/tree/main/rpi/elva-byra-iot-worker">here</a>.</p>
</li>
<li><p>The wiring guide for the cells can be found <a target="_blank" href="https://learn.sparkfun.com/tutorials/load-cell-amplifier-hx711-breakout-hookup-guide/all">here</a>.</p>
</li>
</ul>
<p>To read data, we needed to know how to communicate with the analog-to-digital (ADC) component. Reading through the <a target="_blank" href="https://cdn.sparkfun.com/datasheets/Sensors/ForceFlex/hx711_english.pdf">documentation</a>, we found that it uses serial communication and a 24-bit data protocol (+gain bits) to send data over the wire.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700724566655/d096f067-a4cf-44f8-a7ba-e3e0eb3fc243.jpeg" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687091305154/d7552b56-62e6-49c2-893c-bbef1777c15c.png" alt class="image--center mx-auto" /></p>
<p>The following protocol is implemented like the following image. In short, we ensure we can read data from the ADC and dump each bit into a temporary buffer, which finally translates to an actual value in grams.</p>
<pre><code class="lang-rust"><span class="hljs-comment">// ~/rpi/elva-byra-scale/src/hx711.rs#L107</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">read</span></span>(&amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">f32</span>, HX711Error&gt; {
        <span class="hljs-keyword">if</span> !<span class="hljs-keyword">self</span>.dout.is_low() {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(HX711Error::new(HX711ErrorType::DoutNotReady));
        }

        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> buff = <span class="hljs-number">0</span>;

        <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..<span class="hljs-number">24</span> {
            <span class="hljs-keyword">self</span>.send_pulse()?;
            thread::sleep(Duration::from_nanos(<span class="hljs-number">100</span>));
            buff &lt;&lt;= <span class="hljs-number">1</span>;
            buff |= <span class="hljs-keyword">match</span> <span class="hljs-keyword">self</span>.dout.read() {
                Level::Low =&gt; <span class="hljs-number">0b0</span>,
                Level::High =&gt; <span class="hljs-number">0b1</span>,
            };
        }

        <span class="hljs-comment">// Sets gain for following reads...</span>
        <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..<span class="hljs-keyword">match</span> <span class="hljs-keyword">self</span>.gain {
            Gain::G32 =&gt; <span class="hljs-number">3</span>,
            Gain::G64 =&gt; <span class="hljs-number">2</span>,
            Gain::G128 =&gt; <span class="hljs-number">1</span>,
        } {
            <span class="hljs-keyword">self</span>.send_pulse()?;
        }

        <span class="hljs-literal">Ok</span>(<span class="hljs-keyword">self</span>.translate(buff))
}
</code></pre>
<h3 id="heading-debugging">Debugging</h3>
<p>We can debug the communication over the wires using a <a target="_blank" href="https://www.amazon.se/AZDelivery-Analyzer-USB-Kabel-kompatibel-inklusive/dp/B01MUFRHQ2/ref=asc_df_B01MUFRHQ2/?tag=shpngadsglede-21&amp;linkCode=df0&amp;hvadid=476458787949&amp;hvpos=&amp;hvnetw=g&amp;hvrand=8825784032754899236&amp;hvpone=&amp;hvptwo=&amp;hvqmt=&amp;hvdev=c&amp;hvdvcmdl=&amp;hvlocint=&amp;hvlocphy=1012511&amp;hvtargid=pla-404738525278&amp;psc=1">logic analyzer</a> and <a target="_blank" href="https://www.saleae.com/downloads/">this helpful program</a>. A typical information exchange looked like this.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700644212318/3a51a8f5-d19b-4dc4-9b1b-1a9a483a62c6.png" alt class="image--center mx-auto" /></p>
<p>The upper channel is the data channel (sent from the ADC), and the lower is the pulses sent from the Raspberry over the SCK channel (which dictates the read-timings).</p>
<p>We read the expected bytes into our process using the documented timings. Then convert the 24 bits to an integer value (that doesn't mean anything to us for now). After we have ensured that the readings are stable, i.e., not bouncing all over the place and giving us random values, we can move on to translating the actual output to a representation we can understand.</p>
<h3 id="heading-calibration-amp-output">Calibration &amp; Output</h3>
<p>Next, we placed the fridge on the scale, ensured all sensors were actively engaged, and noted a few readings. Then, place one kilogram on the scale and do the same thing. We should be able to determine two averages, which will help us translate pressure changes to actual grams.</p>
<p>We now have two reference values, one when the scale is empty and one with one kilogram on the scale.</p>
<p><code>points_per_gram = (one_kg_reading_avg - empty_scale_reading_avg) / 1000</code></p>
<pre><code class="lang-rust"><span class="hljs-comment">// ~/elva-byra-scale/src/hx711.rs#L153</span>
<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">translate</span></span>(&amp;<span class="hljs-keyword">self</span>, read: <span class="hljs-built_in">i32</span>) -&gt; <span class="hljs-built_in">f32</span> {
    (read <span class="hljs-keyword">as</span> <span class="hljs-built_in">f32</span> - <span class="hljs-keyword">self</span>.offset) / <span class="hljs-keyword">self</span>.points_per_gram
}
</code></pre>
<p>Great, we now use this function to translate the reading from the scale to something we can understand and reason about in the upstream services.</p>
<p>Using an offset (empty_scale) of 1076761 in our case and 1099180 (one_kg_reading). We get the following output while having a few things in the fridge that would be ~10 kg of weight.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>0b</td><td>10</td><td>Grams</td></tr>
</thead>
<tbody>
<tr>
<td>000101000001101000000001</td><td>1317377</td><td>10732.68</td></tr>
<tr>
<td>000101000001100110111100</td><td>1317308</td><td>10729.60</td></tr>
<tr>
<td>000101000001101001000100</td><td>1317444</td><td>10735.67</td></tr>
</tbody>
</table>
</div><p>The scaling process reads the scale value at a fixed interval and outputs a more readable JSON structure to the <code>/tmp/byra.sock</code> so that other processes may act on the changes.</p>
<pre><code class="lang-bash">$ netcat -U /tmp/byra.sock
<span class="hljs-comment"># {"datetime":"2023-06-18T12:11:48.759024680Z","grams":3660.4785}</span>
</code></pre>
<p>The secondary process listens to this socket and pushes each new sample to <strong>AWS IoT core</strong> using <strong>MQTT</strong> and the credentials we received after deploying the infrastructure.</p>
<h3 id="heading-the-result">The Result</h3>
<p>Finally, we can ensure we're always prepared to supply our guests with cold beverages on arrival and our <strong>BoA</strong> metric remains in the safe zone.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700580328879/802556e4-7174-40c4-ba31-8daad15f42c1.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700580425921/0da974f2-060d-4a95-9801-3f45e8a6e512.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700723892321/af7a2062-bbac-4ac3-b8fd-0500b97a7078.jpeg" alt class="image--center mx-auto" /></p>
<hr />
<p>If you enjoyed this post, follow me on <a target="_blank" href="https://github.com/JoelRoxell">GitHub</a>. I dabble with everything related to fully managed serverless solutions on AWS, embedded stuff, and Rust.</p>
<hr />
<div class="hn-embed-widget" id="leadfeeder"></div><p> <a target="_blank" href="https://elva-group.com">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Get your idea deployed to prod already]]></title><description><![CDATA[You know that nifty idea you've been thinking about? The website that would revolutionize how you rule a small country/go grocery shopping? Whatever your idea is you can't share it with anyone because it's still in your head. It's time to do somethin...]]></description><link>https://blog.elva-group.com/deploy-to-prod-already</link><guid isPermaLink="true">https://blog.elva-group.com/deploy-to-prod-already</guid><category><![CDATA[AWS]]></category><category><![CDATA[React]]></category><category><![CDATA[sst]]></category><category><![CDATA[vite]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Alvin Johansson]]></dc:creator><pubDate>Tue, 05 Sep 2023 08:24:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693203447839/98c4a6a7-fc84-4f3c-b89c-abdf9311ba38.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You know that nifty idea you've been thinking about? The website that would revolutionize how you rule a small country/go grocery shopping? Whatever your idea is you can't share it with anyone because it's still in your head. It's time to do something about it. Take 15 minutes and get it deployed serverlessly and forever hanging over your head as something you "should finish some time".</p>
<p>For this tutorial you need:</p>
<ul>
<li><p>an AWS account</p>
</li>
<li><p>AWS credentials configured correctly</p>
</li>
<li><p>node.js 16</p>
</li>
<li><p>npm 7</p>
</li>
</ul>
<p>We'll start with creating a project with <a target="_blank" href="https://docs.sst.dev/">SST</a>. A very neat thing with SST is that you can choose your preferred frontend framework to go with it. (If you already have a frontend repository set up for your idea you can install SST in "drop-in mode"). For this tutorial I'm creating a basic standalone React + Vite app.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚖</div>
<div data-node-type="callout-text">You can read more about how SST compares to other AWS frameworks in Sebastian Bille's post <a target="_blank" href="https://blog.elva-group.com/serverless-frameworks-for-2023#heading-sst">here</a>.</div>
</div>

<pre><code class="lang-bash">npx create-sst@latest my-great-project
<span class="hljs-built_in">cd</span> my-great-project
npm install
</code></pre>
<p>Then you can go ahead and open it in your editor of choice.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> packages
npm create vite@latest
</code></pre>
<p>When asked about the name of the project, call it "web". This creates the basic website in the <code>packages/web</code> folder. Now we need to add a construct for the site in your stack. Open <code>stacks/MyStack.ts</code> and add the StaticSite construct beneath the <code>bus.subscribe()</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> web = <span class="hljs-keyword">new</span> StaticSite(stack, <span class="hljs-string">"web"</span>, {
  path: <span class="hljs-string">"packages/web"</span>,
  buildOutput: <span class="hljs-string">"dist"</span>,
  buildCommand: <span class="hljs-string">"npm run build"</span>,
  environment: {
    VITE_APP_API_URL: api.url,
  },
});

stack.addOutputs({
  ApiEndpoint: api.url,
  CloudfrontUrl: web.url
});
</code></pre>
<p>It's time to start Vite locally together with the resources set up in your stack. In your root directory run this:</p>
<pre><code class="lang-bash">npx sst dev
</code></pre>
<p>After it has finished bootstrapping and deploying, open another terminal and run this:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> packages/web
npm i
npx sst <span class="hljs-built_in">bind</span> vite
</code></pre>
<p>And now you're ready to develop your website as you see wish. Change the color of the background or add a div with a funky quote or something. Next up is deploying this.</p>
<p>It's super simple.</p>
<pre><code class="lang-bash">npx sst deploy --stage prod
</code></pre>
<p>That's about it.</p>
<p>Considering you bought the domain for the page years ago when you first had the idea it's time to put it in use. Either transfer ownership of the domain over to this AWS account or create a Hosted Zone in Route 53 with the domain. Then when it's ready just modify the Site construct like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> web = <span class="hljs-keyword">new</span> StaticSite(stack, <span class="hljs-string">"web"</span>, {
  path: <span class="hljs-string">"packages/web"</span>,
  buildOutput: <span class="hljs-string">"dist"</span>,
  buildCommand: <span class="hljs-string">"npm run build"</span>,
  customDomain: {
    domainName: <span class="hljs-string">'verycoolwebsite.com'</span>,
    domainAlias: <span class="hljs-string">'www.verycoolwebsite.com'</span>
  },
  environment: {
    VITE_APP_API_URL: api.url,
  },
});

stack.addOutputs({
  ApiEndpoint: api.url,
  CloudfrontUrl: web.url,
  CustomDomain: web.customDomainUrl
});
</code></pre>
<p>Deploy and check out your site. Send it to all your friends! At least you have a nice-looking url.</p>
<hr />
<p>If you enjoyed this post you could follow me on 𝕏 at <a target="_blank" href="https://twitter.com/PaliagoAlvin">@Paliago</a>. I mostly engage with the serverless community and post pictures of my pets.</p>
<hr />
<div class="hn-embed-widget" id="leadfeeder"></div><p> <a target="_blank" href="https://elva-group.com">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Unlocking Scalability: Harnessing the Power of SQS and Lambda Integration]]></title><description><![CDATA[AWS Simple Queue Service (SQS) provides a reliable and scalable messaging solution, while AWS Lambda offers serverless computing capabilities. Combining these two services can enable robust and event-driven architectures. In this blog post, we will e...]]></description><link>https://blog.elva-group.com/unlocking-scalability-harnessing-the-power-of-sqs-and-lambda-integration</link><guid isPermaLink="true">https://blog.elva-group.com/unlocking-scalability-harnessing-the-power-of-sqs-and-lambda-integration</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws lambda]]></category><category><![CDATA[SQS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Cloud]]></category><dc:creator><![CDATA[Christoffer Pozeus]]></dc:creator><pubDate>Mon, 26 Jun 2023 06:22:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Iq9SaJezkOE/upload/b5288f48ba93c10915c9fbe452f7ec75.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS Simple Queue Service (SQS) provides a reliable and scalable messaging solution, while AWS Lambda offers serverless computing capabilities. Combining these two services can enable robust and event-driven architectures. In this blog post, we will explore the integration between SQS and Lambda in-depth, understanding the mechanisms behind message consumption and processing. We will delve into the options available for processing messages individually or in batches, highlighting the advantages and considerations for each approach. Additionally, we will discuss noteworthy configurations for Lambda event source mappings and SQS, such as batch size, batch window, maximum concurrency, and queue visibility timeout.</p>
<h2 id="heading-understanding-sqs-and-lambda-integration">Understanding SQS and Lambda Integration</h2>
<p>SQS and Lambda provide a powerful integration for building scalable and event-driven architectures. With Lambda event source mappings, you can process messages from SQS queues asynchronously, decoupling components and ensuring reliable message delivery. When a Lambda function subscribes to an SQS queue, it uses a polling mechanism to wait for messages to arrive. Lambda consumes messages in batches. For each batch, Lambda triggers your function once. If there are more messages in the queue, Lambda can scale up to 1,000 concurrent functions, adding up to 60 functions per minute. Upon successful processing, Lambda automatically removes the messages from the queue.</p>
<h2 id="heading-consume-messages-from-sqs-using-lambda">Consume messages from SQS using Lambda</h2>
<p>When working with SQS, you can process messages individually or in batches. The Lambda Event Source Mapping supports larger batches, allowing up to 10,000 messages or 6 MB in a single batch for standard SQS queues. In contrast, the SDK is limited to 10 messages per API call. This is one of the reasons why you should use Lambda Event Source Mapping whenever possible when you consume messages from SQS.</p>
<p>Processing messages individually offers advantages such as faster processing and simpler error handling. However, there are situations where batch processing is more appropriate. Batch processing is beneficial when you need higher throughput, improved efficiency, or when cost optimization is important.</p>
<h3 id="heading-processing-individual-messages">Processing individual messages</h3>
<p>Consuming individual SQS messages is relatively simple in Lambda, each message is treated as an independent event triggering the execution of your Lambda function. It is still important to implement appropriate error handling to capture any exceptions or errors that may occur during message processing. When a message is successfully processed by your Lambda function, Lambda will automatically delete the message from the queue. However, if an error is caught during the execution of the function, the message will be returned to the queue for further processing or retries. This ensures that messages are not lost in case of errors and allows for proper handling and reprocessing of failed messages.</p>
<p>Here's an example of a Lambda Function written in TypeScript that consumes individual messages:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { SQSHandler, SQSEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-lambda"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler: SQSHandler = <span class="hljs-keyword">async</span> (event: SQSEvent) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> record <span class="hljs-keyword">of</span> event.Records) {
      <span class="hljs-keyword">const</span> message = record.body;
      <span class="hljs-comment">// Process the message here</span>
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Processing message:"</span>, message);
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error processing SQS messages:"</span>, error);
    <span class="hljs-keyword">throw</span> error;
  }
};
</code></pre>
<h3 id="heading-processing-batches-of-messages">Processing batches of messages</h3>
<p>When Lambda processes a batch of messages from an SQS queue, the messages remain in the queue but are temporarily hidden based on the queue's visibility timeout (more on this later in this blog post). If your Lambda function successfully handles and processes the batch, Lambda automatically deletes the messages from the queue. However, if your function encounters an error while processing a batch, all messages in that batch reappear in the queue, making them visible again.</p>
<p>To ensure that messages are not processed multiple times, you have a couple of options. Firstly, you can configure your event source mapping to include <code>ReportBatchItemFailures</code> in the function response. This allows you to handle and track failed messages within your function code, this is the recommended approach when dealing with batches. Alternatively, you can utilize the Amazon SQS API action called DeleteMessage to explicitly remove messages from the queue as your Lambda function successfully processes them. The use of this API action ensures that messages are not reprocessed inadvertently.</p>
<p>I will provide two examples of how you can utilize the ReportBatchItemFailures functionality to return partial failures of messages, this will ensure that our function doesn't process messages more than once. I will demonstrate how this can be done by constructing a batchItemFailures function response as well as using the <a target="_blank" href="http://npmjs.com/package/@middy/core">Middy</a> middleware.</p>
<blockquote>
<p><strong><em>⚠️</em></strong> <em>Please note that you need to configure your Lambda event source mapping to include batch item failures for these examples to work.</em></p>
</blockquote>
<p>Here's an example of a Lambda Function written in TypeScript that consumes a batch of messages and returns BatchItemFailures in the function response:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { SQSEvent, SQSHandler, SQSBatchResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-lambda'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler: SQSHandler = <span class="hljs-keyword">async</span> (event: SQSEvent) =&gt; {
  <span class="hljs-keyword">const</span> failedMessageIds: <span class="hljs-built_in">string</span>[] = [];

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> record <span class="hljs-keyword">of</span> event.Records) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> message = record.body;
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Processing message:"</span>, message);
      <span class="hljs-comment">// Process the message here</span>
    } <span class="hljs-keyword">catch</span> (error) {
      failedMessageIds.push(record.messageId);
    }
  }

  <span class="hljs-keyword">const</span> response: SQSBatchResponse = {
    batchItemFailures: failedMessageIds.map(<span class="hljs-function">(<span class="hljs-params">id</span>) =&gt;</span> ({
      itemIdentifier: id,
    })),
  };

  <span class="hljs-keyword">return</span> response;
};
</code></pre>
<p>Here's an example of a Lambda Function written in TypeScript that consumes a batch of messages using the <a target="_blank" href="https://middy.js.org/docs/middlewares/sqs-partial-batch-failure/">sqs-partial-batch-failure</a> Middy middleware. This code simplifies the consumption of message batches. It automatically includes the failed message IDs as BatchItemFailures in the function response, eliminating the need for manual error tracking and reducing development overhead.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> middy <span class="hljs-keyword">from</span> <span class="hljs-string">'@middy/core'</span>;
<span class="hljs-keyword">import</span> sqsPartialBatchFailureMiddleware <span class="hljs-keyword">from</span> <span class="hljs-string">'@middy/sqs-partial-batch-failure'</span>;
<span class="hljs-keyword">import</span> { SQSEvent, SQSRecord } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-lambda'</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mainHandler</span>(<span class="hljs-params">event: SQSEvent</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">any</span>&gt; </span>{
  <span class="hljs-keyword">const</span> messagePromises = event.Records.map(processMessage);
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.allSettled(messagePromises);
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processMessage</span>(<span class="hljs-params">record: SQSRecord</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">any</span>&gt; </span>{
  <span class="hljs-keyword">const</span> message = record.body;
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Processing message:"</span>, message);
  <span class="hljs-comment">// Process the message here</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = middy(mainHandler).use(sqsPartialBatchFailureMiddleware());
</code></pre>
<h2 id="heading-noteworthy-configurations">Noteworthy configurations</h2>
<h3 id="heading-lambda-event-source-mapping">Lambda Event Source Mapping</h3>
<p><strong>Batch size</strong></p>
<p>The batch size (<code>BatchSize</code>) configuration determines the number of records sent to the Lambda function within each batch. For standard queues, you can set the batch size up to a maximum of 10,000 records. However, it's important to note that the total size of the batch cannot exceed 6 MB, regardless of the number of records configured.</p>
<p><strong>Batch window</strong></p>
<p>The batch window (<code>MaximumBatchingWindowInSeconds</code>) setting determines the maximum time, in seconds, that records are gathered before invoking the Lambda function. Please note that this configuration applies only to standard queues.</p>
<p><strong>Maximum Concurrency</strong></p>
<p>The maximum concurrency (<code>ScalingConfig</code>) feature enables us to set a maximum number of concurrent invocations for an Event Source Mapping, eliminating the issues caused by excessive throttling that was previously present when using reserved concurrency in Lambda. With this capability, we have gained better control over concurrency, especially when utilizing multiple Event Source Mappings with the same function.</p>
<h3 id="heading-sqs-configurations">SQS configurations</h3>
<p><strong>Queue visibility timeout</strong></p>
<p>The visibility timeout (<code>VisibilityTimeout</code>) in SQS is a setting that determines how long a message remains invisible in the queue after it has been retrieved by a consumer. When a consumer receives a message from the queue, it becomes temporarily hidden from other consumers for the duration of the visibility timeout. If you choose a batch window greater than 0 seconds, it is important to consider the increased processing time within your queue's visibility timeout. It's recommended to set the visibility timeout to at least six times your function's timeout, plus the value of the batch window (<code>MaximumBatchingWindowInSeconds</code>). This ensures sufficient time for your Lambda function to process each batch of events and handle potential retries caused by throttling errors. If you for example would have function timeout of 30 seconds and a batch window of 20 seconds. This will be set to (30 x 6) + 20 = 200 seconds.</p>
<p>You can read more about this here: <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-eventsource">https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html#events-sqs-eventsource</a></p>
<p><strong>Dead-letter queues (DLQs)</strong></p>
<p>When a message fails to be processed by Lambda, it is returned to the queue for retrying. However, to prevent the message from being added to the queue multiple times and causing unnecessary consumption of Lambda resources, it is recommended to designate a Dead Letter Queue (DLQ) and send failed messages there. To control the number of retries for failed messages, you can set the <code>Maximum receives</code> value for the DLQ. Once a message has been re-added to the queue more times than the Maximum receives value, it will be moved to the DLQ. This allows you to process these failed messages at a later time, separate from the main queue.</p>
<p>You can read more about when to use a DLQ here: <a target="_blank" href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html#sqs-dead-letter-queues-when-to-use">https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html#sqs-dead-letter-queues-when-to-use</a></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, the integration between SQS and Lambda provides a powerful and scalable solution for building event-driven architectures. By leveraging event source mappings, configuring batch processing, and utilizing noteworthy features like dead-letter queues and visibility timeouts, you can ensure reliable message processing and optimize resource utilization. Embrace this integration to unlock the full potential of distributed systems and create resilient applications that scale effortlessly.</p>
<hr />
<div class="hn-embed-widget" id="leadfeeder"></div><p> </p>
<p><a target="_blank" href="https://elva-group.com/"><strong>Elva</strong></a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[Using AppConfig Feature Flags with Organization Units]]></title><description><![CDATA[AWS AppConfig is a tool under AWS Systems Manager which handles, you guessed it, configurations of your applications. One of the major features are Feature Flags which enables you to toggle a feature wherever you need to within your solutions. It's a...]]></description><link>https://blog.elva-group.com/using-appconfig-feature-flags-with-organization-units</link><guid isPermaLink="true">https://blog.elva-group.com/using-appconfig-feature-flags-with-organization-units</guid><category><![CDATA[AWS]]></category><category><![CDATA[AppConfig]]></category><category><![CDATA[  feature flags]]></category><category><![CDATA[serverless]]></category><category><![CDATA[#AWSOrganization]]></category><dc:creator><![CDATA[Alvin Johansson]]></dc:creator><pubDate>Tue, 11 Apr 2023 06:34:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1679652792377/e53e85ed-ffbf-4ae5-a1de-8f9f001d0f49.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS AppConfig is a tool under AWS Systems Manager which handles, you guessed it, configurations of your applications. One of the major features are Feature Flags which enables you to toggle a feature wherever you need to within your solutions. It's a very nifty feature, way more capable than just a boolean toggle and we can use it in both front- and backend of our solutions.</p>
<p>The use-case we explore in this post is about enabling a central account (Tools) to handle feature flags for all environments and services within an Organization. The service we want to set up feature flags for in this post is called ElvaFlags Inc and is located in it's own accounts (dev and prod). This would for example enable several services to look for the same flag and can be used to enable the feature all over your application at once.</p>
<h3 id="heading-organization-units">Organization Units</h3>
<p>AWS Organizations and Organization Units are a great way to handle your multi-account strategy. <a target="_blank" href="https://blog.elva-group.com/managing-aws-at-scale-multi-account-strategy">Our own Tobias wrote a great post about it</a> and also helped some during the real life scenario when we implemented this. The tl;dr you need to for this post is:</p>
<ul>
<li><p>Organizations are the outer shell of the account structure. All accounts are within this.</p>
</li>
<li><p>Organization Units can be viewed as "folders" in which you can group your accounts. OUs can exist within other OUs.</p>
</li>
</ul>
<h2 id="heading-the-solution">The Solution</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679663399098/1c04676b-d94b-4510-bedf-5f50d2b85302.png" alt class="image--center mx-auto" /></p>
<p>We'll start with setting up an AppConfig application in the account where we want to centrally manage the feature flags. In our case the Tools account.</p>
<ol>
<li><p>Create an Application. Applications are a logical grouping of Configuration Profiles which in turn can have flags. A suggestion would be to name it after your application/solution you're developing.</p>
</li>
<li><p>Next up is creating an Environment. AWS describes them as "logical deployment groups of AppConfig targets" so a name like <code>dev</code> or <code>prod</code> would make sense here.</p>
</li>
<li><p>The next step is to create a Configuration Profile. Configuration profiles are either collections of feature flags or a "Freeform configuration". In this post we're focusing on FFs.</p>
</li>
<li><p>In the configuration profile you create the flags you want to toggle.</p>
</li>
<li><p>Save the configuration with the flags. Give it a version label or let AWS handle it for you.</p>
</li>
<li><p>Create an IAM Role in the same account with a policy which allows the actions: <code>GetLatestConfiguration</code> and <code>StartConfigurationSession</code>. Example policy:</p>
</li>
</ol>
<pre><code class="lang-bash">{
    <span class="hljs-string">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-string">"Statement"</span>: [
        {
            <span class="hljs-string">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-string">"Action"</span>: [
                <span class="hljs-string">"appconfig:GetLatestConfiguration"</span>,
                <span class="hljs-string">"appconfig:StartConfigurationSession"</span>
            ],
            <span class="hljs-string">"Resource"</span>: <span class="hljs-string">"*"</span>
        }
    ]
}
</code></pre>
<ol>
<li>Create a trust relationship for the role. It should allow the the action <code>AssumeRole</code> on all AWS accounts (wait for it), but have a condition. The condition is a <code>StringLike</code> where you can define what OUs you want to allow access to the FFs. Example trust relationship:</li>
</ol>
<pre><code class="lang-bash">{
    <span class="hljs-string">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-string">"Statement"</span>: [
        {
            <span class="hljs-string">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-string">"Principal"</span>: {
                <span class="hljs-string">"AWS"</span>: <span class="hljs-string">"*"</span>
            },
            <span class="hljs-string">"Action"</span>: <span class="hljs-string">"sts:AssumeRole"</span>,
            <span class="hljs-string">"Condition"</span>: {
                <span class="hljs-string">"ForAnyValue:StringLike"</span>: {
                    <span class="hljs-string">"aws:PrincipalOrgPaths"</span>: [
                        <span class="hljs-string">"o-orgiId/r-rootId/ou-WorkloadsId*"</span>
                    ]
                }
            }
        }
    ]
}
</code></pre>
<p>In the example here I'm allowing the AssumeRole action on all OrgPaths that begin with <code>my organization id/organization root id/WorkloadsOuId*</code> which essentially means all accounts within my Workloads OU. Since it's an array you can add several OUs if you need to. Now everything is ready in the Tools account!</p>
<p>To then fetch the feature flags in your front- or backend you need to first get the STS token and then use that as the credentials for the AppConfig client. Here is an example code snippet to finish the writeup:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> stsClient = <span class="hljs-keyword">new</span> STSClient({
  region: REGION,
});

<span class="hljs-keyword">interface</span> Settings {
  applicationID: <span class="hljs-built_in">string</span>;
  configurationProfileID: <span class="hljs-built_in">string</span>;
  environmentID: <span class="hljs-built_in">string</span>;
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getAppConfiguration = <span class="hljs-keyword">async</span> (
  settings: Settings,
) =&gt; {
  <span class="hljs-keyword">const</span> stsResponse = <span class="hljs-keyword">await</span> stsClient.send(
    <span class="hljs-keyword">new</span> AssumeRoleCommand({
      RoleArn: READ_FLAGS_ROLE,
      RoleSessionName: ASSUME_ROLE_SESSION_NAME,
    }),
  );

  <span class="hljs-keyword">if</span> (!stsResponse.Credentials) {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  <span class="hljs-keyword">const</span> { AccessKeyId, SecretAccessKey, SessionToken } =
    stsResponse.Credentials;

  <span class="hljs-keyword">if</span> (!AccessKeyId || !SecretAccessKey) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Failed to retrieve <span class="hljs-subst">${ASSUME_ROLE_SESSION_NAME}</span> `</span>);
  }

  <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> AppConfigDataClient({
    region: REGION,
    credentials: {
      accessKeyId: AccessKeyId,
      secretAccessKey: SecretAccessKey,
      sessionToken: SessionToken,
    },
  });
  <span class="hljs-keyword">const</span> sessionResponse = <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> StartConfigurationSessionCommand({
      ApplicationIdentifier: settings.applicationID,
      ConfigurationProfileIdentifier: settings.configurationProfileID,
      EnvironmentIdentifier: settings.environmentID,
    }),
  );
  <span class="hljs-keyword">const</span> configurationResponse = <span class="hljs-keyword">await</span> client.send(
    <span class="hljs-keyword">new</span> GetLatestConfigurationCommand({
      ConfigurationToken: sessionResponse.InitialConfigurationToken,
    }),
  );

  <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(
    Buffer.from(configurationResponse.Configuration ?? <span class="hljs-string">''</span>).toString(<span class="hljs-string">'utf-8'</span>),
  );
};
</code></pre>
<p>For additional reading on retrieving and using a configuration AWS has a <a target="_blank" href="https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-retrieving-the-configuration.html">detailed page</a> in their docs.</p>
<p>That's it! Hopefully you learned something!</p>
<hr />
<p>If you enjoyed this post you could follow me on twitter at <a target="_blank" href="https://twitter.com/PaliagoAlvin">@Paliago</a> where I honestly mostly retweet dumb AWS related things. There might be some posts on the horizon as well 😶‍🌫️</p>
<hr />
<div class="hn-embed-widget" id="leadfeeder"></div><p> <a target="_blank" href="https://elva-group.com">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item><item><title><![CDATA[💸 How to save thousands of dollars on AWS WAF]]></title><description><![CDATA[AWS WAF, the managed Web Application Firewall, is a commonly used service to secure APIs, load balancers, and applications.
But because of how the pricing model is set up for WAF, the costs can quickly spiral out of control when adhering to the AWS b...]]></description><link>https://blog.elva-group.com/how-to-save-thousands-of-dollars-on-aws-waf</link><guid isPermaLink="true">https://blog.elva-group.com/how-to-save-thousands-of-dollars-on-aws-waf</guid><category><![CDATA[AWS]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Security]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Sebastian Bille]]></dc:creator><pubDate>Mon, 03 Apr 2023 10:49:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1680518617675/48ed7efe-ef84-4bca-82c0-a63bd245da4a.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS WAF, the managed Web Application Firewall, is a commonly used service to secure APIs, load balancers, and applications.</p>
<p>But because of how the pricing model is set up for WAF, the costs can quickly spiral out of control when adhering to the AWS best practices on multi-account strategies. Namely, the cost of merely having a Web ACL created in an account is $5 per month, and then it's $1 per rule added to that Web ACL. It's a small amount for each account but snowballs as you have hundreds or thousands of accounts, which isn't uncommon in a larger organization, and before you know it, you're paying thousands of dollars just for having these rules existing.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680264908976/21b6f35c-c215-4db6-ba5e-8ed9239e0717.png" alt="The AWS WAF pricing page highlighting the Web ACL and Rule prices" class="image--center mx-auto" /></p>
<h2 id="heading-aws-shield-to-the-rescue">🛡️ AWS Shield to the rescue</h2>
<p>AWS Shield is a managed service that protects against DDOS attacks. The "Standard" version of Shield is free of charge, and all AWS users automatically benefit from this service. There's, however, also an "Advanced" version of this service that many might have heard about, but few actually have any hands-on experience with, as it's $3000 per month with a minimum of 12 months commitment - and there's no free tier. All accounts in the AWS Organization will benefit from the same subscription from the management account, though.</p>
<p>Shield Advanced offers a <a target="_blank" href="https://docs.aws.amazon.com/waf/latest/developerguide/ddos-advanced-summary.html">higher level of protection</a>, you get access to the <a target="_blank" href="https://docs.aws.amazon.com/waf/latest/developerguide/ddos-srt-support.html">Shield Response Team</a>, and a few other features. But, the somewhat unexpected key feature in this case actually lies within the pricing model:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1680265036182/9e40a6cc-0378-49c0-a58c-8294a9426232.png" alt class="image--center mx-auto" /></p>
<p>Did you catch that? Let's look again.</p>
<blockquote>
<p>Because the Amazon CloudFront Distribution is already protected under AWS Shield Advanced, <strong>there are no additional charges for AWS WAF web ACL, rule</strong> or request fees.</p>
</blockquote>
<p>If a resource in an account is protected by Shield Advanced, the WAF Web ACL and Rule costs for that account are waived. But a Web ACL and its rules are not created as part of a resource; they're created individually and are then attached to a resource, so how does that actually work? It's not very intuitive, but the costs are waived for a specific Web ACL as long as at least one resource that Shield Advanced protects has the Web ACL attached.</p>
<blockquote>
<p>⚠️ Note that AWS Shield Advanced Data Transfer and other AWS WAF fees still apply. Managed rule groups such as Targeted Bots and Account Takeover Prevention are also not included in the Shield Advanced subscription.</p>
</blockquote>
<p>What this means in practice is that if you're spending over $3000 per month on Web ACL and Rule fees, you can effectively cap those costs at $3000 and prevent them from spiraling further as your number of AWS accounts grows by subscribing to AWS Shield and enrolling your resources. And as an added bonus, you'll benefit from improved DDOS protection. Because of all this, it can be a good idea to automatically create a dummy resource that uses the Web ACL when vending new accounts - because otherwise, the fees won't be waived until a resource that does is deployed. Another important aspect is that you can use an AWS FirewallManager Policy, at no additional cost, to automatically subscribe all new accounts to Shield Advanced and protect all resources that use a WAF.</p>
<p>In conclusion, AWS Shield Advanced can be a game-changer when it comes to reducing the costs of AWS WAF. By protecting resources with Shield Advanced, the costs for WAF Web ACL and Rule are waived, which can save thousands of dollars for organizations with a large number of AWS accounts. While the high upfront cost of Shield Advanced may be daunting, the higher level of protection, access to the Shield Response Team, and, primarily, the cost savings on WAF can make it a fantastic hidden investment for organizations that heavily rely on AWS.</p>
<p>You can read more about AWS Shield Advanced <a target="_blank" href="https://aws.amazon.com/shield/">here</a> and about WAF best practices <a target="_blank" href="https://docs.aws.amazon.com/waf/latest/developerguide/waf-managed-protections-best-practices.html">here</a>!</p>
<hr />
<p>Hi there, I'm <a target="_blank" href="https://sebastianbille.com/"><strong>Sebastian Bille</strong></a>! If you enjoyed this post or just want a constant feed of memes, AWS/serverless talk, and the occasional new blog post, make sure to follow me on Twitter at <a target="_blank" href="https://twitter.com/TastefulElk"><strong>@TastefulElk</strong></a> or on <a target="_blank" href="https://www.linkedin.com/in/sebastianbille/"><strong>LinkedIn</strong></a> 👋</p>
<hr />
<div class="hn-embed-widget" id="leadfeeder"></div><p> <a target="_blank" href="https://elva-group.com">Elva</a> is a serverless-first consulting company that can help you transform or begin your AWS journey for the future</p>
]]></content:encoded></item></channel></rss>