<?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[Stefan Olaru]]></title><description><![CDATA[Cloud Architect & Developer. I specialize in AWS Serverless solutions and Event-Driven architectures. 
Sharing my learnings from helping startups build, launch,]]></description><link>https://blog.stefanolaru.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 18 Oct 2026 00:44:30 GMT</lastBuildDate><atom:link href="https://blog.stefanolaru.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Streaming AI Responses on AWS Lambda]]></title><description><![CDATA[While building Creditaki, an AI-powered credit assistant, I had two non-negotiable requirements: a fully serverless architecture and real-time streaming for the best user experience.
Users expect AI responses to appear token by token, like ChatGPT. B...]]></description><link>https://blog.stefanolaru.com/streaming-ai-responses-on-aws-lambda</link><guid isPermaLink="true">https://blog.stefanolaru.com/streaming-ai-responses-on-aws-lambda</guid><category><![CDATA[aws lambda]]></category><category><![CDATA[AI]]></category><category><![CDATA[streaming]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Node.js]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Fri, 19 Sep 2025 14:34:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758292319990/0c6aa8dc-c070-456e-b2e0-16a7b9a18f18.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While building <a target="_blank" href="https://creditaki.ro">Creditaki</a>, an AI-powered credit assistant, I had two non-negotiable requirements: a <strong>fully serverless</strong> architecture and <strong>real-time streaming</strong> for the best user experience.</p>
<p>Users expect AI responses to appear token by token, like ChatGPT. But AWS Lambda and API Gateway aren't designed for long-lived, streaming responses out of the box.</p>
<p>I wanted <strong>Server-Sent Events (SSE)</strong>: a simple way to stream AI responses into a chat UI without managing servers. Here's how Lambda Response Streaming solved both problems.</p>
<h2 id="heading-the-problem">The Problem</h2>
<p>Staying serverless while delivering real-time UX meant every common approach had deal-breaking limitations:</p>
<ul>
<li><p><strong>API Gateway HTTP + AWS Lambda (proxy)</strong></p>
<ul>
<li><p>30-second timeout limit for HTTP APIs</p>
</li>
<li><p>No true streaming, responses buffer until function completion</p>
</li>
</ul>
</li>
<li><p><strong>API Gateway WebSockets</strong></p>
<ul>
<li>Requires complex connection management and keepalive logic</li>
</ul>
</li>
<li><p><strong>Async job + polling</strong></p>
<ul>
<li>Poor UX, users stare at loading spinners while clients poll for updates</li>
</ul>
</li>
</ul>
<p>What I wanted was <strong>progressive delivery</strong>: send partial <strong>LLM streaming responses</strong> immediately, without breaking my serverless constraint</p>
<h2 id="heading-the-solution-lambda-response-streaming">The Solution: Lambda Response Streaming</h2>
<p>AWS introduced Lambda Response Streaming about two years ago (2023), but the ideal use cases weren't immediately obvious.</p>
<p><strong>Interacting with AI models turned out to be the perfect fit.</strong></p>
<p>Lambda Response Streaming lets functions send data directly to clients instead of buffering everything until completion. Combined with the rise of streaming LLM models like those in AWS Bedrock, this creates exactly what users expect from modern conversational AI interfaces.</p>
<p>My architecture handles privacy compliance while leveraging the best AI models:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758288722740/07512aca-c84c-4e26-8a2c-df03a5e04075.png" alt class="image--center mx-auto" /></p>
<p>The flow is straightforward but powerful:</p>
<ol>
<li><p>User request flows through CloudFront to the Lambda Function URL</p>
</li>
<li><p>Comprehend extracts and anonymizes personal financial data</p>
</li>
<li><p>System prompt is read from S3 bucket (if not cached)</p>
</li>
<li><p>User message is stored in DynamoDB</p>
</li>
<li><p>Anonymized data sent to Bedrock for AI analysis</p>
</li>
<li><p>Bedrock streams tokens back through Lambda as Server-Sent Events</p>
</li>
<li><p>User gets real-time responses without personal data leaving the EU</p>
</li>
</ol>
<p><strong>Why CloudFront?</strong> It provides out-of-the-box country-level access control. Since <strong>Creditaki</strong> targets a single market, I wanted to prevent non-targeted users from consuming expensive AI tokens.</p>
<p>The timing worked perfectly: Lambda Response Streaming matured just as AI model streaming became the expected UX standard.</p>
<h2 id="heading-implementation-with-nodejs-and-typescript">Implementation with Node.js and TypeScript</h2>
<h3 id="heading-infrastructure-as-code">Infrastructure as Code</h3>
<p>I’m using AWS CDK to create the Lambda function and Lambda Function URL resources.</p>
<p>The Lambda Function URL requires specific configuration for response streaming:<br /><code>invokeMode: RESPONSE_STREAM</code> enables the streaming capability. By default, Lambda will buffer the entire response.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> chatFunction = <span class="hljs-keyword">new</span> nodeLambda.NodejsFunction(<span class="hljs-built_in">this</span>, <span class="hljs-string">"ChatFunction"</span>, {
  runtime: lambda.Runtime.NODEJS_20_X,
  timeout: cdk.Duration.seconds(<span class="hljs-number">180</span>), <span class="hljs-comment">// Extended timeout for AI processing</span>
  memorySize: <span class="hljs-number">256</span>,
  environment: {
    DDB_TABLE: <span class="hljs-built_in">this</span>.ddbTable.tableName,
    S3_BUCKET: <span class="hljs-built_in">this</span>.s3Bucket.bucketName,
    <span class="hljs-comment">// other env </span>
  },
});

<span class="hljs-keyword">const</span> chatFunctionUrl = chatFunction.addFunctionUrl({
  authType: lambda.FunctionUrlAuthType.NONE,
  invokeMode: lambda.InvokeMode.RESPONSE_STREAM, <span class="hljs-comment">// this is the setting</span>
  cors: {
    allowedOrigins: [<span class="hljs-string">"*"</span>],
    allowedMethods: [lambda.HttpMethod.GET, lambda.HttpMethod.POST],
  },
});
</code></pre>
<p>The 180-second timeout accommodates AI workflows where models use external tools. Bedrock models can call APIs, search knowledge bases, or perform multi-step reasoning that takes time to complete.</p>
<h3 id="heading-application-code">Application Code</h3>
<p>The core of Lambda Response Streaming is the <code>streamifyResponse</code> wrapper and custom stream processing.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Lambda handler</span>
<span class="hljs-built_in">exports</span>.handler = awslambda.streamifyResponse(
  <span class="hljs-keyword">async</span> (event, responseStream) =&gt; {
    <span class="hljs-keyword">await</span> pipeline(
      Readable.from(sendMessage(event)),
      responseStream
    );
  }
);
</code></pre>
<p>Since I wanted to use <strong>POST requests</strong> for chat messages, I used <code>fetch</code> with manual stream processing instead of EventSource (which only supports GET).</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// frontend integration</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendChatMessage</span>(<span class="hljs-params">message</span>) </span>{
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/c'</span>, {
    method: <span class="hljs-string">'POST'</span>,
    headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
    body: <span class="hljs-built_in">JSON</span>.stringify({ message })
  });

  <span class="hljs-keyword">const</span> reader = response.body.getReader();
  <span class="hljs-keyword">const</span> decoder = <span class="hljs-keyword">new</span> TextDecoder();

  <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
    <span class="hljs-keyword">const</span> { done, value } = <span class="hljs-keyword">await</span> reader.read();
    <span class="hljs-keyword">if</span> (done) <span class="hljs-keyword">break</span>;

    <span class="hljs-keyword">const</span> chunk = decoder.decode(value);
    <span class="hljs-keyword">const</span> lines = chunk.split(<span class="hljs-string">'\n'</span>);

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> line <span class="hljs-keyword">of</span> lines) {
      <span class="hljs-keyword">if</span> (line.startsWith(<span class="hljs-string">'data: '</span>)) {
        <span class="hljs-keyword">const</span> data = <span class="hljs-built_in">JSON</span>.parse(line.slice(<span class="hljs-number">6</span>));
        <span class="hljs-keyword">if</span> (data.type === <span class="hljs-string">'token'</span>) {
          appendToChat(data.content); <span class="hljs-comment">// Update UI in real-time</span>
        }
      }
    }
  }
}
</code></pre>
<p>This gives users the immediate feedback they expect from AI interactions - tokens appear as they're generated, just like ChatGPT.</p>
<p><strong>Why Manual Stream Processing Instead of EventSource?</strong></p>
<p>The browser's <code>EventSource</code> API is the standard way to consume Server-Sent Events, but it has a critical limitation: <strong>it only supports GET requests</strong>.</p>
<p>In my chat implementation, I need both methods:</p>
<ul>
<li><p><strong>POST /c</strong> - Send new messages with user input</p>
</li>
<li><p><strong>GET /c</strong> - Retrieve chat session history</p>
</li>
</ul>
<pre><code class="lang-javascript"><span class="hljs-comment">// This won't work for sending messages - EventSource only supports GET</span>
<span class="hljs-keyword">const</span> eventSource = <span class="hljs-keyword">new</span> EventSource(<span class="hljs-string">'/c'</span>, {
  <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,  <span class="hljs-comment">// ❌ Not supported</span>
  <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">message</span>: <span class="hljs-string">"Hello"</span> })
});

<span class="hljs-comment">// Solution: Use fetch for both GET and POST with consistent stream processing</span>
<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/c'</span>, {
  <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,  <span class="hljs-comment">// ✅ Send messages</span>
  <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">message</span>: <span class="hljs-string">"Hello"</span> })
});

<span class="hljs-comment">// Same streaming logic works for GET requests too</span>
<span class="hljs-keyword">const</span> sessionResponse = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/c'</span>, {
  <span class="hljs-attr">method</span>: <span class="hljs-string">'GET'</span>  <span class="hljs-comment">// ✅ Get chat history</span>
});
</code></pre>
<p>Using <code>fetch</code> for both endpoints keeps the streaming implementation consistent across your API, rather than mixing EventSource (GET only) with custom fetch logic (POST).</p>
<p>Many developers miss this nuance when building chat interfaces - they assume EventSource will handle all SSE scenarios.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<p>Lambda Response Streaming transformed how I deliver AI experiences at Creditaki:</p>
<ul>
<li><p><strong>Real-time LLM streaming</strong> without complex infrastructure</p>
</li>
<li><p><strong>AWS Bedrock integration</strong> with privacy compliance</p>
</li>
<li><p><strong>Cost control</strong> through CloudFront geo-blocking</p>
</li>
<li><p><strong>Simple Node.js implementation</strong> compared to WebSocket alternatives</p>
</li>
</ul>
<p>In short: ChatGPT-style streaming, on a 100% serverless stack.</p>
<p>The pattern works well for any AI application where users expect streaming responses. As more developers build AI workflows, Lambda Response Streaming provides a clean serverless solution that scales automatically.</p>
<p>Check out <a target="_blank" href="https://creditaki.ro">Creditaki - Credit AI Assistant</a> to see this streaming pattern in action.</p>
]]></content:encoded></item><item><title><![CDATA[Debugging AWS SES templates]]></title><description><![CDATA[AWS SES is extremely powerful but it's likely surpassed only by AWS Cognito in terms of opacity and debugging difficulty. It usually works, but when it doesn't, it's a time-sink.
I've been working on migrating a client's on-premise shopping cart solu...]]></description><link>https://blog.stefanolaru.com/debugging-aws-ses-templates</link><guid isPermaLink="true">https://blog.stefanolaru.com/debugging-aws-ses-templates</guid><category><![CDATA[AWS]]></category><category><![CDATA[email]]></category><category><![CDATA[SES]]></category><category><![CDATA[debugging]]></category><category><![CDATA[transactional]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Fri, 23 Feb 2024 12:05:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/JISTS0ZCXTo/upload/e21ee3c0f9e9d1e6d016f67d06edb777.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AWS SES is extremely powerful but it's likely surpassed only by AWS Cognito in terms of opacity and debugging difficulty. It usually works, but when it doesn't, it's a time-sink.</p>
<p>I've been working on migrating a client's on-premise shopping cart solution to AWS, and one of the tasks was to refactor the email notifications feature to use AWS SES instead of Mailgun.</p>
<p>For transactional emails such as order confirmations, shipping notifications, or payment receipts the email templates work like magic, you set it and forget it.</p>
<p>Both Mailgun and AWS SES support <strong>email templates</strong>, they're based on the Handlebars template system and work pretty much the same, or at least they should.</p>
<h3 id="heading-the-problem">The problem</h3>
<p>Post migration all transactional emails are getting sent but not delivered to the inbox. No clue of what's happening, no errors whatsoever.</p>
<h3 id="heading-debugging-steps">Debugging steps</h3>
<p>I started to investigate the possible causes:</p>
<ul>
<li><p>✅ the account is moved out of sandbox mode</p>
</li>
<li><p>✅ the sender's identity is verified</p>
</li>
<li><p>✅ the DKIM is set</p>
</li>
<li><p>✅ <code>SendTemplatedEmailCommand</code> returns a successful message</p>
</li>
<li><p>✅ sends count against <em>Daily email usage</em> in the SES dashboard</p>
</li>
<li><p>✅ checked the Spam folder, nothing</p>
</li>
</ul>
<p>Everything looks fine, yet emails are not reaching the inbox.</p>
<p>I did a quick test with a one-line template and it works. <strong>Bingo! 🥳</strong> So there's a problem with the email template rendering.</p>
<p>Ok, great, but <strong>how do I debug the AWS SES template</strong>? The syntax looks correct!</p>
<h3 id="heading-the-solution">The solution</h3>
<p>I quickly looked for some tools but didn't find anything meaningful, so went to the docs and found this CLI method <code>aws sesv2 test-render-email-template</code> which saved my day!</p>
<p>You can use this command with the <code>--template-name</code> and <code>--template-data</code><br />parameters and get a preview of the email content or an error.</p>
<p>Of course, it was an email template rendering error and this is the only way I could figure it out.</p>
<p><code>An error occurred (BadRequestException) when calling the TestRenderEmailTemplate operation: Attribute 'tax_amount' is not present in the rendering data.</code></p>
<p>The error is misleading because the <code>tax_amount</code> was present in my <code>--template-data</code> but had a <code>null</code> value for this specific case:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"id"</span>: <span class="hljs-string">"2cl7Nr9KoU2KvB8ADy0s1hnD111"</span>,
    <span class="hljs-attr">"customer_name"</span>: <span class="hljs-string">"Stefan Potter"</span>,
    <span class="hljs-attr">"currency"</span>: <span class="hljs-string">"USD"</span>,
    <span class="hljs-attr">"items"</span>:
        [
            {
                <span class="hljs-attr">"image"</span>: <span class="hljs-string">"https://path/to/images/product-1.jpg"</span>,
                <span class="hljs-attr">"quantity"</span>: <span class="hljs-number">1</span>,
                <span class="hljs-attr">"price"</span>: <span class="hljs-number">89.00</span>,
                <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Wireless Headphones"</span>,
                <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Simple product with available quantity."</span>,
                <span class="hljs-attr">"id"</span>: <span class="hljs-string">"HEADPHONESX"</span>
            }
        ],
    <span class="hljs-attr">"shipping_address"</span>: <span class="hljs-string">"Behind Corner 5&lt;br /&gt;Crater 12345&lt;br /&gt;Moon"</span>,
    <span class="hljs-attr">"shipping_method"</span>: <span class="hljs-string">"flat"</span>,
    <span class="hljs-attr">"shipping_amount"</span>: <span class="hljs-number">12.00</span>,
    <span class="hljs-attr">"billing_method"</span>: <span class="hljs-string">"stripe"</span>,
    <span class="hljs-attr">"tax_amount"</span>: <span class="hljs-literal">null</span>,
    <span class="hljs-attr">"total_amount"</span>: <span class="hljs-number">101.00</span>
}
</code></pre>
<p>Replacing the value with <code>0</code> fixed the template rendering.</p>
<p>If the template data contains non-compliant variables (whatever that means) or is missing variables, then Amazon SES won't deliver the email, but it won't tell you specifically.</p>
<p>I later came across this warning buried deeply in the CLI docs:</p>
<blockquote>
<p>However, if Amazon SES can't render the email because the template contains errors, it doesn't send the email. Additionally, because it already accepted the message, Amazon SES doesn't return a message stating that it was unable to send the email.</p>
</blockquote>
<p>The recommended way to handle these sorts of issues is to configure Amazon SES to <a target="_blank" href="https://docs.aws.amazon.com/ses/latest/dg/send-personalized-email-api.html#send-personalized-email-set-up-notifications">send Rendering Failure event notifications through Amazon SNS</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Exporting RDS Snapshots to Athena using Step Functions]]></title><description><![CDATA[A recent challenge I had on a project was enabling access to a production MySQL database on Amazon RDS, mostly for data analytics purposes. The requirement was to run ad-hoc SQL queries for periodic analytics reports and extract data to populate some...]]></description><link>https://blog.stefanolaru.com/exporting-rds-snapshots-to-athena-using-step-functions</link><guid isPermaLink="true">https://blog.stefanolaru.com/exporting-rds-snapshots-to-athena-using-step-functions</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS RDS]]></category><category><![CDATA[data analytics]]></category><category><![CDATA[SQL]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Tue, 14 Nov 2023 14:15:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/NndKt2kF1L4/upload/d8e58fb1691b4c840b22159913fa4045.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A recent challenge I had on a project was enabling access to a production MySQL database on Amazon RDS, mostly for data analytics purposes. The requirement was to run ad-hoc SQL queries for periodic analytics reports and extract data to populate some non-real-time dashboards.</p>
<p>Allowing direct access to the production database was a flat-out "nope" for security reasons and because any direct interaction with the production database poses potential risks.</p>
<p>Now, you might wonder, "Why not just spin up an RDS read replica and call it a day?". Good question!</p>
<p>An RDS read replica could have been a straightforward choice, a sidekick that would allow our non-dev colleagues to run SQL queries without stirring up any trouble in the production setup.</p>
<p>But there was no need for real-time data, most of the queries are done on previous days, if not weeks or months' data, so a 24-hour data replication delay was acceptable, and keeping up an additional RDS instance would be unnecessary.</p>
<p>A principle that resonates with my identity as a <strong>cloud architect</strong> is to focus on crafting secure and efficient solutions and avoid unnecessary infrastructure. This often leads me towards serverless approaches.</p>
<p>As we have automated backups enabled on our RDS instance, it means we have daily snapshots that are already available, the only thing left is getting that data into S3 so it can be crawled with Glue and queried using Athena.</p>
<p>And this is the <strong>architecture</strong> I came up with:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700036594990/03a8d271-8356-44ff-b370-c1d39a5eb160.png" alt class="image--center mx-auto" /></p>
<p>I love working with Step Functions, it's like having superpowers to orchestrate various services while having a crystal-clear view of the workflow, and when it comes to event-driven architectures, I find it satisfying to design systems that dynamically respond to specific events or changes.</p>
<p>So, Step Functions and EventBridge together? I'm all in!</p>
<h2 id="heading-the-service-components">The service components</h2>
<ul>
<li><p><strong>RDS instance</strong> - with automated backups enabled (you probably have it already)</p>
</li>
<li><p><strong>S3 Bucket</strong> - where the RDS data gets exported to and crawled from</p>
</li>
<li><p><strong>KMS key</strong> - to keep the things encrypted</p>
</li>
<li><p><strong>2 Lambda functions</strong> - one to prepare the export files for crawling, the other to send SendTaskSuccess to resume workflow execution</p>
</li>
<li><p><strong>3 EventBridge rules</strong> - listen for snapshot/export/crawler events</p>
</li>
<li><p><strong>Glue Crawler</strong> - prepares and organizes the data catalog</p>
</li>
<li><p><strong>Step Function</strong> - orchestrates the workflow</p>
</li>
</ul>
<h3 id="heading-rds-snapshots">RDS Snapshots</h3>
<p>The first step is to find out when an RDS automated snapshot is created, for this we create an EventBridge rule that will match the RDS DB snapshot event.</p>
<p>The automated snapshot names have the following format <code>rds:dbid-yyyy-mm-dd-hh-mm</code></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"detail-type"</span>: [<span class="hljs-string">"RDS DB Snapshot Event"</span>],
  <span class="hljs-attr">"source"</span>: [<span class="hljs-string">"aws.rds"</span>],
  <span class="hljs-attr">"detail"</span>: {
    <span class="hljs-attr">"Message"</span>: [{
      <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"Automated snapshot created"</span>
    }],
    <span class="hljs-attr">"SourceIdentifier"</span>: [{
      <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"rds:YOUR-DB-ID"</span>
    }]
  }
}
</code></pre>
<p>When an automated snapshot gets created the event will match the above pattern, and fire up our Step Function execution.</p>
<h3 id="heading-exporting-rds-snapshot-to-s3">Exporting RDS Snapshot to S3</h3>
<p>Once the snapshot is created we need to export it into S3.</p>
<p>So, the first state of our Step Function is <strong>StartExportTask</strong> which will take care of the export.</p>
<pre><code class="lang-json"><span class="hljs-string">"StartExportTask"</span>: {
    <span class="hljs-attr">"Type"</span>: <span class="hljs-string">"Task"</span>,
    <span class="hljs-attr">"Next"</span>: <span class="hljs-string">"Wait for Export (TT)"</span>,
    <span class="hljs-attr">"Parameters"</span>: {
        <span class="hljs-attr">"ExportTaskIdentifier.$"</span>: <span class="hljs-string">"States.ArrayGetItem(States.StringSplit($.detail.SourceIdentifier, ':'),1)"</span>,
        <span class="hljs-attr">"IamRoleArn"</span>: <span class="hljs-string">"${ExportRoleArn}"</span>,
        <span class="hljs-attr">"KmsKeyId"</span>: <span class="hljs-string">"${KmsKeyId}"</span>,
        <span class="hljs-attr">"S3BucketName"</span>: <span class="hljs-string">"${S3Bucket}"</span>,
        <span class="hljs-attr">"SourceArn.$"</span>: <span class="hljs-string">"$.detail.SourceArn"</span>
    },
    <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:states:::aws-sdk:rds:startExportTask"</span>
}
</code></pre>
<p>The <code>ExportTaskIdentifier</code> is the name of the Export to S3 task and will also be the S3 <em>folder</em> where the data is exported. Note each Export Task name needs to be unique and for some reason, an Export to S3 task can't be deleted once created.</p>
<p>We use the Intrinsic Functions to remove the <code>rds:</code> prefix from the <code>SourceIdentifier</code> so we can use it as an S3 <em>folder</em> name.</p>
<p>The data will be exported in S3 in Apache Parquet format, the process is asynchronous and takes quite a bit. It depends on your database type and size, as the export task first restores and scales the entire database before extracting the data to Amazon S3.</p>
<p>In my experience, it takes at least 30 minutes just to get the export task out of <code>STARTING</code> status and a minimum of 5 minutes to <code>COMPLETE</code>.</p>
<h3 id="heading-waiting-for-the-export-to-s3-to-complete">Waiting for the Export to S3 to complete</h3>
<p>Now, we need to wait for the Export to S3 to complete. As mentioned previously, the Export Task is asynchronous, it runs in the background so we need to pause our workflow and for this, we will add a state with a callback.</p>
<p>A state with a callback in Step Functions allows the workflow to pause until an external process signals its completion by calling the SendTaskSuccess (or SendTaskFailure) APIs with a task token.</p>
<p>The <strong>Wait for Export (TT)</strong> state will store the task token into an object in our S3 bucket and pause the workflow. Note the <code>.waitForTaskToken</code> suffix to the <code>Resource</code> ARN.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Wait for Export (TT)"</span>: {
        <span class="hljs-attr">"Type"</span>: <span class="hljs-string">"Task"</span>,
        <span class="hljs-attr">"Next"</span>: <span class="hljs-string">"Move Exported Objects"</span>,
        <span class="hljs-attr">"Parameters"</span>: {
            <span class="hljs-attr">"Body.$"</span>: <span class="hljs-string">"$$.Task.Token"</span>,
            <span class="hljs-attr">"Bucket"</span>: <span class="hljs-string">"${S3Bucket}"</span>,
            <span class="hljs-attr">"Key"</span>: <span class="hljs-string">"export_task_token"</span>
        },
        <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:states:::aws-sdk:s3:putObject.waitForTaskToken"</span>,
        <span class="hljs-attr">"HeartbeatSeconds"</span>: <span class="hljs-number">3600</span>,
        <span class="hljs-attr">"ResultPath"</span>: <span class="hljs-literal">null</span>
    }
}
</code></pre>
<p>To find out when the Export to S3 task gets completed we created another EventBridge rule with the following pattern:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"detail-type"</span>: [<span class="hljs-string">"RDS DB Snapshot Event"</span>],
  <span class="hljs-attr">"source"</span>: [<span class="hljs-string">"aws.rds"</span>],
  <span class="hljs-attr">"detail"</span>: {
    <span class="hljs-attr">"SourceIdentifier"</span>: [{
      <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"rds:YOUR-DB-ID"</span>
    }],
    <span class="hljs-attr">"Message"</span>: [{
      <span class="hljs-attr">"prefix"</span>: <span class="hljs-string">"Export task completed"</span>
    }]
  }
}
</code></pre>
<p>Matching the above pattern signals the snapshot export is complete and will invoke the Lambda function that's responsible for resuming our workflow. The Lambda reads the task token that was written in the S3 object and calls <code>SendTaskSuccess</code> API.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { SFNClient, SendTaskSuccessCommand } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"@aws-sdk/client-sfn"</span>),
    { S3Client, GetObjectCommand } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"@aws-sdk/client-s3"</span>);
<span class="hljs-keyword">const</span> sf = <span class="hljs-keyword">new</span> SFNClient(), s3 = <span class="hljs-keyword">new</span> S3Client();
<span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-comment">// get the task token from s3</span>
    <span class="hljs-keyword">const</span> taskToken = <span class="hljs-keyword">await</span> s3
        .send(
            <span class="hljs-keyword">new</span> GetObjectCommand({
                <span class="hljs-attr">Bucket</span>: process.env.S3_BUCKET,
                <span class="hljs-attr">Key</span>: event.source === <span class="hljs-string">"aws.glue"</span> ? <span class="hljs-string">"glue_task_token"</span> : <span class="hljs-string">"export_task_token"</span>,
            })
        )
        .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> data.Body.transformToString(<span class="hljs-string">"utf-8"</span>))
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(err);
            <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
        });
    <span class="hljs-comment">// send the task token to step function</span>
    <span class="hljs-keyword">await</span> sf.send(
        <span class="hljs-keyword">new</span> SendTaskSuccessCommand({
            <span class="hljs-attr">output</span>: <span class="hljs-built_in">JSON</span>.stringify(event),
            <span class="hljs-attr">taskToken</span>: taskToken,
        })
    );
    <span class="hljs-keyword">return</span>;
};
</code></pre>
<h3 id="heading-preparing-the-data-for-the-glue-crawler">Preparing the data for the Glue Crawler</h3>
<p>Now that we have the data exported in S3 in Apache Parquet we need to move it in the folder our Glue Crawler was instructed to look for it.</p>
<p>Every export is made in a different folder uniquely named after the export task identifier, so we need to move the Parquet files into the <code>data</code> folder so it can be crawled. Here's the Lambda function that does the job:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { S3Client, ListObjectsV2Command, CopyObjectCommand, DeleteObjectCommand } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"@aws-sdk/client-s3"</span>);
<span class="hljs-keyword">const</span> s3 = <span class="hljs-keyword">new</span> S3Client();
<span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-comment">// list the objects in the bucket</span>
    <span class="hljs-keyword">const</span> objects = <span class="hljs-keyword">await</span> s3
        .send(
            <span class="hljs-keyword">new</span> ListObjectsV2Command({
                <span class="hljs-attr">Bucket</span>: process.env.S3_BUCKET,
                <span class="hljs-attr">Prefix</span>: event.prefix,
            })
        )
        .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> data.Contents)
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(err);
            <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
        });
    <span class="hljs-comment">// copy the objects to the data folder</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> object <span class="hljs-keyword">of</span> objects) {
        <span class="hljs-keyword">await</span> s3
            .send(
                <span class="hljs-keyword">new</span> CopyObjectCommand({
                    <span class="hljs-attr">Bucket</span>: process.env.S3_BUCKET,
                    <span class="hljs-attr">CopySource</span>: <span class="hljs-string">`<span class="hljs-subst">${process.env.S3_BUCKET}</span>/<span class="hljs-subst">${object.Key}</span>`</span>,
                    <span class="hljs-attr">Key</span>: <span class="hljs-string">`data/<span class="hljs-subst">${object.Key.split(<span class="hljs-string">"/"</span>).slice(<span class="hljs-number">1</span>).join(<span class="hljs-string">"/"</span>)}</span>`</span>,
                })
            )
            .then(<span class="hljs-function">() =&gt;</span> {
                <span class="hljs-comment">// delete the old object</span>
                s3.send(
                    <span class="hljs-keyword">new</span> DeleteObjectCommand({
                        <span class="hljs-attr">Bucket</span>: process.env.S3_BUCKET,
                        <span class="hljs-attr">Key</span>: object.Key,
                    })
                );
            })
            .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
                <span class="hljs-built_in">console</span>.log(err);
                <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
            });
    }
};
</code></pre>
<p>To be honest, I didn't want to use Lambda function for this but there was no other option. I initially tried using states directly in the Step Functions, unfortunately, the string manipulation is still very limited with Intrinsic Functions, and didn't find a good way to remove the prefix from the S3 Object Key to move the exports to the standardized folder. The flow would have looked as below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1699628231072/92d24d9c-5867-4d9c-955e-69a03c9c3f07.jpeg" alt class="image--center mx-auto" /></p>
<p>Crawling the data</p>
<p>Once the data is moved we can start the Glue Crawler and prepare it to be queried by Athena.</p>
<p>The Glue Crawler analyzes the content and structure of the data, extracting key details such as table definitions and column information. This metadata is then stored in the AWS Glue Data Catalog, allowing Athena to query the data.</p>
<p>The crawl process is also asynchronous and takes a couple of minutes.</p>
<h3 id="heading-waiting-for-the-glue-crawler-to-complete">Waiting for the Glue Crawler to complete</h3>
<p>In my Step Function, I added another state that waits for the crawl to complete, this is optional. I added it because I want to be notified if the workflow fails (not part of the article/template).</p>
<p>It uses the same technique, writing the task token to an S3 object and pausing the workflow until an event matches the below pattern and the Lambda gets invoked to resume the process.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"detail-type"</span>: [<span class="hljs-string">"Glue Crawler State Change"</span>],
  <span class="hljs-attr">"source"</span>: [<span class="hljs-string">"aws.glue"</span>],
  <span class="hljs-attr">"detail"</span>: {
    <span class="hljs-attr">"crawlerName"</span>: [<span class="hljs-string">"my_crawler_name"</span>],
    <span class="hljs-attr">"state"</span>: [<span class="hljs-string">"Succeeded"</span>]
  }
}
</code></pre>
<p>Note the Standard Workflow for Step Functions can run up to a year, and we pay per state transition, so waiting for the callback won't cost extra.</p>
<p>However, I set a heartbeat to 3600 seconds for both states using a callback to allow the entire workflow to timeout if the Glue Crawled doesn't complete within that timeframe.</p>
<p>And there you have it – a secure, cost-efficient, and hassle-free way to run SQL queries on your RDS data while keeping your production database safe.</p>
<h2 id="heading-summary">Summary</h2>
<p>Here's the <a target="_blank" href="https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/review?templateURL=https://stefanolaru-cfn.s3.amazonaws.com/stepfunctions-rds-to-athena/template.yml&amp;stackName=rds_glue_athena">1-click Deployment</a> link to get going quickly, or <a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/tree/main/stepfunctions-rds-to-athena">click to view</a> the complete self-contained Cloudformation template that can be deployed into any AWS account.</p>
]]></content:encoded></item><item><title><![CDATA[Run Lambda functions with fixed IP]]></title><description><![CDATA[A Lambda function runs in its own container and when invoked, the external requests to 3rd-party services will appear as coming from various IP addresses.
This becomes a problem when our Lambda function needs to communicate with IP-restricted access ...]]></description><link>https://blog.stefanolaru.com/run-lambda-functions-with-fixed-ip</link><guid isPermaLink="true">https://blog.stefanolaru.com/run-lambda-functions-with-fixed-ip</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws lambda]]></category><category><![CDATA[vpc]]></category><category><![CDATA[cloudformation]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Tue, 28 Mar 2023 09:35:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/u5Zt-HoocrM/upload/bd375bf8f135603a845589bf672d3e4f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A Lambda function runs in its own container and when invoked, the external requests to 3rd-party services will appear as coming from various IP addresses.</p>
<p>This becomes a problem when our Lambda function needs to communicate with IP-restricted access resources. I recently needed in a serverless project a Lambda function to connect to an Azure SQL Database that only accepted connections from whitelisted IP addresses.</p>
<p>There's no way to configure the IP address of a Lambda function.</p>
<p>The solution is to run the Lambda inside a VPC and route the outbound connections through a NAT that gets assigned an Elastic IP. This way the external requests done by the Lambda function will have the same IP address on every invocation.</p>
<p>When running inside a VPC, our Lambda function doesn't have access to the internet, even when connected to a public subnet. There's no way around using a NAT.</p>
<p>The standard implementation of the service architecture looks as in the diagram below. <strong>NAT is the key piece when we need a fixed IP for our Lambda functions.</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679986632834/759ce51b-a91c-4e64-ba0a-d47c89167f32.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-whats-a-nat"><strong>What's a NAT?</strong></h2>
<p>A Network Address Translation (NAT) is a compute instance that enables traffic between our resources in a private subnet and the Internet.</p>
<p>In AWS we have 2 NAT options: NAT Gateway &amp; NAT Instance.</p>
<p>The <strong>NAT Gateway is the official recommendation</strong>, but it comes at a premium.</p>
<h2 id="heading-nat-gateway-vs-nat-instance"><strong>NAT Gateway vs. NAT Instance</strong></h2>
<p><strong>NAT Gateway</strong> is a highly available and scalable managed service, and it costs $0.045/hr, about ~$33/month before tax.</p>
<p>For personal projects, dev environments, or even in production for low-traffic or non-business critical applications a NAT Gateway is overpriced for its role.</p>
<p>In my case, the Lambda function has to connect just a couple of times a day to the Azure SQL Database and pull some fresh data, it's not a business-critical service, occasional failure can be tolerated. Adding the NAT Gateway into the stack would have 4X'ed the project's monthly AWS bill. Serverless ;)</p>
<p>Instead of a NAT Gateway, I used an EC2 instance that's configured to act as a router between the private subnet where the Lambda is invoked and the outside world, <strong>a NAT Instance</strong>.</p>
<p>Actually, this is nothing new, for many years the only way to create a NAT in AWS was by creating a NAT Instance.</p>
<p>An EC2 t4g.nano instance costs $0.0042/hr, so the smallest NAT Instance can be <strong>10x cheaper compared to NAT Gateway</strong>.</p>
<p>The downside of using a NAT Instance instead of a NAT Gateway is we have to configure and then manage it, we're responsible for replacing the instance if it goes down and for applying software patches.</p>
<p>To solve the availability problem we'll launch the NAT Instance inside an Autoscaling group, there are no additional fees for it, and like this, we make sure to have a healthy NAT Instance running at all times. A self-healing NAT Instance. To patch it we can use AWS Systems Manager.</p>
<p>Worth mentioning that both options fail the <a target="_blank" href="https://www.gomomento.com/blog/fighting-off-fake-serverless-bandits-with-the-true-definition-of-serverless">serverless litmus test</a>.</p>
<h2 id="heading-routing-lambda-through-a-nat-instance">Routing Lambda through a NAT Instance</h2>
<p>In this article, I'll explain how to run your Lambda functions with a fixed IP using a NAT Instance, which is <em>up to 10x cheaper than a NAT Gateway</em>.</p>
<p>The service architecture diagram remains almost the same, we replace the NAT Gateway with an EC2 instance that will do the NAT job.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1679987187261/5d5dc97a-8b28-4ed0-b726-90d4fb6a80f3.png" alt class="image--center mx-auto" /></p>
<p>Setting up a NAT Instance is a bit more complicated than using a NAT Gateway, but worry not, at the end of the article you'll find a <strong>1-click deployment</strong> CloudFormation template that does all the work.</p>
<h2 id="heading-vpc">VPC</h2>
<p>As mentioned previously, we need to run Lambda from inside a VPC, below are the steps and resources to create the VPC:</p>
<ul>
<li><p>create a VPC with 2 subnets in a single Availability Zone</p>
</li>
<li><p>attach an Internet Gateway to the VPC</p>
</li>
<li><p>create 2 Route Tables and associate each with the relevant subnet</p>
</li>
<li><p>in the Public Subnet route table, we add a default route so the requests to any IP (<code>0.0.0.0/0</code>) to be routed through the Internet Gateway. This gives all resources created in the Public Subnet internet access.</p>
</li>
<li><p>note the Private Subnet route table has no routes, we'll add that later</p>
</li>
</ul>
<pre><code class="lang-yaml"><span class="hljs-attr">VPC:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::VPC</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">CidrBlock:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VpcCIDR</span>
        <span class="hljs-attr">EnableDnsSupport:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">EnableDnsHostnames:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span>
<span class="hljs-attr">InternetGateway:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::InternetGateway</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span>
<span class="hljs-attr">InternetGatewayAttachment:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::VPCGatewayAttachment</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">InternetGatewayId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">InternetGateway</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VPC</span>
<span class="hljs-attr">PublicSubnet:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::Subnet</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VPC</span>
        <span class="hljs-attr">AvailabilityZone:</span> <span class="hljs-type">!Select</span> [<span class="hljs-number">0</span>, <span class="hljs-type">!GetAZs</span> <span class="hljs-string">""</span>]
        <span class="hljs-attr">CidrBlock:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PublicSubnetCIDR</span>
        <span class="hljs-attr">MapPublicIpOnLaunch:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">Public</span> <span class="hljs-string">Subnet</span>
<span class="hljs-attr">PublicRouteTable:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::RouteTable</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VPC</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">Public</span> <span class="hljs-string">Routes</span>
<span class="hljs-attr">DefaultPublicRoute:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::Route</span>
    <span class="hljs-attr">DependsOn:</span> <span class="hljs-string">InternetGatewayAttachment</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">RouteTableId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PublicRouteTable</span>
        <span class="hljs-attr">DestinationCidrBlock:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">/0</span>
        <span class="hljs-attr">GatewayId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">InternetGateway</span>
<span class="hljs-attr">PublicSubnetRouteTableAssociation:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::SubnetRouteTableAssociation</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">RouteTableId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PublicRouteTable</span>
        <span class="hljs-attr">SubnetId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PublicSubnet</span>
<span class="hljs-attr">PrivateSubnet:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::Subnet</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VPC</span>
        <span class="hljs-attr">AvailabilityZone:</span> <span class="hljs-type">!Select</span> [<span class="hljs-number">0</span>, <span class="hljs-type">!GetAZs</span> <span class="hljs-string">""</span>]
        <span class="hljs-attr">CidrBlock:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PrivateSubnetCIDR</span>
        <span class="hljs-attr">MapPublicIpOnLaunch:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">Private</span> <span class="hljs-string">Subnet</span>
<span class="hljs-attr">PrivateRouteTable:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::RouteTable</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VPC</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">Private</span> <span class="hljs-string">Routes</span>
<span class="hljs-attr">PrivateSubnetRouteTableAssociation:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::SubnetRouteTableAssociation</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">RouteTableId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PrivateRouteTable</span>
        <span class="hljs-attr">SubnetId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PrivateSubnet</span>
</code></pre>
<h2 id="heading-elastic-ip">Elastic IP</h2>
<p>The Elastic IP will give us the fixed IP that we need to whitelist on the 3rd-party services. It will be associated with the EC2 instance at launch time.</p>
<p>Note the Elastic IP is free as long it is associated with a running instance. If it is not associated, or associated with a stopped instance it will cost $0.005/hr.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">ElasticIP:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::EIP</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-ElasticIP</span>
</code></pre>
<h2 id="heading-nat-instance">NAT Instance</h2>
<h3 id="heading-nat-instance-iam-role">NAT Instance IAM Role</h3>
<p>First, we have to create an IAM Role that can be assumed by our EC2 Instance in order to do the following actions:</p>
<ul>
<li><p><code>ec2:AssociateAddress</code> - to associate the Elastic IP</p>
</li>
<li><p><code>ec2:ModifyInstanceAttribute</code> - to update the EC2 instance attribute</p>
</li>
<li><p><code>ec2:CreateRoute</code> and <code>ec2:ReplaceRoute</code> - to create/replace the route in the Private Subnet route table</p>
</li>
</ul>
<p>Also, note the <code>AmazonSSMManagedInstanceCore</code> managed policy, which allows using AWS Systems Manager to Run Command or Session Manager.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NATInstanceRole:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::IAM::Role</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">RoleName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-NATInstanceRole</span>
        <span class="hljs-attr">AssumeRolePolicyDocument:</span>
            <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
            <span class="hljs-attr">Statement:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                  <span class="hljs-attr">Principal:</span>
                      <span class="hljs-attr">Service:</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-string">ec2.amazonaws.com</span>
                  <span class="hljs-attr">Action:</span> <span class="hljs-string">sts:AssumeRole</span>
        <span class="hljs-attr">ManagedPolicyArns:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore</span>
        <span class="hljs-attr">Policies:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">PolicyName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-NATInstancePolicy</span>
              <span class="hljs-attr">PolicyDocument:</span>
                  <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
                  <span class="hljs-attr">Statement:</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                        <span class="hljs-attr">Action:</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">ec2:AssociateAddress</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">ec2:ReplaceRoute</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">ec2:CreateRoute</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">ec2:ModifyInstanceAttribute</span>
                        <span class="hljs-attr">Resource:</span> <span class="hljs-string">"*"</span>
<span class="hljs-attr">NATInstanceProfile:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::IAM::InstanceProfile</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">InstanceProfileName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-NATInstanceProfile</span>
        <span class="hljs-attr">Roles:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">NATInstanceRole</span>
</code></pre>
<h3 id="heading-nat-instance-security-group">NAT Instance Security Group</h3>
<p>The Security Group restricts the inbound connections to our NAT Instance only to resources from our VPC.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NATInstanceSecurityGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::SecurityGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">GroupDescription:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">NAT</span> <span class="hljs-string">Instance</span> <span class="hljs-string">SG</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VPC</span>
        <span class="hljs-attr">SecurityGroupIngress:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
              <span class="hljs-attr">FromPort:</span> <span class="hljs-number">0</span>
              <span class="hljs-attr">ToPort:</span> <span class="hljs-number">65535</span>
              <span class="hljs-attr">CidrIp:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VpcCIDR</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">NAT</span> <span class="hljs-string">Instance</span> <span class="hljs-string">SG</span>
</code></pre>
<h3 id="heading-autoscaling">Autoscaling</h3>
<p>We're going to launch the NAT Instance in the Public Subnet using EC2 Auto Scaling, there are no additional fees for it, and the service can automatically determine the health status of our instance using EC2 status checks.</p>
<p>If the NAT Instance is unhealthy, it will be terminated and replaced by a new one with minimum downtime. The AutoScalingGroup will make sure we have 1 healthy instance running at all times.</p>
<p>When our instance gets terminated and a new one gets created, the new instance will have a different IP, and that's the reason we didn't create a route in the private route table.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">AutoScalingGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::AutoScaling::AutoScalingGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">VPCZoneIdentifier:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PublicSubnet</span>
        <span class="hljs-attr">LaunchTemplate:</span>
            <span class="hljs-attr">LaunchTemplateId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">LaunchTemplate</span>
            <span class="hljs-attr">Version:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">LaunchTemplate.LatestVersionNumber</span>
        <span class="hljs-attr">MinSize:</span> <span class="hljs-number">1</span>
        <span class="hljs-attr">MaxSize:</span> <span class="hljs-number">1</span>
        <span class="hljs-attr">DesiredCapacity:</span> <span class="hljs-number">1</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">NAT</span> <span class="hljs-string">Instance</span>
              <span class="hljs-attr">PropagateAtLaunch:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">LaunchTemplate:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::LaunchTemplate</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">LaunchTemplateName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-LaunchTemplate</span>
        <span class="hljs-attr">LaunchTemplateData:</span>
            <span class="hljs-attr">ImageId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">EC2InstanceAmi</span>
            <span class="hljs-attr">InstanceType:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">EC2InstanceType</span>
            <span class="hljs-attr">IamInstanceProfile:</span>
                <span class="hljs-attr">Arn:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">NATInstanceProfile.Arn</span>
            <span class="hljs-attr">SecurityGroupIds:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">NATInstanceSecurityGroup</span>
            <span class="hljs-attr">UserData:</span>
                <span class="hljs-attr">Fn::Base64:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">|
                    #!/bin/sh
                    # ... removed for brevity</span>
</code></pre>
<h4 id="heading-userdata">UserData</h4>
<p>The <code>UserData</code> property allows specifying a shell script that will run each time the instance is launched. If our Autoscaling group terminates an unhealthy instance, the new instance will execute this script at launch and automatically set up the NAT Instance.  </p>
<p>Here's what it does:</p>
<ul>
<li><p>updates all existing packages and installs the <code>iptables-services</code> package</p>
</li>
<li><p>enables IP forwarding</p>
</li>
<li><p>configures NAT routing</p>
</li>
<li><p><strong>Associates the Elastic IP</strong> to the EC2 instance, the public IP of the instance will be the Elastic IP, and all requests that pass through this instance will appear as coming from this IP address.</p>
</li>
<li><p><strong>Disables EC2 instance source/destination check</strong>. This is enabled by default forcing the instance to be the source or destination of any traffic it sends or receives. For NAT we need the traffic to pass through.</p>
</li>
<li><p><strong>Creates/updates the route in the Private Subnet route table</strong>. We're adding a default route so the requests made by our resources in the Private Subnet (Lambda) to any destination (<code>0.0.0.0/0</code>) to be routed through the NAT Instance.</p>
</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-comment"># update packages</span>
yum update -y
<span class="hljs-comment"># install iptable</span>
yum install -y iptables-services
<span class="hljs-comment"># enable iptable</span>
systemctl <span class="hljs-built_in">enable</span> iptables.service
<span class="hljs-comment"># enable IP forwarding</span>
sysctl -w net.ipv4.ip_forward=1
<span class="hljs-comment"># configure NAT</span>
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
<span class="hljs-comment"># save iptable</span>
service iptables save
<span class="hljs-comment"># start iptable</span>
service iptables start

<span class="hljs-comment"># get instance-id</span>
EC2_INSTANCE_ID=$(curl -s http://instance-data/latest/meta-data/instance-id)

<span class="hljs-comment"># associate the Elastic IP address to the instance</span>
aws ec2 associate-address --instance-id <span class="hljs-variable">$EC2_INSTANCE_ID</span> --allocation-id <span class="hljs-variable">${ElasticIP.AllocationId}</span> --allow-reassociation  --region <span class="hljs-variable">${AWS::Region}</span>

<span class="hljs-comment"># disable source/destination check</span>
aws ec2 modify-instance-attribute --instance-id <span class="hljs-variable">$EC2_INSTANCE_ID</span> --source-dest-check <span class="hljs-string">"{\"Value\": false}"</span> --region <span class="hljs-variable">${AWS::Region}</span>

<span class="hljs-comment"># replace or create route in the default private route table</span>
aws ec2 replace-route --route-table-id <span class="hljs-variable">${PrivateRouteTable}</span> --destination-cidr-block <span class="hljs-string">"0.0.0.0/0"</span> --instance-id <span class="hljs-variable">$EC2_INSTANCE_ID</span> --region <span class="hljs-variable">${AWS::Region}</span> || aws ec2 create-route --route-table-id <span class="hljs-variable">${PrivateRouteTable}</span> --destination-cidr-block <span class="hljs-string">"0.0.0.0/0"</span> --instance-id <span class="hljs-variable">$EC2_INSTANCE_ID</span> --region <span class="hljs-variable">${AWS::Region}</span>
</code></pre>
<h2 id="heading-lambda-function">Lambda Function</h2>
<p>To run inside a VPC our Lambda function needs permissions to create and manage network interfaces (ENI) and be associated with a subnet and a security group.  </p>
<p>We created the Private Subnet previously with the VPC, so we'll create a new IAM Role that will be assumed by our Lambda and the Security Group.</p>
<h3 id="heading-lambda-execution-role-amp-security-group">Lambda Execution Role &amp; Security Group</h3>
<p>The Lambda execution role is attached the <code>AWSLambdaVPCAccessExecutionRole</code> managed policy which grants permissions to manage ENIs within an Amazon VPC and write to CloudWatch Logs.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NodeLambdaExecutionRole:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::IAM::Role</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">RoleName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-NodeLambdaExecutionRole</span>
        <span class="hljs-attr">AssumeRolePolicyDocument:</span>
            <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
            <span class="hljs-attr">Statement:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                  <span class="hljs-attr">Principal:</span>
                      <span class="hljs-attr">Service:</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-string">lambda.amazonaws.com</span>
                  <span class="hljs-attr">Action:</span> <span class="hljs-string">sts:AssumeRole</span>
        <span class="hljs-attr">ManagedPolicyArns:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole</span>

<span class="hljs-attr">NodeLambdaSecurityGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::SecurityGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">GroupDescription:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">Node</span> <span class="hljs-string">Lambda</span> <span class="hljs-string">SG</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VPC</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">Node</span> <span class="hljs-string">Lambda</span> <span class="hljs-string">SG</span>
</code></pre>
<h3 id="heading-lambda-function-code">Lambda Function Code</h3>
<p>For demo purposes, I included a simple NodeJS Lambda function in the stack that calls the <a target="_blank" href="https://ipify.org">Ipify API</a> and checks if the request made by Lambda comes from the Elastic IP.</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span>.handler = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> https = <span class="hljs-built_in">require</span>(<span class="hljs-string">"https"</span>);
    <span class="hljs-keyword">const</span> url = <span class="hljs-string">"https://api.ipify.org?format=json"</span>;
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
        https
            .get(url, <span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
                <span class="hljs-keyword">let</span> data = <span class="hljs-string">""</span>;
                res.on(<span class="hljs-string">"data"</span>, <span class="hljs-function">(<span class="hljs-params">chunk</span>) =&gt;</span> {
                    data += chunk;
                });
                res.on(<span class="hljs-string">"end"</span>, <span class="hljs-function">() =&gt;</span> {
                    resolve(<span class="hljs-built_in">JSON</span>.parse(data));
                });
            })
            .on(<span class="hljs-string">"error"</span>, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
                reject(err);
            });
    });
    <span class="hljs-keyword">return</span> { <span class="hljs-attr">elastic_ip</span>: process.env.ELASTIC_IP, <span class="hljs-attr">source_ip</span>: response.ip };
};
</code></pre>
<h3 id="heading-deploying-the-lambda-function">Deploying the Lambda Function</h3>
<p>As our function code is up to 4096 chars we can deploy it as a CloudFormation resource using inline code. Note that we pass the <code>ELASTIC_IP</code> as an environment variable so our code can reference it.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">NodeLambdaFunction:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Lambda::Function</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Code:</span>
            <span class="hljs-attr">ZipFile:</span> <span class="hljs-string">|
                # ... function code omitted for brevity
</span>        <span class="hljs-attr">Handler:</span> <span class="hljs-string">index.handler</span>
        <span class="hljs-attr">Role:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">NodeLambdaExecutionRole.Arn</span>
        <span class="hljs-attr">Runtime:</span> <span class="hljs-string">nodejs18.x</span>
        <span class="hljs-attr">Timeout:</span> <span class="hljs-number">30</span>
        <span class="hljs-attr">Environment:</span>
            <span class="hljs-attr">Variables:</span>
                <span class="hljs-attr">ELASTIC_IP:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">ElasticIP</span>
        <span class="hljs-attr">VpcConfig:</span>
            <span class="hljs-attr">SecurityGroupIds:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">NodeLambdaSecurityGroup</span>
            <span class="hljs-attr">SubnetIds:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">PrivateSubnet</span>
</code></pre>
<h2 id="heading-summary">Summary</h2>
<p>Here's the <a target="_blank" href="https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://stefanolaru-cfn.s3.amazonaws.com/lambda-fixed-ip/template.yml&amp;stackName=FixedIPLambda">1-click Deployment</a> link to get going quickly, or <a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/tree/main/lambda-fixed-ip">click to view</a> the complete self-contained Cloudformation template that can be deployed into any AWS account.</p>
]]></content:encoded></item><item><title><![CDATA[Scalable webhook listeners in AWS]]></title><description><![CDATA[Traditionally, when we think about loose coupling and serverless in AWS, it's an API Gateway endpoint triggering a Lambda function that sends messages into SQS.
But API Gateway directly integrates with a handful of other AWS APIs, and SQS is one of t...]]></description><link>https://blog.stefanolaru.com/scalable-webhook-listeners-in-aws</link><guid isPermaLink="true">https://blog.stefanolaru.com/scalable-webhook-listeners-in-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[SQS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[webhooks]]></category><category><![CDATA[cloudformation]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Wed, 08 Mar 2023 20:12:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Q5rMCWwspxc/upload/f23b9d69623beb441f59d2496ab2f5a5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Traditionally, when we think about loose coupling and serverless in AWS, it's an <strong>API Gateway</strong> endpoint triggering a <strong>Lambda function</strong> that sends messages into <strong>SQS</strong>.</p>
<p>But API Gateway directly integrates with a handful of other AWS APIs, and SQS is one of them.</p>
<p>We can have an HTTP endpoint send messages directly to SQS without the need for a Lambda function, and we can even perform request validation at the API Gateway level.</p>
<p>This serverless pattern is extremely handy when creating 3rd party integrations for our services. In this article, I will explain how it works and provide a Cloudformation template you can easily adapt to your use case and deploy with one click.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678305896103/576bde90-4901-4dd5-b5ca-19efc755b3e5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-asynchronous-processing">Asynchronous processing</h2>
<p>Processing incoming webhook data in real time can be challenging, especially if our service needs to perform additional processing or trigger downstream actions.</p>
<p>In these situations, the producer only needs to know if the data was successfully received, the processing can happen in the background, asynchronously, so we return a successful response and send the request in a queue for later processing.</p>
<p>We often know the schema in advance so we can also validate the request before sending the message to SQS. The backend worker service will consume messages from the queue at its own pace, one by one or in batches, and do the hard lifting.</p>
<p>If the order of processing is important we can use an SQS FIFO queue to process the messages in the exact order they were received. We can also bring an SQS Dead Letter Queue into the mix to collect the processing failures for further investigation or retry.</p>
<p>I used a similar architecture for a service that integrates with Mailgun for marketing email sending and reporting. Each time an action is performed by one of the email recipients (open, click, bounce, unsubscribe) Mailgun triggers the service HTTP endpoint and sends an event notification, the notification is stored in SQS, then a Lambda consumes these events, and creates campaign reports and other actions. It handles millions of events per week.</p>
<h2 id="heading-main-components">Main components</h2>
<ul>
<li><p>SQS</p>
</li>
<li><p>API Gateway (REST)</p>
</li>
<li><p>IAM Role</p>
</li>
</ul>
<h2 id="heading-sqs">SQS</h2>
<p>In this template, I'm creating two queues, the queue that receives the messages for processing and one that will act as a Dead Letter Queue and receive all the messages that fail to process more than 5 times.</p>
<p>I'm also using a FIFO queue to illustrate how to use the OpenAPI extension to deal with message grouping and content-based deduplication. But for most use cases, the order of processing isn't important and we can default to a normal queue.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># SQS</span>
<span class="hljs-attr">Queue:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::SQS::Queue</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">QueueName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${QueueName}-${StageName}.fifo</span>
        <span class="hljs-attr">FifoQueue:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">ContentBasedDeduplication:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">RedrivePolicy:</span>
            <span class="hljs-attr">deadLetterTargetArn:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">QueueDLQ.Arn</span>
            <span class="hljs-attr">maxReceiveCount:</span> <span class="hljs-number">5</span>
<span class="hljs-comment"># SQS DLQ</span>
<span class="hljs-attr">QueueDLQ:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::SQS::Queue</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">QueueName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${QueueName}-${StageName}-dlq.fifo</span>
        <span class="hljs-attr">FifoQueue:</span> <span class="hljs-literal">true</span>
</code></pre>
<h2 id="heading-api-gateway">API Gateway</h2>
<p>I'm using the REST API Gateway, not the HTTP type (API Gateway v2). REST API supports more features than HTTP API such as API keys or request validation.</p>
<p>API Gateway will create an <code>/events</code> endpoint that will accept <code>POST</code> requests. It will perform basic validation of the request payload before passing it to SQS.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">APIGateway:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::ApiGateway::RestApi</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Name:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">AWS::StackName</span>
        <span class="hljs-attr">Description:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}</span> <span class="hljs-string">API</span> <span class="hljs-string">Gateway</span>
        <span class="hljs-attr">EndpointConfiguration:</span>
            <span class="hljs-attr">Types:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">REGIONAL</span>
        <span class="hljs-attr">Body:</span>
            <span class="hljs-attr">openapi:</span> <span class="hljs-number">3.0</span><span class="hljs-number">.1</span>
            <span class="hljs-attr">info:</span>
                <span class="hljs-attr">title:</span> <span class="hljs-string">API</span> <span class="hljs-string">Gateway</span> <span class="hljs-string">-&gt;</span> <span class="hljs-string">SQS</span>
            <span class="hljs-attr">paths:</span>
                <span class="hljs-string">/events:</span>
                    <span class="hljs-attr">post:</span>
                        <span class="hljs-attr">description:</span> <span class="hljs-string">Send</span> <span class="hljs-string">events</span> <span class="hljs-string">to</span> <span class="hljs-string">SQS</span>
                        <span class="hljs-attr">operationId:</span> <span class="hljs-string">sendEvent</span>
                        <span class="hljs-attr">requestBody:</span>
                            <span class="hljs-attr">content:</span>
                                <span class="hljs-attr">application/json:</span>
                                    <span class="hljs-attr">schema:</span>
                                        <span class="hljs-string">$ref:</span> <span class="hljs-string">"#/components/schemas/event"</span>
                        <span class="hljs-comment"># enable request validator</span>
                        <span class="hljs-attr">x-amazon-apigateway-request-validator:</span> <span class="hljs-string">basic</span>
                        <span class="hljs-comment"># define request response</span>
                        <span class="hljs-attr">responses:</span>
                            <span class="hljs-attr">"200":</span>
                                <span class="hljs-attr">description:</span> <span class="hljs-string">Successful</span> <span class="hljs-string">request</span>
                            <span class="hljs-attr">"400":</span>
                                <span class="hljs-attr">description:</span> <span class="hljs-string">Bad</span> <span class="hljs-string">request</span>
                            <span class="hljs-attr">"500":</span>
                                <span class="hljs-attr">description:</span> <span class="hljs-string">Server</span> <span class="hljs-string">Error</span>
                        <span class="hljs-comment"># enable api gateway integration</span>
                        <span class="hljs-attr">x-amazon-apigateway-integration:</span>
                            <span class="hljs-comment"># ... removed for brevity</span>
                            <span class="hljs-comment"># ... check SQS Integration section</span>

            <span class="hljs-comment"># define request body schema</span>
            <span class="hljs-attr">components:</span>
                <span class="hljs-attr">schemas:</span>
                    <span class="hljs-attr">event:</span>
                        <span class="hljs-comment"># ... removed for brevity    </span>
                        <span class="hljs-comment"># ... check Request Validation section</span>

            <span class="hljs-comment"># define request validator</span>
            <span class="hljs-attr">x-amazon-apigateway-request-validators:</span>
                <span class="hljs-attr">basic:</span>
                    <span class="hljs-attr">validateRequestParameters:</span> <span class="hljs-literal">false</span>
                    <span class="hljs-attr">validateRequestBody:</span> <span class="hljs-literal">true</span>
</code></pre>
<h3 id="heading-sqs-integration">SQS Integration</h3>
<p>The OpenAPI <code>x-amazon-apigateway-integration</code> extension allows us to configure SQS integration in the <code>Body</code> property for each endpoint. What you need to know is:</p>
<ul>
<li><p><strong>passthroughBehavior</strong> - is set to <code>never</code> so the request is always transformed using <code>requestTemplates</code> value</p>
</li>
<li><p><strong>requestParameters</strong> - SQS POST request sends query parameters as a form in the body of the HTTP request, we need to set the <code>Content-Type</code> header accordingly</p>
</li>
<li><p><strong>requestTemplates</strong> - map the request payload, headers, query, or path parameters to the SQS API request, it uses Velocity templates - check <a target="_blank" href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html">API Gateway mapping template &amp; variable reference</a></p>
</li>
<li><p><strong>responses</strong> - configures the response, in our case returns the SQS MessageId in case of a successful request</p>
</li>
</ul>
<pre><code class="lang-yaml"><span class="hljs-attr">x-amazon-apigateway-integration:</span>
    <span class="hljs-attr">httpMethod:</span> <span class="hljs-string">POST</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">aws</span>
    <span class="hljs-attr">credentials:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">AGW2SQSRole.Arn</span>
    <span class="hljs-attr">uri:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">arn:aws:apigateway:${AWS::Region}:sqs:path/${AWS::AccountId}/${Queue.QueueName}</span>
    <span class="hljs-attr">passthroughBehavior:</span> <span class="hljs-string">never</span>
    <span class="hljs-attr">requestParameters:</span>
        <span class="hljs-attr">integration.request.header.Content-Type:</span> <span class="hljs-string">"'application/x-www-form-urlencoded'"</span>
    <span class="hljs-attr">requestTemplates:</span>
        <span class="hljs-attr">application/json:</span> <span class="hljs-string">|
            #set($MsgDedupId = $context.requestId)
            #set($GroupId = $input.json('$.data.event_type'))
            Action=SendMessage&amp;MessageGroupId=$GroupId&amp;MessageDeduplicationId=$MsgDedupId&amp;MessageBody=$input.body
</span>    <span class="hljs-attr">responses:</span>
        <span class="hljs-attr">default:</span>
            <span class="hljs-attr">statusCode:</span> <span class="hljs-number">200</span>
            <span class="hljs-attr">responseTemplates:</span>
                <span class="hljs-attr">application/json:</span> <span class="hljs-string">|
                    #set($output = $input.path('$'))
                    {
                      "id": "$output.SendMessageResponse.SendMessageResult.MessageId"
                    }</span>
</code></pre>
<h3 id="heading-request-validation">Request Validation</h3>
<p>API Gateway allows us to perform a basic request validation before sending the message to SQS. When the validation fails, API Gateway immediately fails the request, returns a 400 error response, and publishes the validation results in CloudWatch Logs.</p>
<p>We're using the OpenAPI <code>components</code> property to define our model schema, in our case, we'll only validate the request body.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">event:</span>
    <span class="hljs-attr">required:</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">event</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">recipient</span>
        <span class="hljs-bullet">-</span> <span class="hljs-string">timestamp</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">object</span>
    <span class="hljs-attr">properties:</span>
        <span class="hljs-attr">event:</span>
            <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
            <span class="hljs-attr">example:</span> <span class="hljs-string">opened</span>
            <span class="hljs-attr">enum:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">accepted</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">opened</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">clicked</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">failed</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">rejected</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">unsubscribed</span>
                <span class="hljs-bullet">-</span> <span class="hljs-string">complained</span>
        <span class="hljs-attr">recipient:</span>
            <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
            <span class="hljs-attr">example:</span> <span class="hljs-string">"john@example.com"</span>
            <span class="hljs-attr">pattern:</span> <span class="hljs-string">"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$"</span>
        <span class="hljs-attr">timestamp:</span>
            <span class="hljs-attr">type:</span> <span class="hljs-string">string</span>
            <span class="hljs-attr">format:</span> <span class="hljs-string">date-time</span>
            <span class="hljs-attr">example:</span> <span class="hljs-string">"2023-01-01T23:00:00Z"</span>
</code></pre>
<h2 id="heading-iam-role">IAM Role</h2>
<p>The IAM Role will be assumed by the API Gateway service and will allow it to send messages to our SQS queue, it'll also allow API Gateway to write logs into CloudWatch.</p>
<p>Note the IAM policy is only granting the service permission to perform this specific task.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># role allowing AGW to send messages to SQS queue</span>
<span class="hljs-attr">AGW2SQSRole:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::IAM::Role</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">RoleName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-agw2sqs</span>
        <span class="hljs-attr">AssumeRolePolicyDocument:</span>
            <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
            <span class="hljs-attr">Statement:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                  <span class="hljs-attr">Principal:</span>
                      <span class="hljs-attr">Service:</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-string">apigateway.amazonaws.com</span>
                  <span class="hljs-attr">Action:</span> <span class="hljs-string">sts:AssumeRole</span>
        <span class="hljs-attr">Policies:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">PolicyName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-agw2sqs</span>
              <span class="hljs-attr">PolicyDocument:</span>
                  <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
                  <span class="hljs-attr">Statement:</span>
                      <span class="hljs-comment"># allow AGW to send messages to SQS queue</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                        <span class="hljs-attr">Action:</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">sqs:SendMessage</span>
                        <span class="hljs-attr">Resource:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">Queue.Arn</span>
                      <span class="hljs-comment"># let AGW write to CloudWatch</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                        <span class="hljs-attr">Action:</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">logs:CreateLogGroup</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">logs:CreateLogStream</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">logs:PutLogEvents</span>
                        <span class="hljs-attr">Resource:</span> <span class="hljs-string">"*"</span>
</code></pre>
<p>If you're having trouble understanding IAM concepts check my article about it - <a target="_blank" href="https://blog.stefanolaru.com/explain-like-im-5-aws-iam">Explain like I'm 5: AWS IAM</a></p>
<p><a target="_blank" href="https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://stefanolaru-cfn.s3.amazonaws.com/api-gateway-sqs/template.yml&amp;stackName=api-gateway-sqs"><strong>One-click Deployment</strong></a> to get going quickly, or <a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/blob/main/api-gateway-sqs/template.yml">click to view</a> the complete self-contained Cloudformation template that can be deployed into any AWS account.</p>
]]></content:encoded></item><item><title><![CDATA[How to run a highly-available Typesense search in AWS]]></title><description><![CDATA[Retrieving relevant search results blazing-fast is a challenge. A solid search requires typo tolerance, result rankings, synonyms, filtering, faceting, geo search, and so on.
Quite often the underlying database schema is not optimized for our search ...]]></description><link>https://blog.stefanolaru.com/how-to-run-highly-available-typesense-cluster-in-aws</link><guid isPermaLink="true">https://blog.stefanolaru.com/how-to-run-highly-available-typesense-cluster-in-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[cloudformation]]></category><category><![CDATA[Infrastructure as code]]></category><category><![CDATA[Load Balancing]]></category><category><![CDATA[search]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Tue, 20 Dec 2022 12:20:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1671249394516/Ly2zWdhwP.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Retrieving relevant search results blazing-fast is a challenge. A solid search requires typo tolerance, result rankings, synonyms, filtering, faceting, geo search, and so on.</p>
<p>Quite often the underlying database schema is not optimized for our search needs, the relevance of search results is questionable, and running ad-hoc queries on big datasets usually leads to performance issues and slow response.</p>
<p>This is why you should add a dedicated search service into the mix.</p>
<p>Typesense is my favorite, it's simple, fast, typo-tolerant, and open-source. I've been using it on multiple production projects handling searches on datasets of millions of entries.</p>
<p>For production use, it is recommended to run a multi-node Typesense cluster, however, if you're looking for a lighter setup check my other post on <a target="_blank" href="https://blog.stefanolaru.com/running-typesense-search-in-aws"><strong>how to run a single Typesense node in AWS</strong></a>.</p>
<p>In this article, I will explain how to get a highly available (multi-node) Typesense cluster running in AWS using infrastructure as code.</p>
<p>The setup uses 2 AWS CloudFormation stacks that can be deployed into any AWS account:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/blob/main/typesense-multi-node/vpc-template.yml"><strong>the Network Stack</strong></a> (<a target="_blank" href="https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://stefanolaru-cfn.s3.amazonaws.com/typesense-multi-node/vpc-template.yml&amp;stackName=TypesenseVPC">1-click deployment</a>) - creates the VPC and subnets in 3 AZs</p>
</li>
<li><p><a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/blob/main/typesense-multi-node/cluster-template.yml"><strong>the Cluster Stack</strong></a> (<a target="_blank" href="https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://stefanolaru-cfn.s3.amazonaws.com/typesense-multi-node/cluster-template.yml&amp;stackName=TypesenseCluster">1-click deployment</a>)- creates all the cluster resources</p>
</li>
</ul>
<h2 id="heading-how-it-works">How it Works</h2>
<p>Typesense uses the <a target="_blank" href="https://raft.github.io/">Raft consensus algorithm</a> to manage the cluster and recover from node failures. In cluster mode, Typesense will automatically replicate the entire dataset to all nodes. Automatically and continuously.</p>
<p>Raft requires a quorum for consensus and <strong>we need to run a minimum of 3 nodes</strong> to tolerate a 1-node failure, and a 5-node cluster will tolerate failures of up to 2 nodes, but at the expense of slightly higher write latencies.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671208206865/H91FzqVII.jpg" alt class="image--center mx-auto" /></p>
<p>To start a Typesense node as part of a cluster, each node instance needs to have a definition file with the list of all nodes in the following format, separated by commas:</p>
<p><code>&lt;peering_address&gt;:&lt;peering_port&gt;:&lt;api_port&gt;</code></p>
<ul>
<li><p>The <code>peering_address</code> (the EC2 instance private IP address)</p>
</li>
<li><p>The <code>peering_port</code> (the port used for cluster operations - default <code>8107</code>)</p>
</li>
<li><p>The <code>api_port</code> (the port to which clients connect to - default <code>8108</code>)</p>
</li>
</ul>
<p>We will use an Auto Scaling group to launch a minimum of 3 EC2 Instances distributed across 3 Availability Zones and start a Typesense service on each.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># ASG</span>
<span class="hljs-attr">AutoScalingGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::AutoScaling::AutoScalingGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">LaunchConfigurationName:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">LaunchConfig</span>
        <span class="hljs-attr">DesiredCapacity:</span> <span class="hljs-number">3</span>
        <span class="hljs-attr">MinSize:</span> <span class="hljs-number">3</span>
        <span class="hljs-attr">MaxSize:</span> <span class="hljs-number">6</span>
        <span class="hljs-attr">NotificationConfigurations:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">NotificationTypes:</span>
                  <span class="hljs-bullet">-</span> <span class="hljs-string">autoscaling:EC2_INSTANCE_TERMINATE</span>
              <span class="hljs-attr">TopicARN:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">EC2EventsTopic</span>
        <span class="hljs-attr">TargetGroupARNs:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">ALBTargetGroup</span>
        <span class="hljs-attr">VPCZoneIdentifier:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-PublicSubnet1</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-PublicSubnet2</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-PublicSubnet3</span>
</code></pre>
<h3 id="heading-ec2-instance-type">EC2 Instance Type</h3>
<p>CPU capacity is important to handle concurrent search traffic and indexing operations, so <strong>Typesense requires at least 2 vCPUs</strong> of compute capacity to operate.</p>
<p>All the next-gen EC2 T4g instances have at least 2 vCPUs, they're powered by Arm-based AWS Graviton2 processors and are ideal for running applications with moderate CPU usage that experience temporary spikes in usage.</p>
<p>The amount of RAM required is completely dependent on the size of the data you index, but for Typesense to hold the whole index in memory, <strong>the instance memory should be 2-3X the size of the data set</strong>.</p>
<p>As the Typesense process, itself is quite lightweight (20MB RAM with empty dataset) a 2GB memory instance (t4g.small) will likely handle a data set of up to 1GB.</p>
<h3 id="heading-data-storage">Data Storage</h3>
<p>Typesense stores a copy of the raw data on disk and then builds the in-memory index with the data. Then at search time, after determining the final set of documents to return in the API response, it fetches these documents (only) from the disk and puts them in the API response.</p>
<p>We'll use the Amazon EC2 instance store for the node data, although this type of storage is ephemeral and data will be lost if the instance gets terminated. When a new instance is launched the data will be replicated from the other nodes automatically.</p>
<p>We'll have scheduled backups for our peace of mind.</p>
<h2 id="heading-load-balancing">Load Balancing</h2>
<p>AWS Elastic Load Balancing is used to distribute the traffic equally across all nodes, and we're going to add an Application Load Balancer in front of our EC2 instances from all 3 Availability Zones. It will also help us manage the SSL termination.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">ALB:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::ElasticLoadBalancingV2::LoadBalancer</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Name:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}</span>
        <span class="hljs-attr">Subnets:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-PublicSubnet1</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-PublicSubnet2</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-PublicSubnet3</span>
        <span class="hljs-attr">SecurityGroups:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">ALBSecurityGroup.GroupId</span>
</code></pre>
<p>The ALB Security Group allows inbound traffic on ports 80 (HTTP) and 443 (HTTPS).</p>
<pre><code class="lang-yaml"><span class="hljs-attr">ALBSecurityGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::SecurityGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">GroupDescription:</span> <span class="hljs-string">allow</span> <span class="hljs-string">traffic</span> <span class="hljs-string">on</span> <span class="hljs-string">port</span> <span class="hljs-number">80</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-VPCID</span>
        <span class="hljs-attr">SecurityGroupIngress:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
              <span class="hljs-attr">FromPort:</span> <span class="hljs-number">80</span>
              <span class="hljs-attr">ToPort:</span> <span class="hljs-number">80</span>
              <span class="hljs-attr">CidrIp:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">/0</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
              <span class="hljs-attr">FromPort:</span> <span class="hljs-number">443</span>
              <span class="hljs-attr">ToPort:</span> <span class="hljs-number">443</span>
              <span class="hljs-attr">CidrIp:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">/0</span>
</code></pre>
<p>We have 2 ALB listeners:</p>
<ul>
<li><p>one for HTTPS that will forward traffic to the Target Group</p>
</li>
<li><p>one for HTTP that will redirect all requests to HTTPS</p>
</li>
</ul>
<pre><code class="lang-yaml"><span class="hljs-attr">ALBHttpsListener:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::ElasticLoadBalancingV2::Listener</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">DefaultActions:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Type:</span> <span class="hljs-string">forward</span>
              <span class="hljs-attr">TargetGroupArn:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">ALBTargetGroup</span>
        <span class="hljs-attr">LoadBalancerArn:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">ALB</span>
        <span class="hljs-attr">Certificates:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">CertificateArn:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">ALBCertificate</span>
        <span class="hljs-attr">Port:</span> <span class="hljs-number">443</span>
        <span class="hljs-attr">Protocol:</span> <span class="hljs-string">HTTPS</span>
<span class="hljs-attr">ALBListener:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::ElasticLoadBalancingV2::Listener</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">DefaultActions:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Type:</span> <span class="hljs-string">redirect</span>
              <span class="hljs-attr">RedirectConfig:</span>
                  <span class="hljs-attr">Host:</span> <span class="hljs-string">"#{host}"</span>
                  <span class="hljs-attr">Path:</span> <span class="hljs-string">"/#{path}"</span>
                  <span class="hljs-attr">Port:</span> <span class="hljs-number">443</span>
                  <span class="hljs-attr">Protocol:</span> <span class="hljs-string">"HTTPS"</span>
                  <span class="hljs-attr">Query:</span> <span class="hljs-string">"#{query}"</span>
                  <span class="hljs-attr">StatusCode:</span> <span class="hljs-string">HTTP_301</span>
        <span class="hljs-attr">LoadBalancerArn:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">ALB</span>
        <span class="hljs-attr">Port:</span> <span class="hljs-number">80</span>
        <span class="hljs-attr">Protocol:</span> <span class="hljs-string">HTTP</span>
</code></pre>
<p>And finally, the Target Group routes requests to our nodes.</p>
<p>The Target Group also performs health checks on our nodes, it uses Typesense's <code>/health</code> endpoint that will return an HTTP code <code>200</code> and <code>{"ok": true}</code> if our node search service is in a working state.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">ALBTargetGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::ElasticLoadBalancingV2::TargetGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">HealthCheckIntervalSeconds:</span> <span class="hljs-number">30</span>
        <span class="hljs-attr">HealthCheckProtocol:</span> <span class="hljs-string">HTTP</span>
        <span class="hljs-attr">HealthCheckTimeoutSeconds:</span> <span class="hljs-number">15</span>
        <span class="hljs-attr">HealthyThresholdCount:</span> <span class="hljs-number">5</span>
        <span class="hljs-attr">HealthCheckPath:</span> <span class="hljs-string">/health</span>
        <span class="hljs-attr">Matcher:</span>
            <span class="hljs-attr">HttpCode:</span> <span class="hljs-number">200</span>
        <span class="hljs-attr">Name:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}</span>
        <span class="hljs-attr">Port:</span> <span class="hljs-number">8108</span>
        <span class="hljs-attr">Protocol:</span> <span class="hljs-string">HTTP</span>
        <span class="hljs-attr">UnhealthyThresholdCount:</span> <span class="hljs-number">3</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-VPCID</span>
</code></pre>
<p>So we have two health checks in place for our highly available search service:</p>
<ul>
<li><p>Auto Scaling group that checks if our instance is healthy</p>
</li>
<li><p>Load Balancer health checks will check if our application running on the instance is healthy</p>
</li>
</ul>
<h2 id="heading-node-launching">Node Launching</h2>
<p>The Launch Configuration will be used by the Auto Scaling group to configure our EC2 instances (search nodes).</p>
<pre><code class="lang-yaml"><span class="hljs-attr">LaunchConfig:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::AutoScaling::LaunchConfiguration</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">InstanceType:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">InstanceType</span>
        <span class="hljs-attr">ImageId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">LatestLinuxAmiId</span>
        <span class="hljs-attr">IamInstanceProfile:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">EC2InstanceProfile.Arn</span>
        <span class="hljs-attr">SecurityGroups:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">EC2SecurityGroup</span>
        <span class="hljs-attr">UserData:</span>    
            <span class="hljs-attr">Fn::Base64:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">|</span>
                <span class="hljs-comment"># [...] removed for brevity plese check below</span>
</code></pre>
<h2 id="heading-launch-configuration-userdata">Launch Configuration UserData</h2>
<p>UserData allows us to run commands on our instance at launch. When an unhealthy instance is terminated and a new one is created and the UserData will run the commands to ensure the cluster node is reconfigured back to its running state.</p>
<h3 id="heading-1-cloudwatch-agent">1. CloudWatch Agent</h3>
<p>We want to monitor our nodes, CloudWatch Agent will collect metrics and logs from our EC2 instances and send them to CloudWatch. The CloudWatch configuration is created by the CloudFormation template and referenced in the commands below.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># install cloudwatch agent</span>
yum -y install amazon-cloudwatch-agent
<span class="hljs-comment"># auto configure cloudwatch agent</span>
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c ssm:<span class="hljs-variable">${CloudWatchAgentConfig}</span> -s
</code></pre>
<h3 id="heading-2-download-andamp-install-typesense-server">2. Download &amp; Install Typesense Server</h3>
<p>We'll run the service on T4g instances powered by Graviton2 processors running Amazon Linux and there's no package available, so we have to download the <strong>arm64</strong> Binary.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># download &amp; unarchive typesense</span>
curl -O https://dl.typesense.org/releases/<span class="hljs-variable">${TypesenseVersion}</span>/typesense-server-<span class="hljs-variable">${TypesenseVersion}</span>-linux-arm64.tar.gz
tar -xzf typesense-server-<span class="hljs-variable">${TypesenseVersion}</span>-linux-arm64.tar.gz -C /opt/typesense
<span class="hljs-comment"># remove archive</span>
rm typesense-server-<span class="hljs-variable">${TypesenseVersion}</span>-linux-arm64.tar.gz
</code></pre>
<h3 id="heading-3-configure-the-typesense-node">3. Configure the Typesense Node</h3>
<p>We get the EC2 Instance Private IP, create the Typesense data &amp; log folders and the <code>nodes</code> and server config file. Read more about <a target="_blank" href="https://typesense.org/docs/0.23.1/api/server-configuration.html">Typesense server config</a>.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># get instance private IP</span>
EC2_INSTANCE_IP=$(curl -s http://instance-data/latest/meta-data/local-ipv4)

<span class="hljs-comment"># create typesense folders</span>
mkdir -p /opt/typesense/data
mkdir -p /opt/typesense/<span class="hljs-built_in">log</span>

<span class="hljs-comment"># create the nodes file</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$EC2_INSTANCE_IP</span>:<span class="hljs-variable">${TypesensePeeringPort}</span>:<span class="hljs-variable">${TypesenseApiPort}</span>"</span> &gt; /opt/typesense/nodes

<span class="hljs-comment"># create typesense server config file</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"[server]
api-key = <span class="hljs-variable">${TypesenseApiKey.Value}</span>
data-dir = /opt/typesense/data
log-dir = /opt/typesense/log
enable-cors = true
api-port = <span class="hljs-variable">${TypesenseApiPort}</span>
peering-port = <span class="hljs-variable">${TypesensePeeringPort}</span>
peering-address = <span class="hljs-variable">$EC2_INSTANCE_IP</span>
nodes = /opt/typesense/nodes"</span> &gt; /opt/typesense/typesense.ini
</code></pre>
<h3 id="heading-4-create-systemd-service-andamp-enable-the-daemon">4. Create systemd service &amp; enable the daemon</h3>
<p>As we installed it from a binary we have to create a systemd service for the Typesense server. This will make the Typesense server service always available.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># create typesense service</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"[Unit]
Description=Typesense service
After=network.target

[Service]
Type=simple
Restart=always
RestartSec=5
User=ec2-user
ExecStart=/opt/typesense/typesense-server --config=/opt/typesense/typesense.ini

[Install]
WantedBy=default.target"</span> &gt; /etc/systemd/system/typesense.service

<span class="hljs-comment"># start typesense service</span>
systemctl start typesense
<span class="hljs-comment"># enable typesense daemon</span>
systemctl <span class="hljs-built_in">enable</span> typesense
</code></pre>
<h3 id="heading-5-notify-cluster">5. Notify Cluster</h3>
<p>We publish a message using AWS CLI on the SNS topic that a new Typesense node is available.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># notify sns topic of the new instance</span>
aws sns publish --message <span class="hljs-string">"New Instance Ready (instanceId:<span class="hljs-variable">$EC2_INSTANCE_IP</span>)"</span> --topic-arn <span class="hljs-variable">${EC2EventsTopic}</span>  --region <span class="hljs-variable">${AWS::Region}</span>
</code></pre>
<h2 id="heading-cluster-events">Cluster Events</h2>
<p>Every Typesense node needs to be aware of the other cluster nodes in order to automatically replicate the data. When a node instance is terminated or a new node is launched we need to update the configuration on the rest of the nodes.</p>
<p>An SNS topic will receive notifications from the Auto Scaling Group in case of an <code>EC2_INSTANCE_TERMINATE</code> event and also from the AWS CLI (UserData) once the new node is running.</p>
<p>The SNS will then trigger a Lambda that will send a command through the Systems Manager to all the healthy instances to update the <code>nodes</code> definition file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671211428028/MQ6TDQFar.jpg" alt class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/blob/main/typesense-multi-node/functions/instance-listener/index.js">Click here</a> to view the cluster events listener Lambda code.</p>
<h3 id="heading-systems-manager-run-command">Systems Manager Run Command</h3>
<p>AWS SSM helps us securely manage the configuration of our nodes and allows running a shell script on multiple instances at once. This facilitates updating the <code>nodes</code> definition of our Typesense service.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># SSM Document</span>
<span class="hljs-attr">UpdateNodesCommand:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::SSM::Document</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Content:</span>
            <span class="hljs-attr">schemaVersion:</span> <span class="hljs-string">"2.2"</span>
            <span class="hljs-attr">description:</span> <span class="hljs-string">Update</span> <span class="hljs-string">Typesense</span> <span class="hljs-string">Nodes</span>
            <span class="hljs-attr">parameters:</span>
                <span class="hljs-attr">NodeList:</span>
                    <span class="hljs-attr">type:</span> <span class="hljs-string">String</span>
                    <span class="hljs-attr">description:</span> <span class="hljs-string">Nodes</span> <span class="hljs-string">string</span>
            <span class="hljs-attr">mainSteps:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">action:</span> <span class="hljs-string">aws:runShellScript</span>
                  <span class="hljs-attr">name:</span> <span class="hljs-string">updateTypesense</span>
                  <span class="hljs-attr">inputs:</span>
                      <span class="hljs-attr">runCommand:</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-string">echo</span> <span class="hljs-string">"<span class="hljs-template-variable">{{NodeList}}</span>"</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">/opt/typesense/nodes</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-string">sudo</span> <span class="hljs-string">systemctl</span> <span class="hljs-string">restart</span> <span class="hljs-string">typesense</span>

        <span class="hljs-attr">DocumentFormat:</span> <span class="hljs-string">YAML</span>
        <span class="hljs-attr">DocumentType:</span> <span class="hljs-string">Command</span>
        <span class="hljs-attr">Name:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}-update-nodes</span>
        <span class="hljs-attr">TargetType:</span> <span class="hljs-string">/AWS::EC2::Instance</span>
</code></pre>
<h2 id="heading-ec2-iam-role-andamp-security-groups">EC2 IAM Role &amp; Security Groups</h2>
<p>The template will create an Instance Profile, an IAM Role, and a Security Group for the EC2 instances.</p>
<p>The <strong>IAM Role</strong> will be assumed by the EC2 instance and contains policies that will allow only the necessary actions (principle of least privilege).</p>
<ul>
<li><p><code>CloudWatchAgentServerPolicy</code> - to read instance information and write it to CloudWatch Logs and Metrics</p>
</li>
<li><p><code>AmazonSSMManagedInstanceCore</code> - to receive messages for Run Command and read parameters in Parameter Store (CloudWatch agent config)</p>
</li>
<li><p><code>sns:Publish</code> - publish SNS message when a new node is ready</p>
</li>
</ul>
<pre><code class="lang-yaml"><span class="hljs-attr">EC2InstanceRole:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::IAM::Role</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">RoleName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}-instance-role</span>
        <span class="hljs-attr">AssumeRolePolicyDocument:</span>
            <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
            <span class="hljs-attr">Statement:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                  <span class="hljs-attr">Principal:</span>
                      <span class="hljs-attr">Service:</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-string">ec2.amazonaws.com</span>
                  <span class="hljs-attr">Action:</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-string">sts:AssumeRole</span>
        <span class="hljs-attr">Path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">ManagedPolicyArns:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore</span>
        <span class="hljs-attr">Policies:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">PolicyName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}-ec2-policy</span>
              <span class="hljs-attr">PolicyDocument:</span>
                  <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
                  <span class="hljs-attr">Statement:</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                        <span class="hljs-attr">Action:</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">sns:Publish</span>
                        <span class="hljs-attr">Resource:</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">EC2EventsTopic</span>
</code></pre>
<p>The <strong>Instance Profile</strong> allows passing the IAM role to the EC2 instance.</p>
<p>The <strong>Security Group</strong> permits the EC2 instances to:</p>
<ul>
<li><p>receive traffic on Typesense API Port <code>8108</code> from the Load Balancer</p>
</li>
<li><p>communicate with the other VPC nodes on API Port <code>8108</code> and Peering Port <code>8107</code></p>
</li>
</ul>
<pre><code class="lang-yaml"><span class="hljs-attr">EC2SecurityGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::SecurityGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">GroupDescription:</span> <span class="hljs-string">allow</span> <span class="hljs-string">connections</span> <span class="hljs-string">from</span> <span class="hljs-string">ALB</span> <span class="hljs-string">and</span> <span class="hljs-string">SSH,</span> <span class="hljs-string">from</span> <span class="hljs-string">other</span> <span class="hljs-string">instances</span> <span class="hljs-string">within</span> <span class="hljs-string">VPC</span>
        <span class="hljs-attr">VpcId:</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-VPCID</span>
        <span class="hljs-attr">SecurityGroupIngress:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
              <span class="hljs-attr">FromPort:</span> <span class="hljs-number">8108</span>
              <span class="hljs-attr">ToPort:</span> <span class="hljs-number">8108</span>
              <span class="hljs-attr">SourceSecurityGroupId:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">ALBSecurityGroup.GroupId</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
              <span class="hljs-attr">FromPort:</span> <span class="hljs-number">8107</span>
              <span class="hljs-attr">ToPort:</span> <span class="hljs-number">8107</span>
              <span class="hljs-attr">CidrIp:</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-VPCCidrBlock</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
              <span class="hljs-attr">FromPort:</span> <span class="hljs-number">8108</span>
              <span class="hljs-attr">ToPort:</span> <span class="hljs-number">8108</span>
              <span class="hljs-attr">CidrIp:</span> <span class="hljs-type">!ImportValue</span> <span class="hljs-string">typesense-vpc-VPCCidrBlock</span>
</code></pre>
<h2 id="heading-typesense-api-key-generation">Typesense API Key Generation</h2>
<p>The CloudFormation stack will generate a Typesense admin API key using a CustomResource that references a <code>RandomStringGenerator</code> Lambda function.</p>
<p>The <code>TypesenseApiKey</code>is exported in the <code>Output</code> so it can be referenced in other stacks. Optionally it can be stored in a <code>AWS::SSM::Parameter</code> to be used by other AWS services.</p>
<p>The admin API key provides full control over the Typesense API, make sure you create <a target="_blank" href="https://typesense.org/docs/0.23.1/api/api-keys.html#create-an-api-key">Scoped API Keys</a> to be used in your application.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">TypesenseApiKey:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::CloudFormation::CustomResource</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Length:</span> <span class="hljs-number">32</span>
        <span class="hljs-attr">ServiceToken:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">RandomStringGenerator.Arn</span>
</code></pre>
<p>The random string generator Lambda function is deployed using inline code, this works for simple functions when the code length is up to 4096 chars.</p>
<h2 id="heading-summary">Summary</h2>
<p>The setup uses 2 AWS CloudFormation stacks that can be deployed into any AWS account:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/blob/main/typesense-multi-node/vpc-template.yml"><strong>the Network Stack</strong></a> (<a target="_blank" href="https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://stefanolaru-cfn.s3.amazonaws.com/typesense-multi-node/vpc-template.yml&amp;stackName=TypesenseVPC">1-click deployment</a>) - creates the VPC and subnets in 3 AZs</p>
</li>
<li><p><a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/blob/main/typesense-multi-node/cluster-template.yml"><strong>the Cluster Stack</strong></a> (<a target="_blank" href="https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://stefanolaru-cfn.s3.amazonaws.com/typesense-multi-node/cluster-template.yml&amp;stackName=TypesenseCluster">1-click deployment</a>)- creates all the cluster resources</p>
</li>
</ul>
<p>To get started with the Typesense API once you have the service running, check <a target="_blank" href="https://typesense.org/docs/0.23.1/api/">API Reference</a></p>
<p>For educational purposes, this post and the included templates feature some common real-life implementations of the <a target="_blank" href="https://wa.aws.amazon.com/index.en.html">AWS Well-Architected Framework</a> concepts such as high availability, elasticity, scalability, resiliency, security, principles of least privilege, etc.</p>
<p>Cover Photo by <a target="_blank" href="https://unsplash.com/@samthewam24?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Samuel Sianipar</a> on <a target="_blank" href="https://unsplash.com/s/photos/orchestra?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[Explain like I'm 5: AWS IAM]]></title><description><![CDATA[Amazon Web Services (AWS) Identity and Access Management (IAM) controls who can use our AWS resources (authentication) and what they can do with those resources (authorization).
Think of your AWS account as an office building, you're the owner.
Your ...]]></description><link>https://blog.stefanolaru.com/explain-like-im-5-aws-iam</link><guid isPermaLink="true">https://blog.stefanolaru.com/explain-like-im-5-aws-iam</guid><category><![CDATA[AWS]]></category><category><![CDATA[IAM]]></category><category><![CDATA[Amazon Web Services]]></category><category><![CDATA[Explain like I am five]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Sun, 18 Dec 2022 16:20:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1671380266520/DqbycGqRb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Amazon Web Services (AWS) Identity and Access Management (IAM) controls <strong>who</strong> can use our AWS resources (authentication) and <strong>what</strong> they can do with those resources (authorization).</p>
<p>Think of your <strong>AWS account</strong> as an office building, you're the owner.</p>
<p>Your building has great amenities such as a gym, a fancy cafe, an underground garage, an electric car charging station, conference rooms, a technical room, and so on. Those are the <strong>AWS resources</strong>.</p>
<p>You don't want just anyone to be able to come into your building use the gym, grab a coffee, and mess stuff in the technical room, right?</p>
<p>So, you have locks on the doors and you only give an access card to people you trust, like your company employees or contractors.</p>
<p><strong>AWS IAM</strong> works in a similar way, <strong>AWS resources</strong> have "locks" and require permissions to be accessed.</p>
<p>You can then give users (or programs) "keys" to access your AWS resources.</p>
<p>You can give different keys to different users or programs, and you can also decide what they can do with your resources.</p>
<p><strong>General Concepts:</strong></p>
<p><strong>IAM User</strong> - an identity type - single principal (human or an application)</p>
<p><strong>IAM User Group</strong> - a collection of users</p>
<p><strong>IAM Role</strong> - an identity type - multiple principals (human, application, AWS service)</p>
<p><strong>IAM Policy</strong> - a document specifying permissions</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671363119864/-h6FVFC8X.png" alt class="image--center mx-auto" /></p>
<p>Some examples of AWS resources include EC2 instances, S3 buckets, RDS databases, Lambda functions, etc.</p>
<h2 id="heading-iam-user"><strong>IAM User</strong></h2>
<p>The IAM User is a user account that is created within the AWS IAM system.</p>
<p>Each IAM User is given a unique username (within your AWS account) and can authenticate themselves using an <strong>access key</strong> and/or <strong>a password</strong>.</p>
<p>The <strong>access key</strong> is for programmatic access - users that will access your resources via AWS API, CLI, or SDK using an <strong>access key ID</strong> and <strong>secret access key</strong>.</p>
<p><em>The access key is usually for technical users such as developers, sys admins, and other applications (e.g. GitHub).</em></p>
<p>The <strong>password</strong> is for AWS Management Console - users that will access your resources via a graphical interface.</p>
<p>An IAM User has <strong>no permissions</strong> by default but can have both access types enabled, access key and password.</p>
<h2 id="heading-iam-user-groups">IAM User Groups</h2>
<p>IAM User Groups are like a club that the IAM Users join and get special privileges to do certain things. For example, all IAM Users in the "DB Managers" group have permission to add and change things in a database.</p>
<p>So, IAM User Groups are a way to manage and organize IAM Users.</p>
<p>You can create groups and assign users to them, and then apply IAM policies to the group to grant or restrict access to AWS resources.</p>
<p>This is a useful way to manage permissions for large numbers of users, as you can make changes to the group policy and have it apply to all users in the group.</p>
<p>It is also a way to simplify the process of granting permissions to new users, as you can simply add them to the appropriate group instead of having to individually assign permissions.</p>
<h2 id="heading-iam-policy">IAM Policy</h2>
<p>Okay, so think of IAM Policy as a document that contains all the actions you're allowed to perform, or not.</p>
<p>For example, your HVAC contractor will have a policy that allows access to the building but only to the underground garage, the technical room, and the roof.</p>
<p>Similarly, by creating different IAM policies for different IAM users or groups, you can control who has access to which resources in your AWS account.</p>
<p>This helps you make sure that only the right identities have access to the resources they need, and helps you keep your AWS account secure.</p>
<p>IAM policies are written JSON and tell AWS which actions a user or group of users is allowed to perform on which resources.</p>
<p>In the example below the IAM User that has attached that IAM Policy will only be allowed to view the <code>yearly-report.xlsx</code> file from the <code>reports</code> S3 bucket. They won't be able to view the other files or update the report.</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"AllowLatestReport"</span>,
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Action"</span>: [<span class="hljs-string">"s3:GetObject"</span>],
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::reports/yearly-report.xlsx"</span>
        }
    ]
}
</code></pre>
<h2 id="heading-iam-role">IAM Role</h2>
<p>IAM Role is probably the most confusing type of identity, at first.</p>
<p>Remember the cafe in your office building? A catering company supplies it with food and drinks daily and manages the service of the cafe equipment. The number of delivery agents can vary and they're not the same each day, and sometimes they bring in a technician.</p>
<p>You <strong>trust</strong> the catering company employees to <strong>assume</strong> the "catering" <strong>role</strong> and obtain <strong>temporary access</strong> to the underground parking, the elevator, and the cafe to do their job.</p>
<p>An IAM Role works similarly, it's used to grant specific privileges to specific entities for a set duration of time.</p>
<p>An entity can assume a role only if it's allowed by the Trust Policy.</p>
<p>Each time a role is assumed the entity will get assigned a set of temporary (up to 12 hours) security credentials that will allow to access the AWS resource if that's allowed by the Permissions Policy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1671372661774/eZoHd2Qns.png" alt class="image--center mx-auto" /></p>
<p>For example, you can have all the EC2 Instances in your account assume an IAM Role that will allow writing logs and metrics into AWS CloudWatch. You don't have to do that individually.</p>
<h2 id="heading-so-iam-user-or-iam-role">So, IAM User or IAM Role?</h2>
<p>Use <strong>IAM User</strong> when you can identify the single principal to that you want to grant specific privileges for the longer term.</p>
<p>The IAM User can be a human or an application. They will <em>authenticate</em> using an access key id &amp; secret access key or username &amp; password and be <em>authorized</em> by the IAM Policy to perform certain actions on AWS resources.</p>
<p>Use <strong>IAM Role</strong> when you have multiple principals or when you don't know the number of entities that you want to temporarily grant specific privileges.</p>
<p>The entity assuming an IAM Role can be an IAM User or Group, an AWS service or account, a federated identity, etc.</p>
<p>The entity can assume an IAM Role only if it's specified in the role Trust Policy, and then it will be <em>authorized</em> by the IAM Policy to perform certain actions on AWS resources.</p>
]]></content:encoded></item><item><title><![CDATA[How to run Typesense search in AWS]]></title><description><![CDATA[Searching datasets and retrieving relevant results blazing-fast is a challenge, no matter if the underlying database of our application is RDBMS or NoSQL.
If you're serious about search and need typo tolerance, result rankings, synonyms, filtering, f...]]></description><link>https://blog.stefanolaru.com/how-to-run-typesense-search-in-aws</link><guid isPermaLink="true">https://blog.stefanolaru.com/how-to-run-typesense-search-in-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[cloudformation]]></category><category><![CDATA[typesense]]></category><category><![CDATA[infrastructure]]></category><category><![CDATA[search]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Thu, 15 Dec 2022 04:39:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1671078525865/3oqdZt5S_.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Searching datasets and retrieving relevant results blazing-fast is a challenge, no matter if the underlying database of our application is RDBMS or NoSQL.</p>
<p>If you're serious about search and need typo tolerance, result rankings, synonyms, filtering, faceting, geo search, and whatnot, you better forget about those sub-optimal DB queries and bring a dedicated search service into the mix.</p>
<p>There are multiple cloud offerings for hosted search such as Elasticsearch, Algolia, Meilisearch, or Typesense cloud. But due to infrastructure architecture requirements, regulatory reasons, cost optimization, or just cost consolidation it often makes more sense to run our search service in AWS.</p>
<p><em>Note: AWS has its own search service, OpenSearch which is a fork of ElasticSearch. At the time of writing AWS just announced OpenSearch Serverless, which is in beta/preview release.</em></p>
<p>Typesense is my favorite, it's simple, fast, typo-tolerant, and open-source. I've been using it on multiple production projects handling searches on datasets of millions of entries.</p>
<p>For production use, it is recommended to run a multi-node Typesense cluster, however, there are plenty of scenarios when a single-node service will do the job.</p>
<p>In this article, I will explain how to get Typesense running as a single node using infrastructure as code and make it as resilient and robust as it can be in a single Availability Zone and how to size your instance depending on your data set.</p>
<p>The <a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/blob/main/typesense-single-node/template.yml">CloudFormation template</a> will create the following AWS resources:</p>
<ul>
<li><p>EC2 Auto Scaling Group &amp; Launch Configuration</p>
</li>
<li><p>Instance Profile, IAM Roles &amp; Security Group</p>
</li>
<li><p>EBS Volume</p>
</li>
<li><p>Elastic IP</p>
</li>
<li><p>Lambda Function &amp; CloudFormation Custom Resource</p>
</li>
</ul>
<h2 id="heading-a-self-healing-search-service">A self-healing search service</h2>
<p>We're going to launch the instance using EC2 Auto Scaling, there are no additional fees for it, and the service can automatically determine the health status of an instance using EC2 status checks.</p>
<p>If the search service instance is unhealthy, it will be terminated and replaced by a new one with minimum downtime.</p>
<p>When an instance is terminated and a new one gets created, the data stored in the EC2 instance store is lost and the new instance will have a different IP.</p>
<p>That's why it is critical to have persistent block storage and an elastic IP.</p>
<p>EC2 Auto Scaling will make sure to have 1 healthy instance running at all times.</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># ASG</span>
<span class="hljs-attr">AutoScalingGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::AutoScaling::AutoScalingGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">LaunchConfigurationName:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">LaunchConfig</span>
        <span class="hljs-attr">DesiredCapacity:</span> <span class="hljs-number">1</span>
        <span class="hljs-attr">MinSize:</span> <span class="hljs-number">0</span>
        <span class="hljs-attr">MaxSize:</span> <span class="hljs-number">1</span>
        <span class="hljs-attr">AvailabilityZones:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!Select</span> [<span class="hljs-number">0</span>, <span class="hljs-type">!GetAZs</span> <span class="hljs-string">""</span>]
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">PropagateAtLaunch:</span> <span class="hljs-literal">true</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}</span>
</code></pre>
<h3 id="heading-ec2-instance-type">EC2 Instance Type</h3>
<p>CPU capacity is important to handle concurrent search traffic and indexing operations, so <strong>Typesense requires at least 2 vCPUs</strong> of compute capacity to operate.</p>
<p>All the next-gen EC2 T4g instances have at least 2 vCPUs, they're powered by Arm-based AWS Graviton2 processors and are ideal for running applications with moderate CPU usage that experience temporary spikes in usage.</p>
<p>The amount of RAM required is completely dependent on the size of the data you index, but for Typesense to hold the whole index in memory, <strong>the instance memory should be 2-3X the size of the data set</strong>.</p>
<p>As the Typesense process, itself is quite lightweight (20MB RAM with empty dataset) a 2GB memory instance (t4g.small) will likely handle a data set of up to 1GB.</p>
<h3 id="heading-data-storage">Data Storage</h3>
<p>Amazon EC2 instance store is temporary, if the instance stops, hibernates, or terminates the data from the instance store is lost, so we need to use a durable data storage such as EBS.</p>
<p>Typesense stores a copy of the raw data on disk and then builds the in-memory index with the data. Then at search time, after determining the final set of documents to return in the API response, it fetches these documents (only) from the disk and puts them in the API response.</p>
<p>We'll need a fast and durable storage, hence we'll go for EBS <strong>gp3</strong> volume type (next-gen General Purpose SSD).</p>
<p><strong>The size of the EBS volume should be at least the size of the raw dataset.</strong></p>
<h2 id="heading-launch-configuration">Launch Configuration</h2>
<p>The Launch Configuration will be used by the Auto Scaling group to configure our EC2 instance.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">LaunchConfig:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::AutoScaling::LaunchConfiguration</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">InstanceType:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">InstanceType</span>
        <span class="hljs-attr">ImageId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">LatestLinuxAmiId</span>
        <span class="hljs-attr">IamInstanceProfile:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">EC2InstanceProfile.Arn</span>
        <span class="hljs-attr">SecurityGroups:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">EC2SecurityGroup</span>
        <span class="hljs-attr">UserData:</span>
            <span class="hljs-attr">Fn::Base64:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">|</span>
                <span class="hljs-comment"># [...] removed for brevity plese check below</span>
</code></pre>
<h2 id="heading-launch-configuration-userdata">Launch Configuration UserData</h2>
<p>This is where most of the auto-healing magic happens, UserData allows us to run commands on our instance at launch. When an unhealthy instance is terminated and a new one is created it will be empty.</p>
<p>UserData will run the commands to ensure the search service is reconfigured back to its running state.</p>
<h3 id="heading-1-associate-the-ip-andamp-attach-the-ebs-volume">1. Associate the IP &amp; attach the EBS volume</h3>
<pre><code class="lang-bash"><span class="hljs-meta">#!/usr/bin/env bash</span>
<span class="hljs-comment"># get instance-id</span>
EC2_INSTANCE_ID=$(curl -s http://instance-data/latest/meta-data/instance-id)

<span class="hljs-comment"># associate the Elastic IP address to the instance</span>
aws ec2 associate-address --instance-id <span class="hljs-variable">$EC2_INSTANCE_ID</span> --allocation-id <span class="hljs-variable">${ElasticIP.AllocationId}</span> --allow-reassociation  --region <span class="hljs-variable">${AWS::Region}</span>

<span class="hljs-comment"># attach volume</span>
aws ec2 attach-volume --device <span class="hljs-variable">${VolumeDevice}</span> --instance-id <span class="hljs-variable">$EC2_INSTANCE_ID</span> --volume-id <span class="hljs-variable">${EBSVolume}</span> --region <span class="hljs-variable">${AWS::Region}</span>

<span class="hljs-comment"># wait for volume to be properly attached</span>
DATA_STATE=<span class="hljs-string">"unknown"</span>
until [ <span class="hljs-string">"<span class="hljs-variable">${!DATA_STATE}</span>"</span> == <span class="hljs-string">"attached"</span> ]; <span class="hljs-keyword">do</span>
    DATA_STATE=$(aws ec2 describe-volumes \
        --region <span class="hljs-variable">${AWS::Region}</span> \
        --filters \
            Name=attachment.instance-id,Values=<span class="hljs-variable">${!EC2_INSTANCE_ID}</span> \
            Name=attachment.device,Values=<span class="hljs-variable">${VolumeDevice}</span> \
        --query Volumes[].Attachments[].State \
        --output text)

    sleep 5
<span class="hljs-keyword">done</span>

<span class="hljs-comment"># format the volume if doesn't have file system (on stack creation)</span>
blkid --match-token TYPE=ext4 <span class="hljs-variable">${VolumeDevice}</span> || mkfs.ext4 -m0 <span class="hljs-variable">${VolumeDevice}</span>

<span class="hljs-comment"># create mount root folder</span>
mkdir -p <span class="hljs-variable">${VolumeMountPath}</span>

<span class="hljs-comment"># mount the volume to folder</span>
mount <span class="hljs-variable">${VolumeDevice}</span> <span class="hljs-variable">${VolumeMountPath}</span>

<span class="hljs-comment"># persist the volume on restart</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">${VolumeDevice}</span> <span class="hljs-variable">${VolumeMountPath}</span> ext4 defaults,nofail 0 2"</span> &gt;&gt; /etc/fstab
</code></pre>
<h3 id="heading-2-configure-the-typesense-server">2. Configure the Typesense Server</h3>
<p>If you're using the CloudFormation template defaults the <code>VolumeMountPath</code> is set to <code>/mnt/ebs</code> hence:</p>
<ul>
<li><p>The config file will be at <code>/mnt/ebs/typesense.ini</code></p>
</li>
<li><p>Logs will be under <code>/mnt/ebs/log/</code></p>
</li>
<li><p>Data will be stored under <code>/mnt/ebs/data/</code></p>
</li>
</ul>
<pre><code class="lang-bash"><span class="hljs-comment"># create typesense server config</span>
<span class="hljs-keyword">if</span> [ ! -f <span class="hljs-variable">${VolumeMountPath}</span>/typesense.ini ]
<span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"[server]
api-key = <span class="hljs-variable">${TypesenseApiKey.Value}</span>
data-dir = <span class="hljs-variable">${VolumeMountPath}</span>/data
log-dir = <span class="hljs-variable">${VolumeMountPath}</span>/log
api-port = <span class="hljs-variable">${TypesensePort}</span>"</span> &gt; <span class="hljs-variable">${VolumeMountPath}</span>/typesense.ini
    chown ec2-user:ec2-user <span class="hljs-variable">${VolumeMountPath}</span>/typesense.ini
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># create typesense data folder if not exists</span>
<span class="hljs-keyword">if</span> [ ! -d <span class="hljs-variable">${VolumeMountPath}</span>/data ]
<span class="hljs-keyword">then</span>
    mkdir -p <span class="hljs-variable">${VolumeMountPath}</span>/data
    chown ec2-user:ec2-user <span class="hljs-variable">${VolumeMountPath}</span>/data
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># create typesense logs folder if not exists</span>
<span class="hljs-keyword">if</span> [ ! -d <span class="hljs-variable">${VolumeMountPath}</span>/<span class="hljs-built_in">log</span> ]
<span class="hljs-keyword">then</span>
    mkdir -p <span class="hljs-variable">${VolumeMountPath}</span>/<span class="hljs-built_in">log</span>
    chown ec2-user:ec2-user <span class="hljs-variable">${VolumeMountPath}</span>/<span class="hljs-built_in">log</span>
<span class="hljs-keyword">fi</span>
</code></pre>
<h3 id="heading-3-download-andamp-install-typesense-server">3. Download &amp; Install Typesense Server</h3>
<p>As we're running the service on a T4g instance powered by Graviton2 processors we have to download the <strong>arm64</strong> Binary.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># download &amp; unarchive typesense</span>
curl -O https://dl.typesense.org/releases/<span class="hljs-variable">${TypesenseVersion}</span>/typesense-server-<span class="hljs-variable">${TypesenseVersion}</span>-linux-arm64.tar.gz
tar -xzf typesense-server-<span class="hljs-variable">${TypesenseVersion}</span>-linux-arm64.tar.gz -C /home/ec2-user
<span class="hljs-comment"># remove archive</span>
rm typesense-server-<span class="hljs-variable">${TypesenseVersion}</span>-linux-arm64.tar.gz
</code></pre>
<h3 id="heading-4-create-systemd-service-andamp-enable-the-daemon">4. Create systemd service &amp; enable the daemon</h3>
<p>As we installed it from a binary we have to create a systemd service for the Typesense server. This will make the Typesense server service always available.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># create typesense service</span>
<span class="hljs-built_in">echo</span> <span class="hljs-string">"[Unit]
Description=Typesense service
After=network.target

[Service]
Type=simple
Restart=always
RestartSec=5
User=ec2-user
ExecStart=/home/ec2-user/typesense-server --config=<span class="hljs-variable">${VolumeMountPath}</span>/typesense.ini

[Install]
WantedBy=default.target"</span> &gt; /etc/systemd/system/typesense.service

<span class="hljs-comment"># start typesense service</span>
systemctl start typesense
<span class="hljs-comment"># enable typesense daemon</span>
systemctl <span class="hljs-built_in">enable</span> typesense
</code></pre>
<h2 id="heading-ebs-andamp-elastic-ip">EBS &amp; Elastic IP</h2>
<p><strong>Important:</strong> the instance needs to be launched in the same Availability Zone as the EBS volume.</p>
<p><strong>Tip:</strong> to save costs on dev instances, you can create EC2 Auto Scaling scheduled actions that set autoscaling to 0 at night and back to 1 during work hours.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">EBSVolume:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::Volume</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Size:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">VolumeSize</span>
        <span class="hljs-attr">VolumeType:</span> <span class="hljs-string">gp3</span>
        <span class="hljs-attr">AvailabilityZone:</span> <span class="hljs-type">!Select</span> [<span class="hljs-number">0</span>, <span class="hljs-type">!GetAZs</span> <span class="hljs-string">""</span>]
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}</span>
    <span class="hljs-attr">DeletionPolicy:</span> <span class="hljs-string">Snapshot</span>

<span class="hljs-attr">ElasticIP:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::EIP</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Tags:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">Key:</span> <span class="hljs-string">Name</span>
              <span class="hljs-attr">Value:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}</span>
</code></pre>
<h2 id="heading-ec2-iam-role-andamp-security-groups">EC2 IAM Role &amp; Security Groups</h2>
<p>The template will create an Instance Profile, an IAM Role, and a Security Group.</p>
<p>The <strong>IAM Role</strong> will be assumed by the EC2 instance so it can attach the EBS volume and self-associate the Elastic IP. These commands are run using AWS CLI in the Launch Configuration <code>UserData</code> .</p>
<p>The <strong>Instance Profile</strong> allows passing the IAM role to the EC2 instance.</p>
<p>The <strong>Security Group</strong> permits the instance to receive traffic on the port used by the Typesense server (8108) and on port 22 for SSH access.</p>
<p><em>Note:</em> a <code>LambdaExecutionRole</code> is also created to allow the API Key generation function to write logs to AWS CloudWatch, but it's omitted for brevity.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">EC2InstanceProfile:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::IAM::InstanceProfile</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">InstanceProfileName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}-instance-profile</span>
        <span class="hljs-attr">Path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">Roles:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">EC2InstanceRole</span>

<span class="hljs-attr">EC2InstanceRole:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::IAM::Role</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">RoleName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}-instance-role</span>
        <span class="hljs-attr">AssumeRolePolicyDocument:</span>
            <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
            <span class="hljs-attr">Statement:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                  <span class="hljs-attr">Principal:</span>
                      <span class="hljs-attr">Service:</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-string">ec2.amazonaws.com</span>
                  <span class="hljs-attr">Action:</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-string">sts:AssumeRole</span>
        <span class="hljs-attr">Path:</span> <span class="hljs-string">/</span>
        <span class="hljs-attr">Policies:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">PolicyName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}-instance-policy</span>
              <span class="hljs-attr">PolicyDocument:</span>
                  <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
                  <span class="hljs-attr">Statement:</span>
                      <span class="hljs-comment"># allow attaching EBS volumes</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                        <span class="hljs-attr">Action:</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">ec2:AttachVolume</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">ec2:DescribeVolumes</span>
                        <span class="hljs-attr">Resource:</span> <span class="hljs-string">"*"</span>
                      <span class="hljs-comment"># allow Elastic IP association</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">Allow</span>
                        <span class="hljs-attr">Action:</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">ec2:AssociateAddress</span>
                        <span class="hljs-attr">Resource:</span> <span class="hljs-string">"*"</span>
<span class="hljs-comment"># SG</span>
<span class="hljs-attr">EC2SecurityGroup:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::EC2::SecurityGroup</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">GroupDescription:</span> <span class="hljs-string">allow</span> <span class="hljs-string">connections</span> <span class="hljs-string">on</span> <span class="hljs-string">HTTP</span> <span class="hljs-string">&amp;</span> <span class="hljs-string">SSH</span>
        <span class="hljs-attr">SecurityGroupIngress:</span>
            <span class="hljs-comment"># Allow HTTP traffic on Typesense port</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
              <span class="hljs-attr">FromPort:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">TypesensePort</span>
              <span class="hljs-attr">ToPort:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">TypesensePort</span>
              <span class="hljs-attr">CidrIp:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">/0</span>
            <span class="hljs-comment"># Allow SSH - !! narrow CidrIp down, too broad</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">IpProtocol:</span> <span class="hljs-string">tcp</span>
              <span class="hljs-attr">FromPort:</span> <span class="hljs-number">22</span>
              <span class="hljs-attr">ToPort:</span> <span class="hljs-number">22</span>
              <span class="hljs-attr">CidrIp:</span> <span class="hljs-number">0.0</span><span class="hljs-number">.0</span><span class="hljs-number">.0</span><span class="hljs-string">/0</span>
</code></pre>
<h2 id="heading-typesense-api-key-generation">Typesense API Key Generation</h2>
<p>The CloudFormation stack will generate a Typesense admin API key using a CustomResource that references a <code>RandomStringGenerator</code> Lambda function.</p>
<p>The <code>TypesenseApiKey</code>is exported in the <code>Output</code> so it can be referenced in other stacks. Optionally it can be stored in a <code>AWS::SSM::Parameter</code> to be used by other AWS services.</p>
<p>The admin API key provides full control over the Typesense API, make sure you create <a target="_blank" href="https://typesense.org/docs/0.23.1/api/api-keys.html#create-an-api-key">Scoped API Keys</a> to be used in your application.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">TypesenseApiKey:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::CloudFormation::CustomResource</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">Length:</span> <span class="hljs-number">32</span>
        <span class="hljs-attr">ServiceToken:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">RandomStringGenerator.Arn</span>
</code></pre>
<p>The random string generator Lambda function is deployed using inline code, this works for simple functions when the code length is up to 4096 chars.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">RandomStringGenerator:</span>
    <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Lambda::Function</span>
    <span class="hljs-attr">Properties:</span>
        <span class="hljs-attr">FunctionName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">${AWS::StackName}-${EnvironmentType}-random-string</span>
        <span class="hljs-attr">Code:</span>
            <span class="hljs-attr">ZipFile:</span> <span class="hljs-string">&gt;
                const response = require("cfn-response"), crypto = require("crypto");
                exports.handler = (event, context) =&gt;{
                    response.send(event, context, response.SUCCESS, {Value:crypto.randomBytes(parseInt(event['ResourceProperties']['Length'])).toString('hex')});
                };
</span>        <span class="hljs-attr">Handler:</span> <span class="hljs-string">index.handler</span>
        <span class="hljs-attr">Role:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">LambdaExecutionRole.Arn</span>
        <span class="hljs-attr">Runtime:</span> <span class="hljs-string">nodejs16.x</span>
        <span class="hljs-attr">MemorySize:</span> <span class="hljs-number">128</span>
        <span class="hljs-attr">Timeout:</span> <span class="hljs-number">10</span>
</code></pre>
<p><a target="_blank" href="https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?templateURL=https://stefanolaru-cfn.s3.amazonaws.com/typesense-single-node/template.yml&amp;stackName=Typesense"><strong>One-click Deployment</strong></a> to get going quickly, or <a target="_blank" href="https://github.com/stefanolaru/aws-cloudformation-templates/blob/main/typesense-single-node/template.yml">click to view</a> the complete self-contained Cloudformation template that can be deployed into any AWS account.</p>
<p>For complete details about Typesense API check <a target="_blank" href="https://typesense.org/docs/0.23.1/api/">API Reference</a></p>
<h2 id="heading-summary">Summary</h2>
<p>Although this CloudFormation stack template is made especially to get a Typesense search service up and running in minutes, it can be repurposed easily, e.g. for installing WordPress instead of Typesense.</p>
<p>For educational purposes, it features some common real-life scenarios and pattern examples such as:</p>
<ul>
<li><p>Create a single self-healing instance with EC2 Auto Scaling</p>
</li>
<li><p>Use Launch Configuration and UserData to run commands on the instance at launch</p>
</li>
<li><p>Persistent auto-mounting EBS storage + creating a filesystem on new volumes</p>
</li>
<li><p>Auto-install a service from a Linux Binary</p>
</li>
<li><p>Creating a Linux systemd service &amp; daemon</p>
</li>
<li><p>Use of Metadata to group and sort CloudFormation parameters</p>
</li>
<li><p>How to deploy a Lambda function with inline code</p>
</li>
<li><p>Use of Custom Resource to trigger a Lambda and reference the output</p>
</li>
<li><p>Use of CloudFormation Intrinsic Functions (!Ref, !GetAtt, !Sub, !Select, !GetAZs)</p>
</li>
</ul>
<p>Cover photo by <a target="_blank" href="https://unsplash.com/@laughayette?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Marten Newhall</a> on <a target="_blank" href="https://unsplash.com/s/photos/search?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p>
]]></content:encoded></item><item><title><![CDATA[8 DynamoDB bite-sized tips]]></title><description><![CDATA[I've been working mainly with relational databases for more than a decade now, but DynamoDB is my database of choice when it comes to serverless apps. 
Working with DynamoDB requires a different mindset than working with relational databases.
Here ar...]]></description><link>https://blog.stefanolaru.com/8-dynamodb-bite-sized-tips</link><guid isPermaLink="true">https://blog.stefanolaru.com/8-dynamodb-bite-sized-tips</guid><category><![CDATA[AWS]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[serverless]]></category><category><![CDATA[best practices]]></category><category><![CDATA[Programming Tips]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Mon, 11 Apr 2022 04:38:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1649651781862/BogJ6vwZF.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been working mainly with relational databases for more than a decade now, but DynamoDB is my database of choice when it comes to serverless apps. </p>
<p>Working with DynamoDB requires a different mindset than working with relational databases.</p>
<p>Here are 8 bite-sized tips:</p>
<h3 id="heading-1-know-your-access-patterns-in-advance">1. Know your access patterns in advance</h3>
<p>Spend more time upfront so you won’t regret it later.</p>
<p>Before you design your DynamoDB schema, know the questions it will need to answer, and write down all the access patterns.</p>
<p>Use <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.html">AWS NoSQL Workbench</a> to model your data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649650954724/6N4WHfN5V.png" alt="Group 3.png" /></p>
<h3 id="heading-2-keep-related-data-together">2. Keep related data together</h3>
<p>Maintain as few tables as possible, if modeling relational data, aim for a single-table design.</p>
<p>Use generic attributes for indexes, have fun with index overloading.</p>
<p>If you want to see a real-life example check out my other article on <a target="_blank" href="https://blog.stefanolaru.com/how-to-model-relational-data-in-dynamodb">how I'm modeling data</a> for <a target="_blank" href="https://monithor.co">monithor.co</a>.</p>
<p>Also, make sure you watch <a target="_blank" href="https://www.twitter.com/alexbdebrie">@alexbdebrie</a> explain single-table design.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=BnDKD_Zv0og">https://www.youtube.com/watch?v=BnDKD_Zv0og</a></div>
<h3 id="heading-3-avoid-scans">3. Avoid Scans</h3>
<p>Scan operations are less efficient as they always scan the entire table and then filter out values to return the result.</p>
<p>As a table or index grows, the Scan operation slows. Avoid it on large datasets, but if there are no other options use <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html#Scan.ParallelScan">Parallel Scan</a>.</p>
<h3 id="heading-4-use-conditionexpression-to-prevent-overwriting">4. Use ConditionExpression to prevent overwriting</h3>
<p>If an item with the same primary key exists in the table, the PutItem operation will replace it.</p>
<p>Use ConditionExpression to avoid overwriting in a single call Instead of doing 2 calls (a GetItem before PutItem).</p>
<h3 id="heading-5-use-gsi-write-sharding-to-avoid-hot-partitions">5. Use GSI Write Sharding to avoid hot partitions</h3>
<p>Distribute items evenly across key Aspace to avoid hot partitions. Do that by expanding the space, add a random number or a calculated suffix to the partition key.</p>
<p>Use parallel scatter-gather queries to retrieve the results.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1649650942029/dUOPje6v4.png" alt="Group 2-1.png" /></p>
<h3 id="heading-6-use-transactions-to-ensure-consistency">6. Use Transactions to ensure consistency</h3>
<p>Group multiple actions together and submit them as a single all-or-nothing operation, if one action fails they’ll all fail.</p>
<p>Don't group unnecessary operations in a transaction, simpler transactions are more likely to succeed.</p>
<h3 id="heading-7-use-ttl-to-delete-expired-items">7. Use TTL to delete expired items</h3>
<p>Leverage the TTL (Time-to-Live) to auto delete items. E.g. auto-remove log data older than X days from a table just by setting the TTL to the desired timestamp.</p>
<p>Just pay attention to the items you set the TTL attribute to.</p>
<h3 id="heading-8-use-on-demand-pricing-at-first">8. Use On-Demand Pricing at first</h3>
<p>Yes, you'll read that it can be about 7x more expensive, but unless your DynamoDB bill is $$$ it won’t worth the headache.</p>
<p>Once you know your usage patterns you can switch to Provisioned and fine-tune it.</p>
<h3 id="heading-bonus">Bonus</h3>
<p><strong>Watch <a target="_blank" href="https://www.twitter.com/houlihan_rick">@houlihan_rick</a> deep-dive videos on DynamoDB advanced design patterns AWS reInvent</strong>.</p>
<p>Search YouTube and watch them all from 2017 to date, repetition is the mother of all learning.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=xfxBhvGpoa0">https://www.youtube.com/watch?v=xfxBhvGpoa0</a></div>
]]></content:encoded></item><item><title><![CDATA[Create a static site on AWS with CloudFormation]]></title><description><![CDATA[A short story about Monithor's static website
click here to jump straight to the technical part
Leaving the landing page by the very end, when in my acceptance the project is "ready" to launch is a mistake I did way too many times in the past. Nearly...]]></description><link>https://blog.stefanolaru.com/create-a-static-site-on-aws-with-cloudformation</link><guid isPermaLink="true">https://blog.stefanolaru.com/create-a-static-site-on-aws-with-cloudformation</guid><category><![CDATA[AWS]]></category><category><![CDATA[JAMstack]]></category><category><![CDATA[deployment]]></category><category><![CDATA[infrastructure]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Thu, 31 Mar 2022 04:41:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1648699881306/0yf7DozY5.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-a-short-story-about-monithors-static-website">A short story about Monithor's static website</h3>
<p><a class="post-section-overview" href="#heading-deploy-to-aws">click here to jump straight to the technical part</a></p>
<p>Leaving the landing page by the very end, when in my acceptance the project is "ready" to launch is a mistake I did way too many times in the past. Nearly all my abandoned projects were code-first, marketing-later. </p>
<p>The pattern is quite common among fellow developers: buy a domain ⟶ code like crazy for a few weeks  ⟶ run out of steam / find a new project idea ⟶ abandon the project ⟶ repeat. Very few projects even have the chance to get in front of potential users.</p>
<p>Although this article is published as the 3rd in the <a target="_blank" href="https://blog.stefanolaru.com/series/building-monithor">series</a>, the landing page was the first thing I did for Monithor. A landing page helps you look at things from the other side of the fence, and writing the copy gives clarity and helps better define the scope.</p>
<p>I wanted to avoid getting lost in details and spend a lot of time designing &amp; coding the template for the landing page, so I went straight to <a target="_blank" href="https://htmlrev.com/pro-templates.html">HTMLRev</a> and got a great-looking template. I highly recommend Lucian's work, crazy attention to detail, and the code quality is great. </p>
<p>The logo and the color palette are from <a target="_blank" href="https://logology.co">logology</a>, they have a really fun and quick process and the result is great.</p>
<p>I glued the pieces together, wrote the copy, and a day later I had a great-looking landing page that was ready to be deployed. I usually deploy static sites to Netlify, but as Monithor will be built entirely on top of AWS it made more sense to have it all in one place.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648699566684/mpnWUyAmu.png" alt="Screenshot 2022-03-31 at 06.45.13.png" /></p>
<h2 id="heading-deploy-to-aws">Deploy to AWS</h2>
<p>Deploying a static website to AWS might sound simple, but if you want a custom domain with an SSL certificate, and a proper non-www to www redirect, things get messy. It's not complicated, but it's a very cumbersome process to set it up from the AWS console, I did it so many times that I got fed up.</p>
<p>As a developer, fan of Infrastructure as Code (IaC), I decided to create a CloudFormation template that will save you time in the future.</p>
<h3 id="heading-the-standard-way">The standard way</h3>
<p>But first, let me describe the standard way of setting up a static website on AWS to understand why I want to avoid it in the future:</p>
<ol>
<li>create two S3 buckets, one for the naked domain (non-www) and one for the www</li>
<li>enable static website hosting on buckets</li>
<li>set the non-www bucket to redirect requests to the www domain (or the other way around)</li>
<li>request a certificate to cover both the non-www and the www domain</li>
<li>add CNAMES in the Route53 hosted zone to validate the domain for the certificate</li>
<li>wait for the certificate to be issued</li>
<li>create two CloudFront distributions, one for each of the buckets</li>
<li>make sure each distribution Origin points to the bucket website endpoint, not the bucket URL</li>
<li>go back to Route53 and create alias entries to point the domain to the CloudFront distros</li>
<li>done, if we didn't make any mistake</li>
</ol>
<h3 id="heading-the-easy-way">The easy way</h3>
<p>The CloudFormation template will save you time and frustration:</p>
<ul>
<li>creates a single S3 bucket with the required policy</li>
<li>creates a single CloudFront distribution and sets the proper Origin</li>
<li>issues and installs the SSL certificate</li>
<li>automates all the required Route53 DNS records</li>
<li>manages redirects with a CloudFront Function</li>
</ul>
<p>You simply type <code>npm run create-stack</code> command in the terminal and wait for the magic to happen. It will take about 5 minutes to complete though, you can monitor the progress from the <a target="_blank" href="https://us-east-1.console.aws.amazon.com/cloudformation">AWS Console</a></p>
<p>You just need to:</p>
<ul>
<li>have the domain purchased and pointed to the hosted zone created for it in AWS Route53 before creating the stack</li>
<li>have AWS CLI installed and a user with AdministratorAccess</li>
</ul>
<h3 id="heading-create-the-stack">Create the stack</h3>
<p>The <code>create-stack</code> command is as below:</p>
<pre><code class="lang-shell">aws cloudformation create-stack \
--stack-name STACK_NAME \
--template-body file://template.yml \
--profile AWS_PROFILE \
--region us-east-1 \
--parameters ParameterKey=DomainName,ParameterValue=DOMAIN_NAME ParameterKey=HostedZoneId,ParameterValue=HOSTED_ZONE_ID
</code></pre>
<p>Where</p>
<ul>
<li><code>STACK_NAME</code> - is your stack name</li>
<li><code>AWS_PROFILE</code> - is your <a target="_blank" href="https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials_profiles.html">credentials profile</a> in the </li>
<li><code>DOMAIN_NAME</code> - is your domain name (the naked domain, e.g. <em>example.com</em>)</li>
<li><code>HOSTED_ZONE_ID</code> - is your Route53 zone for the domain, should look like <em>Z07414553OA4KK51T5EZA</em></li>
</ul>
<h3 id="heading-deploy-the-site">Deploy the site</h3>
<p>The <code>deploy</code> command will deploy your static site to the S3 bucket and invalidate the CloudFront cache. You may update as per your needs, by default the copy command excludes all paths in the current folder, in order to prevent deploying sensitive info.</p>
<pre><code class="lang-shell">aws s3 cp ./ s3://S3_BUCKET_NAME \
--acl public-read \
--profile AWS_PROFILE \
--recursive \
--exclude "*" \
--include index.html \
--include 404.html \
&amp;&amp; aws cloudfront create-invalidation \
--profile monithor \
--distribution-id DISTRIBUTION_ID \
--paths "/*"
</code></pre>
<p>Where:</p>
<ul>
<li><code>S3_BUCKET_NAME</code> - is your S3 bucket name</li>
<li><code>AWS_PROFILE</code> - is your <a target="_blank" href="https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials_profiles.html">credentials profile</a></li>
<li><code>DISTRIBUTION_ID</code> - is your CloudFront distribution ID</li>
</ul>
<p>And here's the CloudFormation template:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Description:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">a</span> <span class="hljs-string">static</span> <span class="hljs-string">site</span>

<span class="hljs-attr">Parameters:</span>
    <span class="hljs-attr">DomainName:</span>
        <span class="hljs-attr">Description:</span> <span class="hljs-string">Domain</span> <span class="hljs-string">name</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">String</span>
    <span class="hljs-attr">HostedZoneId:</span>
        <span class="hljs-attr">Description:</span> <span class="hljs-string">Hosted</span> <span class="hljs-string">Zone</span> <span class="hljs-string">ID</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">String</span>

<span class="hljs-attr">Resources:</span>
    <span class="hljs-attr">S3Bucket:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::S3::Bucket</span>
        <span class="hljs-attr">DeletionPolicy:</span> <span class="hljs-string">Delete</span>
        <span class="hljs-attr">Properties:</span>
            <span class="hljs-attr">AccessControl:</span> <span class="hljs-string">PublicRead</span>
            <span class="hljs-attr">BucketName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"${AWS::StackName}"</span>
            <span class="hljs-attr">WebsiteConfiguration:</span>
                <span class="hljs-attr">ErrorDocument:</span> <span class="hljs-string">"404.html"</span>
                <span class="hljs-attr">IndexDocument:</span> <span class="hljs-string">"index.html"</span>

    <span class="hljs-attr">S3BucketPolicy:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::S3::BucketPolicy</span>
        <span class="hljs-attr">Properties:</span>
            <span class="hljs-attr">Bucket:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">S3Bucket</span>
            <span class="hljs-attr">PolicyDocument:</span>
                <span class="hljs-attr">Version:</span> <span class="hljs-string">"2012-10-17"</span>
                <span class="hljs-attr">Statement:</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-attr">Effect:</span> <span class="hljs-string">"Allow"</span>
                      <span class="hljs-attr">Action:</span> <span class="hljs-string">"s3:GetObject"</span>
                      <span class="hljs-attr">Principal:</span> <span class="hljs-string">"*"</span>
                      <span class="hljs-attr">Resource:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"${S3Bucket.Arn}/*"</span>
    <span class="hljs-comment">#</span>
    <span class="hljs-attr">CertificateManagerCertificate:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::CertificateManager::Certificate</span>
        <span class="hljs-attr">Properties:</span>
            <span class="hljs-comment"># naked domain</span>
            <span class="hljs-attr">DomainName:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">DomainName</span>
            <span class="hljs-comment"># add www to certificate</span>
            <span class="hljs-attr">SubjectAlternativeNames:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"www.${DomainName}"</span>
            <span class="hljs-attr">ValidationMethod:</span> <span class="hljs-string">DNS</span>
            <span class="hljs-attr">DomainValidationOptions:</span>
                <span class="hljs-comment"># DNS record for the naked domain</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">DomainName:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">DomainName</span>
                  <span class="hljs-attr">HostedZoneId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">HostedZoneId</span>
                <span class="hljs-comment"># DNS record for the www domain</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">DomainName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"www.${DomainName}"</span>
                  <span class="hljs-attr">HostedZoneId:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">HostedZoneId</span>
    <span class="hljs-comment">#</span>
    <span class="hljs-attr">CloudFrontDistribution:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::CloudFront::Distribution</span>
        <span class="hljs-attr">Properties:</span>
            <span class="hljs-attr">DistributionConfig:</span>
                <span class="hljs-attr">Aliases:</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">DomainName</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"www.${DomainName}"</span>
                <span class="hljs-attr">CustomErrorResponses:</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-attr">ErrorCachingMinTTL:</span> <span class="hljs-number">60</span>
                      <span class="hljs-attr">ErrorCode:</span> <span class="hljs-number">404</span>
                      <span class="hljs-attr">ResponseCode:</span> <span class="hljs-number">404</span>
                      <span class="hljs-attr">ResponsePagePath:</span> <span class="hljs-string">"/404.html"</span>
                <span class="hljs-attr">DefaultCacheBehavior:</span>
                    <span class="hljs-attr">AllowedMethods:</span>
                        <span class="hljs-bullet">-</span> <span class="hljs-string">GET</span>
                        <span class="hljs-bullet">-</span> <span class="hljs-string">HEAD</span>
                    <span class="hljs-attr">CachedMethods:</span>
                        <span class="hljs-bullet">-</span> <span class="hljs-string">GET</span>
                        <span class="hljs-bullet">-</span> <span class="hljs-string">HEAD</span>
                    <span class="hljs-attr">Compress:</span> <span class="hljs-literal">true</span>
                    <span class="hljs-attr">DefaultTTL:</span> <span class="hljs-number">86400</span>
                    <span class="hljs-attr">ForwardedValues:</span>
                        <span class="hljs-attr">Cookies:</span>
                            <span class="hljs-attr">Forward:</span> <span class="hljs-string">none</span>
                        <span class="hljs-attr">QueryString:</span> <span class="hljs-literal">true</span>
                    <span class="hljs-attr">MaxTTL:</span> <span class="hljs-number">31536000</span>
                    <span class="hljs-attr">SmoothStreaming:</span> <span class="hljs-literal">false</span>
                    <span class="hljs-attr">TargetOriginId:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"S3-${AWS::StackName}"</span>
                    <span class="hljs-attr">ViewerProtocolPolicy:</span> <span class="hljs-string">"redirect-to-https"</span>
                    <span class="hljs-attr">FunctionAssociations:</span>
                        <span class="hljs-bullet">-</span> <span class="hljs-attr">EventType:</span> <span class="hljs-string">viewer-request</span>
                          <span class="hljs-attr">FunctionARN:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">RedirectFunction.FunctionMetadata.FunctionARN</span>
                <span class="hljs-attr">DefaultRootObject:</span> <span class="hljs-string">"index.html"</span>
                <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">true</span>
                <span class="hljs-attr">HttpVersion:</span> <span class="hljs-string">http2</span>
                <span class="hljs-attr">IPV6Enabled:</span> <span class="hljs-literal">true</span>
                <span class="hljs-attr">Origins:</span>
                    <span class="hljs-bullet">-</span> <span class="hljs-attr">CustomOriginConfig:</span>
                          <span class="hljs-attr">HTTPPort:</span> <span class="hljs-number">80</span>
                          <span class="hljs-attr">HTTPSPort:</span> <span class="hljs-number">443</span>
                          <span class="hljs-attr">OriginKeepaliveTimeout:</span> <span class="hljs-number">5</span>
                          <span class="hljs-comment"># keep http-only to avoid 504 errors after stack creation</span>
                          <span class="hljs-attr">OriginProtocolPolicy:</span> <span class="hljs-string">"http-only"</span>
                          <span class="hljs-attr">OriginReadTimeout:</span> <span class="hljs-number">30</span>
                          <span class="hljs-attr">OriginSSLProtocols:</span>
                              <span class="hljs-bullet">-</span> <span class="hljs-string">TLSv1</span>
                              <span class="hljs-bullet">-</span> <span class="hljs-string">TLSv1.1</span>
                              <span class="hljs-bullet">-</span> <span class="hljs-string">TLSv1.2</span>
                      <span class="hljs-comment">#Bucket website endpoint without http://</span>
                      <span class="hljs-attr">DomainName:</span> <span class="hljs-type">!Join</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-string">""</span>
                          <span class="hljs-bullet">-</span> <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">S3Bucket</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">".s3-website-"</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">AWS::Region</span>
                            <span class="hljs-bullet">-</span> <span class="hljs-string">".amazonaws.com"</span>
                      <span class="hljs-attr">Id:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"S3-${AWS::StackName}"</span>
                <span class="hljs-attr">PriceClass:</span> <span class="hljs-string">PriceClass_All</span>
                <span class="hljs-attr">ViewerCertificate:</span>
                    <span class="hljs-attr">AcmCertificateArn:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">CertificateManagerCertificate</span>
                    <span class="hljs-attr">MinimumProtocolVersion:</span> <span class="hljs-string">TLSv1.1_2016</span>
                    <span class="hljs-attr">SslSupportMethod:</span> <span class="hljs-string">sni-only</span>

    <span class="hljs-attr">RedirectFunction:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::CloudFront::Function</span>
        <span class="hljs-attr">Properties:</span>
            <span class="hljs-attr">AutoPublish:</span> <span class="hljs-literal">true</span>
            <span class="hljs-attr">Name:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"${AWS::StackName}-redirects"</span>
            <span class="hljs-comment"># add the config, even if optional, the stack creation will thrown InternalFailure error otherwise</span>
            <span class="hljs-attr">FunctionConfig:</span>
                <span class="hljs-attr">Comment:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"Redirect to ${DomainName}"</span>
                <span class="hljs-attr">Runtime:</span> <span class="hljs-string">cloudfront-js-1.0</span>
            <span class="hljs-attr">FunctionCode:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">|
                function handler(event) {
                    //
                    var request = event.request;
                    var host = request.headers.host.value;
</span>
                    <span class="hljs-string">if</span> <span class="hljs-string">(!host.startsWith("www."))</span> {
                        <span class="hljs-string">return</span> {
                            <span class="hljs-attr">statusCode:</span> <span class="hljs-number">301</span>,
                            <span class="hljs-attr">statusDescription:</span> <span class="hljs-string">"Permanently moved"</span>,
                            <span class="hljs-attr">headers:</span> {
                                <span class="hljs-attr">location:</span> { <span class="hljs-attr">value:</span> <span class="hljs-string">"https://www."</span> <span class="hljs-string">+</span> <span class="hljs-string">host</span> },
                            },
                        }<span class="hljs-string">;</span>
                    }
                    <span class="hljs-string">return</span> <span class="hljs-string">request;</span>
                <span class="hljs-string">}</span>

    <span class="hljs-attr">Route53RecordSetGroup:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::Route53::RecordSetGroup</span>
        <span class="hljs-attr">Properties:</span>
            <span class="hljs-comment"># keep the . suffix</span>
            <span class="hljs-attr">HostedZoneName:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"${DomainName}."</span>
            <span class="hljs-comment"># https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html#cfn-route53-aliastarget-hostedzoneid</span>
            <span class="hljs-attr">RecordSets:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">Name:</span> <span class="hljs-type">!Ref</span> <span class="hljs-string">DomainName</span>
                  <span class="hljs-attr">Type:</span> <span class="hljs-string">A</span>
                  <span class="hljs-attr">AliasTarget:</span>
                      <span class="hljs-attr">DNSName:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">CloudFrontDistribution.DomainName</span>
                      <span class="hljs-attr">EvaluateTargetHealth:</span> <span class="hljs-literal">false</span>
                      <span class="hljs-attr">HostedZoneId:</span> <span class="hljs-string">Z2FDTNDATAQYW2</span> <span class="hljs-comment"># leave hardcoded, don't confuse w/ !Ref HostedZoneId</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">Name:</span> <span class="hljs-type">!Sub</span> <span class="hljs-string">"www.${DomainName}"</span>
                  <span class="hljs-attr">Type:</span> <span class="hljs-string">A</span>
                  <span class="hljs-attr">AliasTarget:</span>
                      <span class="hljs-attr">DNSName:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">CloudFrontDistribution.DomainName</span>
                      <span class="hljs-attr">EvaluateTargetHealth:</span> <span class="hljs-literal">false</span>
                      <span class="hljs-attr">HostedZoneId:</span> <span class="hljs-string">Z2FDTNDATAQYW2</span> <span class="hljs-comment"># leave hardcoded, don't confuse w/ !Ref HostedZoneId</span>

<span class="hljs-attr">Outputs:</span>
    <span class="hljs-attr">WebsiteURL:</span>
        <span class="hljs-attr">Value:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">S3Bucket.WebsiteURL</span>
        <span class="hljs-attr">Description:</span> <span class="hljs-string">URL</span> <span class="hljs-string">for</span> <span class="hljs-string">website</span> <span class="hljs-string">hosted</span> <span class="hljs-string">on</span> <span class="hljs-string">S3</span>
    <span class="hljs-attr">CloudfrontDomainName:</span>
        <span class="hljs-attr">Value:</span> <span class="hljs-type">!GetAtt</span> <span class="hljs-string">CloudFrontDistribution.DomainName</span>
</code></pre>
<p>Truth be told, it looks much more complex than it is. I also created a GitHub repository with the template so you can get started quicker. <a target="_blank" href="https://github.com/stefanolaru/cloudformation-static-site">Check it out</a>.</p>
<h2 id="heading-credits">Credits</h2>
<ul>
<li><a target="_blank" href="https://nickolaskraus.org/articles/creating-a-static-website-using-cloudformation">Nickolas Kraus</a></li>
<li><a target="_blank" href="https://aws.amazon.com/about-aws/whats-new/2021/05/cloudfront-functions/">AWS CloudFormation Functions</a></li>
<li><a target="_blank" href="https://github.com/awslabs/aws-cloudformation-templates">AWS CloudFormation</a></li>
<li>Cover photo by <a target="_blank" href="https://unsplash.com/@sgabriel">Sebastien Gabriel</a></li>
</ul>
<h2 id="heading-about">About</h2>
<p><a target="_blank" href="https://www.monithor.co">Monithor</a> is a web service monitoring tool that helps you check websites or APIs' uptime and functionality and be alerted when they don't work as expected. It's built on AWS using the Serverless framework and it's open-source</p>
]]></content:encoded></item><item><title><![CDATA[Running cronjobs at scale in AWS]]></title><description><![CDATA[Monithor's core functionality is running jobs on a schedule. The service is built using the Serverless framework on top of AWS and allows users to schedule and run multiple monitoring jobs as frequently as once per minute.
There are various ways to r...]]></description><link>https://blog.stefanolaru.com/running-cronjobs-at-scale-in-aws</link><guid isPermaLink="true">https://blog.stefanolaru.com/running-cronjobs-at-scale-in-aws</guid><category><![CDATA[AWS]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[serverless]]></category><category><![CDATA[monitoring]]></category><category><![CDATA[DynamoDB]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Tue, 29 Mar 2022 04:18:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1648489902773/cXiFH1G9h.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://www.monithor.co">Monithor</a>'s core functionality is running jobs on a schedule. The service is built using the <a target="_blank" href="https://www.serverless.com">Serverless</a> framework on top of AWS and allows users to schedule and run multiple monitoring jobs as frequently as once per minute.</p>
<p>There are various ways to run cronjobs on a schedule in AWS, I analyzed the options that follow the serverless principles. Although all of them can perform pretty well in the case of a single-tenant system, we'll offer a cloud-hosted plan, which is a multi-tenant system that needs to scale. </p>
<p>Solution requirements:</p>
<ul>
<li><strong>precision</strong> - run as close to the scheduled time, preferably within seconds, an occasional execution delay up to a couple of seconds is acceptable</li>
<li><strong>scale</strong> - possibly tens of thousands of scheduled monitoring jobs, occasionally thousands to be executed around the same time</li>
<li><strong>costs</strong> - keep the AWS bill within reasonable limits</li>
</ul>
<p>Now, let's dig into the options that AWS offers out of the box:</p>
<p><strong>1. DynamoDB TTL + Streams</strong></p>
<p>Set a Lambda function to process the DynamoDB Streams and react to data modifications. Each monitor job would have a DynamoDB entry with the expiration time (TTL) equal to the scheduled execution time. Once the entry is deleted would trigger the monitor runner logic.</p>
<p><strong>Problem</strong>: The lack of precision. </p>
<p>As per <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/howitworks-ttl.html">DynamoDB docs</a> <code>TTL typically deletes expired items within 48 hours of expiration.</code> - this means a task could be triggered minutes if not hours after the scheduled time.</p>
<p><strong>2. EventBridge Rules (CloudWatch Events)</strong></p>
<p>Create an EventBridge Rule for every monitor and set the Lambda containing the runner logic as the rule target.</p>
<p><strong>Problem</strong>: The service quotas. </p>
<p>At the time of writing EventBridge Rules are limited to 300 per event bus and a maximum of 100 event buses per account. These are soft limits that will suffice for plenty of use cases, and can probably be increased on request. But the low initial limit is deterring.</p>
<p><strong>3. Step Functions</strong></p>
<p>We could make use of the wait state of the Step Functions and pause the workflow until the monitor's scheduled time of execution, it can be delayed even up to an entire year.</p>
<p><strong>Problem</strong>: Relatively high cost.</p>
<h2 id="heading-the-solution">The solution</h2>
<p>Given that none of the out-of-the-box solutions tick all the requirements we have to start playing with the building blocks that AWS has to offer.</p>
<p>DynamoDB already stores all our scheduled tasks, including the time when they should run, so we'll create two Lambda functions: a <em>Scheduler</em> and a <em>Runner</em> (or executor). The <em>Scheduler</em> will pull the jobs to be executed in the next interval from DynamoDB and publish them into a SQS queue that will be immediately consumed by <em>Runners</em>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648437212376/tOprXZ35P.png" alt="runner-flow.png" /></p>
<h3 id="heading-the-scheduler-function">The Scheduler Function</h3>
<p>The <em>Scheduler</em> function will be triggered by an EventBridge Rule once a minute.</p>
<p>The scope of the <em>Scheduler</em> function is to <code>Query</code> all scheduled monitoring jobs in the system (possibly thousands) and select those that are due for execution in the subsequent whole-minute interval. For example, the scheduler function that runs at 6:58 am will get all the monitors that need to run between 6:59:00 and 6:59:59 am. It would then enqueue those monitors into the SQS queue. </p>
<p>SQS allows us to set <code>DelaySeconds</code> parameter that will keep the message published to the queue invisible to consumers until the scheduled execution time.</p>
<p>The message payload contains all the info necessary for the <em>Runner</em> to execute the monitoring job, e.g. monitor config, project variables, and notification settings.</p>
<p>A drawback of this approach is that a monitoring job once sent to the queue to be executed can't be canceled but given the short time interval that's acceptable.</p>
<h4 id="heading-scaling-the-scheduler">Scaling the Scheduler</h4>
<p>The <em>Scheduler</em> function could become a bottleneck in case thousands of jobs have to be executed at the same time. There are intervals of the day that are prone to become hotspots, think the first minute of a day, or the first minute of every hour.</p>
<p>We added a dedicated global secondary index (RUNNER) that will help to <code>Query</code> which items are to be sent to the SQS queue. It uses the <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-sharding.html">write sharding</a> technique to better distribute writes across a partition key space and a scatter-gather query. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648527693111/qvcihPDMU.png" alt="GSI_Monithor_RUNNER.png" /></p>
<p>The tests show that it has a big impact on the DynamoDB query performance, so we can safely assume that we can get and send thousands, if not tens of thousands of monitoring jobs to the SQS queue in a matter of seconds.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1648485544782/pzMJw5W5-.png" alt="Screenshot 2022-03-24 at 14.04.17.png" /></p>
<p>If you want to dig deeper into Monithor's data model, I wrote an <a target="_blank" href="https://blog.stefanolaru.com/how-to-model-relational-data-in-dynamodb">extensive post</a> on the matter.</p>
<p>Here's how the Scheduler function code looks and how scatter-gather queries are done:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// get the monitors of this cron interval</span>
    <span class="hljs-keyword">const</span> promises = [],
        sqsMessages = [],
        sqsPromises = [],
        <span class="hljs-comment">// create shards array starting 0</span>
        shards = [...Array(process.env.SHARDS_COUNT).keys()];

    <span class="hljs-comment">// loop shards - scatter-gather query</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> idx <span class="hljs-keyword">in</span> shards) {
        promises.push(
            ddb
                .query({
                    <span class="hljs-attr">TableName</span>: process.env.DDB_TABLE,
                    <span class="hljs-attr">IndexName</span>: <span class="hljs-string">"RUNNER"</span>,
                    <span class="hljs-attr">KeyConditionExpression</span>:
                        <span class="hljs-string">"#RUNNER = :runner AND #SK BETWEEN :t1 AND :t2"</span>,
                    <span class="hljs-attr">FilterExpression</span>: <span class="hljs-string">"#status = :status"</span>,
                    <span class="hljs-attr">ExpressionAttributeNames</span>: {
                        <span class="hljs-string">"#RUNNER"</span>: <span class="hljs-string">"RUNNER"</span>,
                        <span class="hljs-string">"#SK"</span>: <span class="hljs-string">"nextrun_at"</span>,
                        <span class="hljs-string">"#status"</span>: <span class="hljs-string">"status"</span>,
                    },
                    <span class="hljs-attr">ExpressionAttributeValues</span>: AWS.DynamoDB.Converter.marshall({
                        <span class="hljs-string">":runner"</span>: <span class="hljs-string">"runner#"</span> + idx,
                        <span class="hljs-string">":status"</span>: <span class="hljs-string">"ENABLED"</span>, <span class="hljs-comment">// only the enabled monitors</span>
                        <span class="hljs-string">":t1"</span>: t1, <span class="hljs-comment">// next rounded minute start</span>
                        <span class="hljs-string">":t2"</span>: t1 + <span class="hljs-number">59</span>,
                    }),
                    <span class="hljs-attr">ScanIndexForward</span>: <span class="hljs-literal">false</span>,
                })
                .promise()
        );
    }

    <span class="hljs-comment">// await all promises to finish // parallel</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.allSettled(promises)
        .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
            res.forEach(<span class="hljs-function">(<span class="hljs-params">v</span>) =&gt;</span> {
                <span class="hljs-keyword">if</span> (v.value.Items) {
                    v.value.Items.forEach(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
                        <span class="hljs-comment">// unmarshal item</span>
                        item = AWS.DynamoDB.Converter.unmarshall(item);
                        <span class="hljs-comment">// create SQS mesage</span>
                        sqsMessages.push({
                            <span class="hljs-attr">Id</span>: item.PK.replace(<span class="hljs-string">"monitor#"</span>, <span class="hljs-string">""</span>),
                            <span class="hljs-attr">MessageBody</span>: <span class="hljs-built_in">JSON</span>.stringify(item),
                            <span class="hljs-comment">// delay the diff in seconds until scheduled time</span>
                            <span class="hljs-attr">DelaySeconds</span>:
                                item.nextrun_at -
                                <span class="hljs-built_in">Math</span>.floor(+<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>() / <span class="hljs-number">1000</span>),
                        });
                    });
                }
            });
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(err.message);
            <span class="hljs-keyword">return</span> [];
        });

    <span class="hljs-comment">// no monitors to enqueue, stop</span>
    <span class="hljs-keyword">if</span> (!sqsMessages.length) {
        <span class="hljs-keyword">return</span> [];
    }

    <span class="hljs-comment">// group sqs messages into chunks of 10 for batch sending</span>
    chunkArray(sqsMessages, <span class="hljs-number">10</span>).forEach(<span class="hljs-function">(<span class="hljs-params">chunk</span>) =&gt;</span> {
        sqsPromises.push(
            sqs
                .sendMessageBatch({
                    <span class="hljs-attr">Entries</span>: chunk,
                    <span class="hljs-attr">QueueUrl</span>: process.env.QUEUE,
                })
                .promise()
        );
    });

    <span class="hljs-comment">// wait for the sqs messages to be published</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.allSettled(sqsPromises)
        .then()
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(err);
        });
</code></pre>
<h2 id="heading-the-runner-function">The Runner function</h2>
<p>The <em>Runner</em> function will be triggered by the SQS queue, will receive up to 10 monitoring jobs and execute those in parallel. The execution of a runner consists in making the monitoring request and asserting the results and writing in the DynamoDB table a log entry and updating the monitor's next execution time and counters.</p>
<p>Depending on the notification settings, the assertion result is further published to an SNS topic that triggers a Lambda fan-out function that handles the notification sending (email, webhooks, etc). </p>
<h4 id="heading-scaling-the-runner">Scaling the Runner</h4>
<p>When monitoring jobs are available in the queue, Lambda reads up to 5 batches and sends them to our Runner function. If jobs are still available, Lambda increases the number of processes that are reading batches by up to 60 more instances per minute.</p>
<p>This will allow us to run thousands of monitoring jobs per minute.</p>
<h2 id="heading-resources">Resources</h2>
<ul>
<li>Yan Cui's post on <a target="_blank" href="https://theburningmonk.com/2019/05/using-cloudwatch-and-lambda-to-implement-ad-hoc-scheduling/">Using CloudWatch and Lambda to implement ad-hoc scheduling</a></li>
<li>Michael Bahr's post on <a target="_blank" href="https://acloudguru.com/blog/engineering/serverless-scheduler">Serverless Scheduler</a></li>
</ul>
<h2 id="heading-about">About</h2>
<p><a target="_blank" href="https://www.monithor.co">Monithor</a> is a web service monitoring tool that helps you check websites or APIs' uptime and functionality and be alerted when they don't work as expected. It's built on AWS using the Serverless framework and it's open-source.</p>
]]></content:encoded></item><item><title><![CDATA[How to model relational data in DynamoDB]]></title><description><![CDATA[I'm building monithor.co - an open-source web monitoring service built with Serverless on top of AWS. It's using DynamoDB as the database, a highly performant NoSQL database that's been increasingly popular for serverless applications over the past y...]]></description><link>https://blog.stefanolaru.com/how-to-model-relational-data-in-dynamodb</link><guid isPermaLink="true">https://blog.stefanolaru.com/how-to-model-relational-data-in-dynamodb</guid><category><![CDATA[AWS]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[serverless]]></category><category><![CDATA[monitoring]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Mon, 14 Mar 2022 10:18:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1647253159921/zTV-4doNv.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm building <a target="_blank" href="https://www.monithor.co">monithor.co</a> - an open-source web monitoring service built with Serverless on top of AWS. It's using DynamoDB as the database, a highly performant NoSQL database that's been increasingly popular for serverless applications over the past years.</p>
<p>After more than a decade of dealing mostly with relational databases, using DynamoDB requires looking at things from a different angle, unlike traditional SQL databases, it does not use table joins and other relational database constructs. However, we can model many common relational designs in a single DynamoDB table but the process is different.</p>
<h2 id="heading-dynamodb-table-basics">DynamoDB Table Basics</h2>
<p>Before getting deeper into things let's summarize the DynamoDB table basic concepts:</p>
<ul>
<li><strong>Partition (hash) key (PK)</strong> - defines in which partition the data is stored into</li>
<li><strong>Sort (range) key (SK)</strong> - defines the sorting of items in the partition, and it's optional. You can query it by condition expression (<strong>=</strong>, <strong>&gt;</strong>, <strong>&lt;</strong>, <strong>between</strong>, <strong>begins_with</strong>, <strong>contains</strong>, <strong>in</strong>) combined with PK</li>
<li><strong>Local secondary index (LSI)</strong> - allows defining a different sort key for PK. It can only be created when the table is created</li>
<li><strong>Global secondary index (GSI)</strong> - allows defining a new partition and/or sort key. GSI is the central part of most design patterns in a single table design. Both LSI and GSI are implemented as copying all the data to a new table-like structure</li>
<li><strong>Attributes</strong> - can be scalar (single value) or complex (list, maps, sets)</li>
</ul>
<p>There are three ways of reading data from DynamoDB:</p>
<ul>
<li><strong>GetItem</strong> - retrieves a single item from a table. This is the most efficient way to read a single item because it provides direct access to the physical location of the item.</li>
<li><strong>Query</strong> - retrieves all of the items that have a specific partition key. Within those items, you can apply a condition to the sort key and retrieve only a subset of the data. Query provides quick, efficient access to the partitions where the data is stored.</li>
<li><strong>Scan</strong> - retrieves all of the items in the specified table. (This operation should not be used with large tables, because it can consume large amounts of system resources.</li>
</ul>
<h2 id="heading-key-concepts-when-using-dynamodb">Key concepts when using DynamoDB</h2>
<h3 id="heading-know-your-access-patterns-in-advance">Know your access patterns in advance</h3>
<p>You shouldn't start designing the DynamoDB schema until you know the questions it will need to answer. Understanding the business problems and the application use cases upfront is essential. Write down all the access patterns, here's what the access pattern list looks like for Monithor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647235176434/cOmL59eYQ.png" alt="Screenshot 2022-03-14 at 07.19.04.png" /></p>
<p>Our table can grow quite big so we want to avoid <code>Scan</code> at all costs so we'll create our data model so we can extract what we need using <code>Query</code> and <code>GetItem</code>.</p>
<h3 id="heading-single-table-design">Single table design</h3>
<p>You should maintain as few tables as possible in a DynamoDB application, we'll go with a single table design.</p>
<p>Yes, having all the data in a single table might look confusing and hard to read, but it offers a rich set of flexible and efficient design patterns, better utilization of resources, and easier configuration, capacity planning, and monitoring.</p>
<p>When using a single table, use generic attributes for the primary key like PK (partition key), SK (sort key), GS1PK (first global secondary index partition key), GS1SK (first global secondary index sort key).</p>
<p>Also, identify the type of the item by prefixing keys with type. e.g. project#1234 (project = type, 1234 = id)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647235953765/EkLn7ILAi.png" alt="Screenshot 2022-03-14 at 07.32.00.png" /></p>
<p>The DynamoDB single-table design requires a different mindset than relational database table design, I recommend using NoSQL workbench for DynamoDB for data modeling and visualization. https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.html</p>
<h2 id="heading-monithors-table-design">Monithor's Table Design</h2>
<ul>
<li>a composite primary key (<strong>PK</strong> and <strong>SK</strong>)</li>
<li>a local secondary index with the sort key <strong>created_at</strong> - allows querying PK created within an interval of time</li>
<li>a local secondary index with the sort key <strong>nextrun_at</strong> - allows querying PK due to be executed within a time interval</li>
<li>a global secondary index (<strong>GSI</strong>) with the sort key set as table's sort key (<strong>SK</strong>)</li>
</ul>
<p>Here's the CloudFormation template for our DynamoDB single table.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Resources:</span>
    <span class="hljs-comment"># DynamoDB</span>
    <span class="hljs-attr">ddbTable:</span>
        <span class="hljs-attr">Type:</span> <span class="hljs-string">AWS::DynamoDB::Table</span>
        <span class="hljs-comment"># enable table retention on production use</span>
        <span class="hljs-comment"># DeletionPolicy: Retain</span>
        <span class="hljs-attr">Properties:</span>
            <span class="hljs-attr">TableName:</span> <span class="hljs-string">${self:custom.resourceName}</span>
            <span class="hljs-attr">AttributeDefinitions:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">PK</span>
                  <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">S</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">SK</span>
                  <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">S</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">created_at</span>
                  <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">N</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">nextrun_at</span>
                  <span class="hljs-attr">AttributeType:</span> <span class="hljs-string">N</span>
            <span class="hljs-attr">KeySchema:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">PK</span>
                  <span class="hljs-attr">KeyType:</span> <span class="hljs-string">HASH</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">SK</span>
                  <span class="hljs-attr">KeyType:</span> <span class="hljs-string">RANGE</span>
            <span class="hljs-attr">LocalSecondaryIndexes:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">IndexName:</span> <span class="hljs-string">pk_created</span>
                  <span class="hljs-attr">KeySchema:</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">PK</span>
                        <span class="hljs-attr">KeyType:</span> <span class="hljs-string">HASH</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">created_at</span>
                        <span class="hljs-attr">KeyType:</span> <span class="hljs-string">RANGE</span>
                  <span class="hljs-attr">Projection:</span>
                      <span class="hljs-attr">ProjectionType:</span> <span class="hljs-string">ALL</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">IndexName:</span> <span class="hljs-string">pk_run</span>
                  <span class="hljs-attr">KeySchema:</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">PK</span>
                        <span class="hljs-attr">KeyType:</span> <span class="hljs-string">HASH</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">nextrun_at</span>
                        <span class="hljs-attr">KeyType:</span> <span class="hljs-string">RANGE</span>
                  <span class="hljs-attr">Projection:</span>
                      <span class="hljs-attr">ProjectionType:</span> <span class="hljs-string">ALL</span>
            <span class="hljs-attr">GlobalSecondaryIndexes:</span>
                <span class="hljs-bullet">-</span> <span class="hljs-attr">IndexName:</span> <span class="hljs-string">GSI</span>
                  <span class="hljs-attr">KeySchema:</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">GSI</span>
                        <span class="hljs-attr">KeyType:</span> <span class="hljs-string">HASH</span>
                      <span class="hljs-bullet">-</span> <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">SK</span>
                        <span class="hljs-attr">KeyType:</span> <span class="hljs-string">RANGE</span>
                  <span class="hljs-attr">Projection:</span>
                      <span class="hljs-attr">ProjectionType:</span> <span class="hljs-string">ALL</span>

            <span class="hljs-comment"># use the On Demand pricing</span>
            <span class="hljs-attr">BillingMode:</span> <span class="hljs-string">PAY_PER_REQUEST</span>
            <span class="hljs-comment"># autodelete logs after specified time</span>
            <span class="hljs-attr">TimeToLiveSpecification:</span>
                <span class="hljs-attr">AttributeName:</span> <span class="hljs-string">expires_at</span>
                <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">TRUE</span>
</code></pre>
<h2 id="heading-relational-data">Relational data</h2>
<p>Modeling relational data in DynamoDB requires reusing or overloading keys and secondary indexes for different purposes. Overly-simplified, the data model for Monithor looks as in the diagram below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647245195024/HYE027e6L.png" alt="relations.drawio.png" /></p>
<ul>
<li><strong>Organization</strong> - the account that owns the projects &amp; cloud subscription, an organization can have multiple projects (one-to-many)</li>
<li><strong>Project</strong> - the monitors are organized into projects for easier monitoring and access control, a project can have multiple monitors (one-to-many), but a project also has and belongs to many users (many-to-many)</li>
<li><strong>Monitor</strong> - the monitor will create a log entry with the result of each execution, so a monitor can have multiple logs (one-to-many)</li>
</ul>
<h3 id="heading-one-to-many-relationship">One-to-Many relationship</h3>
<p>Let's take aside the relationship between projects and monitors, a project can have multiple monitors. In the UI we'll have to display all the monitors of a project, so we'll have to create an API endpoint that will query all monitors by project ID.</p>
<p>We rely on the <strong>GSI</strong> global secondary index, for each monitor entry we'll populate the GSI with the project ID prefixed with the entity type <strong>project#</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647247769818/s2ufJPbOg.png" alt="Screenshot 2022-03-14 at 10.49.01.png" /></p>
<p>Then, when we query on the <strong>GSI</strong> index the key conditions will be <code>GSI=project#1234 AND SK begins_with(monitor#)</code> which will return the information we're looking for.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647248061732/CjpdNjaPg.png" alt="Screenshot 2022-03-14 at 10.52.35.png" /></p>
<p>The query code would look like this:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> monitors = <span class="hljs-keyword">await</span> ddb
        .query({
            <span class="hljs-attr">TableName</span>: process.env.DDB_TABLE,
            <span class="hljs-attr">IndexName</span>: <span class="hljs-string">"GSI"</span>,
            <span class="hljs-attr">KeyConditionExpression</span>: <span class="hljs-string">"#GSI = :gsi AND begins_with(#SK, :prefix)"</span>,
            <span class="hljs-attr">ExpressionAttributeNames</span>: {
                <span class="hljs-string">"#GSI"</span>: <span class="hljs-string">"GSI"</span>,
                <span class="hljs-string">"#SK"</span>: <span class="hljs-string">"SK"</span>
            },
            <span class="hljs-attr">ExpressionAttributeValues</span>: AWS.DynamoDB.Converter.marshall({
                <span class="hljs-string">":gsi"</span>: <span class="hljs-string">"project#1234"</span>,
                <span class="hljs-string">":prefix"</span>: <span class="hljs-string">"monitor#"</span>
            }),
        });
</code></pre>
<p>And here's yet another example of a one-to-many relationship. We need to show a list of all projects within the organization dashboard UI. In this case, we'll fill the organization ID in the <strong>GSI</strong> of the project item.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647248886663/wJNC7KgW5.png" alt="Screenshot 2022-03-14 at 11.04.21.png" /></p>
<p>Querying the <strong>GSI</strong> with the key conditions <code>GSI=org#1234 AND SK begins_with(project#)</code> will return all the projects of that organization.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647248987554/OSHC5_ik0.png" alt="Screenshot 2022-03-14 at 11.03.43.png" /></p>
<h3 id="heading-many-to-many-relationship">Many-to-Many relationship</h3>
<p>The many-to-many relationships may seem a bit difficult at first, in a relational database we sort it out with a relationship table and two JOIN statements, but in DynamoDB this is not possible.</p>
<p>On Monithor, the core many-to-many relationship is between projects &amp; users. We want to have granular access control for every project, so a user can have different access levels on multiple projects. A project can have multiple users.</p>
<p>We rely on the table sort key to store the association between entities, we store a concatenated value of user ID &amp; project ID plus making use of <strong>GSI</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647249902796/vmxcM2sz6.png" alt="Screenshot 2022-03-14 at 11.16.52.png" /></p>
<p>In the app UI, once the user logs in we'll want to display a list of the projects they have access to. We know the user ID from the login session so we'll have the query the table index with the following key conditions <code>PK=user#1234 AND SK begins_with user#1234|project#</code></p>
<p>This will return all the project IDs the user#1234 has access to.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647250620467/7IvCp1kOC.png" alt="Screenshot 2022-03-14 at 11.35.53.png" /></p>
<p>The code will look as follows.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> monitors = <span class="hljs-keyword">await</span> ddb
        .query({
            <span class="hljs-attr">TableName</span>: process.env.DDB_TABLE,
            <span class="hljs-attr">KeyConditionExpression</span>: <span class="hljs-string">"#PK = :pk AND begins_with(#SK, :prefix)"</span>,
            <span class="hljs-attr">ExpressionAttributeNames</span>: {
                <span class="hljs-string">"#PK"</span>: <span class="hljs-string">"PK"</span>,
                <span class="hljs-string">"#SK"</span>: <span class="hljs-string">"SK"</span>
            },
            <span class="hljs-attr">ExpressionAttributeValues</span>: AWS.DynamoDB.Converter.marshall({
                <span class="hljs-string">":pk"</span>: <span class="hljs-string">"user#1234"</span>,
                <span class="hljs-string">":prefix"</span>: <span class="hljs-string">"user#1234|project#"</span>
            }),
        });
</code></pre>
<p>There's another UI screen where we need to display a list of users that have access to a project. This time we'll query the <strong>GSI*</strong> with the following key conditions <code>GSI=project#1234 AND SK begins_with user#</code></p>
<p>This will return all the user IDs that have access to project#1234.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1647251086849/BC3EBNQgu.png" alt="Screenshot 2022-03-14 at 11.40.00.png" /></p>
<h2 id="heading-resources">Resources</h2>
<p>If you want to get deeper into DynamoDB I can't recommend enough Alex DeBrie's <a target="_blank" href="https://www.dynamodbbook.com/">The DynamoDB Book</a>.</p>
<p>Also, have a look at the <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html">Best Practices for Designing and Architecting with DynamoDB</a>.</p>
<h2 id="heading-about">About</h2>
<p><a target="_blank" href="https://www.monithor.co">Monithor</a> is a web service monitoring tool that helps you check websites or APIs' uptime and functionality and be alerted when they don't work as expected. It's built on AWS using the Serverless framework and it's open-source.</p>
]]></content:encoded></item><item><title><![CDATA[Essential HTTP security headers]]></title><description><![CDATA[I received a security audit report from a client a few weeks ago, they were very worried as according to the report their website had plenty of high & medium severity vulnerabilities. 
I quickly realized that most of the reported issues are due to mi...]]></description><link>https://blog.stefanolaru.com/essential-http-security-headers</link><guid isPermaLink="true">https://blog.stefanolaru.com/essential-http-security-headers</guid><category><![CDATA[apache]]></category><category><![CDATA[Security]]></category><category><![CDATA[http]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[coding]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Sun, 29 Aug 2021 11:50:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1630237797856/WchetZroC.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I received a security audit report from a client a few weeks ago, they were very worried as according to the report their website had plenty of high &amp; medium severity vulnerabilities. </p>
<p>I quickly realized that most of the reported issues are due to missing or miss configured  HTTP headers. It was a good opportunity to review and summarize the security headers any website or web app should use.</p>
<p>The HTTP headers can be configured on the server to enhance the overall security of the website or web application, adding them won't literally make the site or app more secure, but will prevent the exploitation of potential vulnerabilities.</p>
<p>There are 2 types of HTTP headers we have to pay attention to:</p>
<h4 id="1-the-headers-that-protect-against-attacks">1) The headers that protect against attacks</h4>
<ul>
<li>X-Frame-Options</li>
<li>X-Content-Type-Options</li>
<li>X-XSS-Protection</li>
<li>Content-Security-Policy</li>
<li>Strict-Transport-Security</li>
<li>Access-Control-Allow-Origin</li>
</ul>
<h4 id="2-the-headers-that-leak-information">2) The headers that leak information</h4>
<ul>
<li>Server</li>
<li>X-Powered-By</li>
</ul>
<h2 id="1-the-headers-that-protect-against-attacks">1) The headers that protect against attacks</h2>
<p>These headers can be quite easily set using .htaccess, just make sure you have Apache's mod_headers enabled.</p>
<h3 id="x-frame-options">X-Frame-Options</h3>
<p>Protects against <a target="_blank" href="https://en.wikipedia.org/wiki/Clickjacking">clickjacking</a>.</p>
<p>If a malicious website can embed your site as an iframe it may allow attackers to invoke unintended actions by the user with clickjacking.</p>
<p>To prevent your website from being embedded in an iframe set the X-Frame-Options to DENY.</p>
<pre><code class="lang-shell">Header always append X-Frame-Options DENY
</code></pre>
<h3 id="x-content-type-options">X-Content-Type-Options</h3>
<p>Protects against <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#mime_sniffing">MIME sniffing</a>.</p>
<p>When a malicious HTML document is served from your domain (for example, if an image uploaded to a photo service contains valid HTML markup), some browsers will treat it as an active document and allow it to execute scripts in the context of the application. </p>
<p>This is known as content sniffing, the automatic detection of the content type, we need to tell browsers to not sniff content.</p>
<pre><code class="lang-shell">Header set X-Content-Type-Options nosniff
</code></pre>
<h3 id="x-xss-protection-deprecated">X-XSS-Protection (deprecated)</h3>
<p>Protects against <a target="_blank" href="https://en.wikipedia.org/wiki/Cross-site_scripting">cross-site scripting (XSS)</a>.</p>
<p>The X-XSS-Protection header has been <a target="_blank" href="https://owasp.org/www-project-secure-headers/#x-xss-protection">deprecated</a> by modern browsers and its use can introduce additional security issues on the client-side. (Better use Content-Security-Policy instead).</p>
<p>It is recommended to set the header as X-XSS-Protection: 0 in order to disable the XSS Auditor, and not allow it to take the default behavior of the browser handling the response.</p>
<pre><code class="lang-shell">Header set X-XSS-Protection 0
</code></pre>
<h3 id="content-security-policy-csp">Content-Security-Policy (CSP)</h3>
<p>Protects against <a target="_blank" href="https://en.wikipedia.org/wiki/Cross-site_scripting">cross-site scripting (XSS)</a>, <a target="_blank" href="https://en.wikipedia.org/wiki/Clickjacking">clickjacking</a> and HTML injection attacks.</p>
<p>The Content-Security-Policy provides an added layer to mitigate XSS attacks by restricting which scripts can be executed by the page. It's probably the most important header, but also the most tedious to configure.</p>
<p>Please note there's no one-size-fits-all CSP, you have to <a target="_blank" href="https://report-uri.com/home/generate">generate CSP policy</a> specific to the resources loaded by your website.</p>
<pre><code class="lang-shell">Header set Content-Security-Policy "default-src * data: 'unsafe-inline' 'unsafe-eval'"
</code></pre>
<h3 id="strict-transport-security-hsts">Strict-Transport-Security (HSTS)</h3>
<p>Protects against <a target="_blank" href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">man-in-the-middle</a> attacks.</p>
<p>HTTP Strict Transport Security instructs the browser to access the webserver over HTTPS only.</p>
<pre><code class="lang-shell">Header set Strict-Transport-Security "max-age=10886400; includeSubDomains; preload"
</code></pre>
<p>Learn more about HSTS <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security">here</a>.</p>
<h3 id="access-control-allow-origin">Access-Control-Allow-Origin</h3>
<p>Protects against <a target="_blank" href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">cross-site request forgery (CSRF)</a> attacks. </p>
<p>This is a CORS (Cross-Origin Resource Sharing) header. By default, modern browsers enforce the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy">same-origin policy</a> to prevent a web page from accessing cross-origin resources.</p>
<p>Many developers use a wildcard Access-Control-Allow-Origin header which explicitly opens up cross-origin requests from the attacker’s origin.</p>
<p>Don't set this header unless you need to allow AJAX requests from external origins, if so, be specific.</p>
<pre><code class="lang-shell"> Header set Access-Control-Allow-Origin "https://mysite.com"
</code></pre>
<h3 id="htaccess">.htaccess</h3>
<p>Enable the headers that protect against attacks using these directives:</p>
<pre><code class="lang-shell">&lt;IfModule mod_headers.c&gt;
    # Security Headers
    Header set X-XSS-Protection 0
    Header always append X-Frame-Options DENY
    Header set X-Content-Type-Options nosniff
    Header set Strict-Transport-Security "max-age=10886400; includeSubDomains; preload"
    #Header set Content-Security-Policy "default-src * data: 'unsafe-inline' 'unsafe-eval'"
&lt;/IfModule&gt;
</code></pre>
<h2 id="2-the-headers-that-leak-information">2) The headers that leak information</h2>
<p>To update these headers you'll likely need to get your hands dirty with server configuration files.</p>
<h3 id="server">Server</h3>
<p>This header contains information about the software used by the backend server that handles the request.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1630231845434/rP552xNB4.png" alt="Screenshot 2021-08-29 at 13.10.21.png" /></p>
<p>An overly-detailed <strong>Server</strong> value might make it easier for attackers to exploit known security holes.</p>
<p>Update the server settings to show the least info possible.</p>
<p>In httpd.conf update ServerTokens to "Prod"</p>
<blockquote>
<p>ServerTokens Prod</p>
</blockquote>
<p>Or, if you have WHM access, go to Service Configuration &gt; Apache Configuration &gt; Global Configuration and set Server Tokens to "Product Only".</p>
<h3 id="x-powered-by">X-Powered-By</h3>
<p>This header reveals information about the technology used in an application and can be a valuable hint for hackers who can exploit the security weaknesses of the technology, especially when using outdated software.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1630231612289/wzg5fT9a2.png" alt="Screenshot 2021-08-29 at 13.06.29.png" /></p>
<p>It's highly recommended to remove this header.</p>
<p>In php.ini set:</p>
<blockquote>
<p>expose_php = Off</p>
</blockquote>
<p>Or, if you have WHM access, go to Software &gt; MultiPHP INI Editor &gt; Editor Mode tab and look for the expose_php directive and set it to "Off".</p>
]]></content:encoded></item><item><title><![CDATA[PHP - the king is dead, long live the king!]]></title><description><![CDATA[There was a major shift in web development technologies in the past years, lots of things moved to the client-side, frontend frameworks such as Vue, React or Svelte are all the hype at the moment, Node.js & Python gained lots of popularity across bac...]]></description><link>https://blog.stefanolaru.com/php-the-king-is-dead-long-live-the-king</link><guid isPermaLink="true">https://blog.stefanolaru.com/php-the-king-is-dead-long-live-the-king</guid><category><![CDATA[PHP]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Server side rendering]]></category><category><![CDATA[programming languages]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Wed, 18 Aug 2021 05:16:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1629261980994/mihXacsqC.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There was a major shift in web development technologies in the past years, lots of things moved to the client-side, frontend frameworks such as Vue, React or Svelte are all the hype at the moment, Node.js &amp; Python gained lots of popularity across backend developers.</p>
<p>Taking a glance at Google trends we're seeing PHP interest is in a downwards spiral.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1629211821717/zvjeYUPAP.png" alt="Screenshot 2021-08-17 at 17.38.22.png" /></p>
<p>The quick adoption of client-side rendering, the new shiny tools, plus its bad reputation among developers, seem to have slowly pushed PHP in the back of people’s minds.</p>
<p>PHP is the most widely used server-side language and also the most hated, sometimes viscerally hated. It has been reportedly dying for at least a decade. </p>
<p>Yet, it's still here as the web backbone, powering at least <a target="_blank" href="https://w3techs.com/technologies/details/pl-php">7 out of 10 websites</a>.</p>
<h3 id="why-so-much-hate-on-a-scripting-language">Why so much hate on a scripting language?</h3>
<ul>
<li>it's technically inconsistent, has some design flaws</li>
<li>it's easy to learn, doesn't appeal to programming purists</li>
<li>it's very popular, but lacks the hype, there's nothing sexy in building with PHP</li>
<li>it’s cool to hate PHP</li>
</ul>
<p>A scripting language is just a tool that helps web developers solve problems. Getting fanatic about a language is as dumb as getting fanatic about a hammer.</p>
<blockquote>
<p>There are only two kinds of languages: the ones people complain about and the ones nobody uses.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1629264287676/y1FasBYN_.gif" alt="idk-hugh-laurie.gif" /></p>
<p>Yes, PHP might not be the best or the most modern scripting language, but it does the job for many and has come a long way since the dreaded version 3. </p>
<p>The difference in performance between <a target="_blank" href="https://kinsta.com/blog/php-benchmarks/">PHP5.6 and 7.4 is huge</a>, and considering the improvements and capabilities added under PHP8, it looks very alive and kicking, and better yet, <a target="_blank" href="https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/php-ruby.html">faster than Python or Ruby</a>.</p>
<p>There are a plethora of extremely popular tools built on PHP, such as <a target="_blank" href="https://wordpress.org/">WordPress</a> that literally dominates the CMS market, <a target="_blank" href="https://mediawiki.org">MediaWiki</a> the software behind Wikipedia, <a target="_blank" href="https://magento.com/">Magento</a> and <a target="_blank" href="https://www.opencart.com/">OpenCart</a>, two popular open-source e-commerce solutions.</p>
<p>There's also a vibrant and lovely community around the <a target="_blank" href="https://laravel.com">Laravel</a> framework, tons of modern web apps and APIs are built on Laravel (using PHP).</p>
<h3 id="is-php-dead">Is PHP dead?</h3>
<p>Absolutely not. </p>
<p>It seems like it's slowly losing its steam, but it's not going away anytime soon.</p>
<h3 id="is-it-still-worth-it-to-learn-php-as-a-new-developer">Is it still worth it to learn PHP as a new developer?</h3>
<p>Yes.</p>
<p>It has a lean learning curve, and the likelihood of having to deal with PHP projects is huge. Learn the fundamentals, if you like it, dig deeper, but don't stop at it.</p>
<p>Continuous learning is a must for software developers, when you stop learning you quickly become irrelevant.</p>
<h3 id="do-i-use-php">Do I use PHP?</h3>
<p>Yes, on a daily basis.</p>
<p>I have to admit that I didn't start any new project on PHP for quite some time. But that's just because the projects were more suitable for another technology, not that I have something with PHP. Looking forward to spinning up a Laravel 8 app in the near future.</p>
<p><em>What do you think about PHP's future?</em></p>
]]></content:encoded></item><item><title><![CDATA[On-the-fly image resizing with AWS Lambda, S3 & CloudFront]]></title><description><![CDATA[I've been working (on the side) lately on a redesign for the first SaaS I've built, a handmade marketplace for Romania's market. At the time of writing, the marketplace has close to 40k active products for sale, not including the drafts, products pen...]]></description><link>https://blog.stefanolaru.com/on-the-fly-image-resizing-with-aws-lambda-s3-and-cloudfront</link><guid isPermaLink="true">https://blog.stefanolaru.com/on-the-fly-image-resizing-with-aws-lambda-s3-and-cloudfront</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws lambda]]></category><category><![CDATA[image processing]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Tue, 10 Aug 2021 10:05:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1628589895276/98BnQcpWt.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I've been working (on the side) lately on a redesign for the first SaaS I've built, a <a target="_blank" href="https://crafty.ro">handmade marketplace</a> for Romania's market. At the time of writing, the marketplace has close to 40k active products for sale, not including the drafts, products pending approval, or the sold ones.</p>
<p>The new design features larger images, it will display thumbnails with different proportions and also makes use of "srcset" attribute to display responsive images.</p>
<p>The old logic works like this: the user uploads product photos &gt; the app server creates predefined image sizes using GD Library &gt; uploads into an S3 bucket. It does the job, but wastes server resources.</p>
<p>There are about 300k images to be resized, running a batch process to resize all the images into new dimensions will be time-consuming and quite costly. </p>
<p>Why process and store the assets that may not even be accessed by users? I'm going to process them on the fly (lazily), a resized image will only be created if a user requests that specific size.</p>
<h2 id="architecture-overview">Architecture overview</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628581246416/Y0Hx6YiLM.png" alt="aws-resizer-diagram.png" /></p>
<p>1) The user requests a resized image via Cloudfront (if the asset exists it will be returned and everything stops here)</p>
<p>2) Cloudfront requests the asset from the S3 bucket (using a website endpoint)</p>
<p>3) Because the asset doesn't exist the browser will be temporary redirected (307) to the API Gateway endpoint</p>
<p>4) API Gateway triggers the Lambda function</p>
<p>5) The Lambda function downloads the original image from the S3 bucket, resizes it, and uploads the resized image back into the bucket with the requested key</p>
<p>6) The API Gateway returns a permanent redirect (301) to the newly created asset Cloudfront URL</p>
<h2 id="the-setup">The Setup</h2>
<ul>
<li><a class="post-section-overview" href="#iam-user">IAM</a></li>
<li><a class="post-section-overview" href="#lambda-api-gateway">Lambda</a></li>
<li><a class="post-section-overview" href="#lambda-api-gateway">API Gateway</a></li>
<li><a class="post-section-overview" href="#s3">S3</a></li>
<li><a class="post-section-overview" href="#cloudfront">CloudFront</a></li>
</ul>
<h3 id="iam-user">IAM User</h3>
<p>We'll start by creating an IAM user that will allow our Lambda function to put the resized images on S3.</p>
<ol>
<li><p>Log in to the AWS account and go to the <a target="_blank" href="https://console.aws.amazon.com/iam/home#/home">Identity &amp; Access Management (IAM)</a></p>
</li>
<li><p>Go to Users and click "Add users" &gt; pick a username that'll be easier to remember &gt; check "Programmatic access" 
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628583327141/tR5eevQWFP.png" alt="Screenshot 2021-08-10 at 11.07.40.png" /></p>
</li>
<li><p>Click "Next" to go through to the Permissions page, click on "Attach existing policies" directly, and select <strong>AmazonS3FullAccess*</strong>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628583304582/-ysI-ok5q.png" alt="Screenshot 2021-08-10 at 11.07.58.png" /></p>
</li>
<li><p>View and copy the API Key &amp; Secret to a temporary place, we'll need them for the Lambda config.</p>
</li>
</ol>
<blockquote>
<p>Only use <strong>AmazonS3FullAccess</strong> policy for a quick demonstrative setup. In a production environment, I recommend following the <a target="_blank" href="https://en.wikipedia.org/wiki/Principle_of_least_privilege">principle of least privilege</a> and set more restrictive access. The Lambda function only requires getObject &amp; putObject permissions to our specific S3 bucket.</p>
</blockquote>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"VisualEditor0"</span>,
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Action"</span>: [
                <span class="hljs-string">"s3:PutObject"</span>,
                <span class="hljs-string">"s3:GetObject"</span>
            ],
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::crafty-products/*"</span>
        }
    ]
}
</code></pre>
<h3 id="lambda-and-api-gateway">Lambda &amp; API Gateway</h3>
<h3 id="using-serverless-framework">Using Serverless Framework</h3>
<p>I'm using the <a target="_blank" href="https://serverless.com">Serverless framework</a> to build and deploy my services, but it can also be done directly from the AWS Console.</p>
<p>If you're using Serverless this is the Lambda function config from the serverless.yml file:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># ...</span>
<span class="hljs-attr">functions:</span>
    <span class="hljs-comment"># on the fly image resizer function</span>
    <span class="hljs-attr">resizer:</span>
        <span class="hljs-attr">handler:</span> <span class="hljs-string">src/resizer.handler</span>
        <span class="hljs-attr">memorySize:</span> <span class="hljs-number">1024</span>
        <span class="hljs-attr">timeout:</span> <span class="hljs-number">15</span>
        <span class="hljs-attr">environment:</span>
            <span class="hljs-attr">AWS_KEY:</span> <span class="hljs-string">${self:custom.environment.AWS_KEY}</span>
            <span class="hljs-attr">AWS_SECRET:</span> <span class="hljs-string">${self:custom.environment.AWS_SECRET}</span>
            <span class="hljs-attr">BUCKET:</span> <span class="hljs-string">${self:custom.environment.PRODUCTS_BUCKET}</span>
            <span class="hljs-attr">CDN_URL:</span> <span class="hljs-string">${self:custom.environment.CDN_URL}</span>
        <span class="hljs-attr">events:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">http:</span>
                  <span class="hljs-attr">path:</span> <span class="hljs-string">/resizer</span>
                  <span class="hljs-attr">method:</span> <span class="hljs-string">GET</span>
<span class="hljs-comment"># ...</span>
</code></pre>
<p>When the service gets deployed with Serverless (<code>sls deploy</code>) it will create a Lambda function and the API Gateway that will listen for the GET requests and trigger the Lambda to resize the image. </p>
<p>I get into more details about Serverless configs &amp; deploy in another post: <a target="_blank" href="https://blog.stefanolaru.com/deploy-aws-serverless-apps-using-github-actions">how to automatically deploy Serverless applications on AWS using GitHub Actions</a>.</p>
<h3 id="using-aws-console">Using AWS Console</h3>
<p>If not using Serverless, everything can be done from the AWS console, though it's quite cumbersome, here are the steps:</p>
<h4 id="creating-the-lambda-function">Creating the Lambda function</h4>
<p>1) Go to the <a target="_blank" href="https://console.aws.amazon.com/lambda/home">Lambda Functions</a>
 and click "Create function"</p>
<p>2) Select "Author from Scratch", enter the function name and select the Node runtime (14 or newer), create the function.</p>
<p>3) <a target="_blank" href="https://github.com/stefanolaru/imageresizer/archive/refs/heads/main.zip">Download the zip</a> from GitHub, it contains the Lambda function</p>
<p>4) In the function's code tab, click "Upload from zip" in the top right and upload the function zip.
Note that the Sharp module is quite large so you won't be able to edit the function in the AWS Console, but you can add Sharp as a <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html">Lambda Layer</a> and maintain the ability to update your function from the AWS Console editor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628585532058/F2AOeRb0w.png" alt="Screenshot 2021-08-10 at 11.51.56.png" /></p>
<p>5) In the function's configuration tab we'll add the environment variables</p>
<ul>
<li>AWS_KEY (the IAM user access key)</li>
<li>AWS_SECRET (the IAM user access secret)</li>
<li>BUCKET (the S3 bucket containing the images)</li>
<li>CDN_URL (the CloudFront URL, we don't have it at this step, we'll set this up later)</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628586420299/EUErq4dwu.png" alt="Screenshot 2021-08-10 at 12.06.24.png" /></p>
<p>6) In the function's configuration, in the general configuration tab set the Memory to 1024MB and Timeout to at least 15 seconds to allocate enough resources &amp; time for Lambda to process larger images. Default settings (128MB/3s timeout) will likely fail.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628587427445/PWgtzxl2T.png" alt="Screenshot 2021-08-10 at 12.21.02.png" /></p>
<h4 id="the-lambda-function-code">The Lambda function code</h4>
<p>You can find the Lambda function code on <a target="_blank" href="https://github.com/stefanolaru/imageresizer">GitHub</a></p>
<p>It reads the source image from an S3 bucket as a stream, pipes it to Sharp, then writes the resized version as a stream back to the S3 bucket. It uses Node.js streams under the hood to prevent loading large amount of data in the Lambda function's memory and Sharp, a high-performance Node.js module for image processing (https://sharp.pixelplumbing.com/)</p>
<p>The function has a set of predefined image sizes, you will have to adjust to your needs or remove entirely. The goal was to prevent malicious users increase the AWS bill by generating images that won't ever be used.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//</span>
<span class="hljs-keyword">const</span> s3 = <span class="hljs-keyword">new</span> AWS.S3(),
    sizes =[
        <span class="hljs-string">"360x270"</span>, <span class="hljs-comment">// larger thumb restricted</span>
        <span class="hljs-string">"480h"</span>, <span class="hljs-comment">// height restricted</span>
        <span class="hljs-string">"640w"</span>,
        <span class="hljs-string">"1280w"</span>, <span class="hljs-comment">// largest web image</span>
    ];
<span class="hljs-comment">//</span>
</code></pre>
<p>The Lambda function will be triggered with a GET request like this:
<code>... ?key=produs_227613/360x270/img.AZ70KuRzw9.jpg</code></p>
<p>The <strong>key</strong> query string parameter contains the S3 path, the desired output size, and the original asset filename.</p>
<p>The sizes are of 3 types:</p>
<ul>
<li>width x height - i.e. <strong>360x270</strong> will generate a width &amp; height constricted thumbnail 360px wide and 270px high</li>
<li>width only - i.e. <strong>640w</strong> will generate an image 640px wide and the height will be automatically adjusted to maintain proportions</li>
<li>height only - i.e. <strong>640h</strong> will generate an image 640px high and the width will be automatically adjusted to maintain proportions</li>
</ul>
<h4 id="creating-the-api-gateway">Creating the API Gateway</h4>
<p>1) Go to the <a target="_blank" href="https://console.aws.amazon.com/apigateway/main/apis">API Gateway</a> and click "Create API"</p>
<p>2) We need an HTTP API, so click "Build" on that</p>
<p>3) For the integrations select Lambda and the name of the function we just created, then give the new API a name and click "Next"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628585839793/QGdMQ99Uf.png" alt="Screenshot 2021-08-10 at 11.57.04.png" /></p>
<p>4) For the routes we'll use a GET method and route "/resize" path to our Lambda integration, click "Next"
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628585952746/gcturdiJj.png" alt="Screenshot 2021-08-10 at 11.57.51.png" /></p>
<p>5) I'm using "prod" as a stage in the samples below, you can name the stage any way you want
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628586056024/exIZwotbK.png" alt="Screenshot 2021-08-10 at 11.59.49.png" /></p>
<p>6) Create the API and copy the Invoke URL somewhere, we'll need it for S3
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628586203389/nzrcFfbvy.png" alt="Screenshot 2021-08-10 at 12.02.40.png" /></p>
<p>The lambda function will now be triggered by Invoke URL + route, i.e.
<code>https://bm8w4yiv1b.execute-api.us-east-1.amazonaws.com/prod/resize</code></p>
<h3 id="s3">S3</h3>
<h4 id="1-enable-static-website-hosting">1) Enable "Static website hosting"</h4>
<p>Open AWS console go to S3 &gt; select the bucket hosting the images &gt; Properties &gt; Static website hosting.</p>
<p>Copy the bucket website endpoint somewhere, we'll need it for the CloudFront settings.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628579104949/yQPNe-KgA.png" alt="Screenshot 2021-08-10 at 10.03.58.png" /></p>
<h4 id="2-redirection-rules">2) Redirection rules</h4>
<p>This will temporarily redirect any S3 object that doesn't exist to the API Gateway URL passing the S3 key as a query string. Make sure you update the hostname with yours.</p>
<pre><code class="lang-json">[
    {
        <span class="hljs-attr">"Condition"</span>: {
            <span class="hljs-attr">"HttpErrorCodeReturnedEquals"</span>: <span class="hljs-string">"404"</span>,
            <span class="hljs-attr">"KeyPrefixEquals"</span>: <span class="hljs-string">""</span>
        },
        <span class="hljs-attr">"Redirect"</span>: {
            <span class="hljs-attr">"HostName"</span>: <span class="hljs-string">"k28ihy1li5.execute-api.us-east-1.amazonaws.com"</span>,
            <span class="hljs-attr">"HttpRedirectCode"</span>: <span class="hljs-string">"307"</span>,
            <span class="hljs-attr">"Protocol"</span>: <span class="hljs-string">"https"</span>,
            <span class="hljs-attr">"ReplaceKeyPrefixWith"</span>: <span class="hljs-string">"prod/resizer?key="</span>
        }
    }
]
</code></pre>
<p>In other words, when a visitor tries to load a product thumbnail with this URL:
<code>https://d1p41knyaaw8le.cloudfront.net/produs_227613/360x270/img.AZ70KuRzw9.jpg</code></p>
<p>If the thumbnail doesn't exist in S3, the browser will get redirected to the API gateway URL:
<code>https://k28ihy1li5.execute-api.us-east-1.amazonaws.com/prod/resizer?key=produs_227613/360x270/img.AZ70KuRzw9.jpg</code></p>
<h4 id="3-bucket-policy">3) Bucket Policy</h4>
<p>Open the bucket "Permissions" tab and allow GetObject action to any asset in your bucket (update the resource ARN accordingly) </p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"AddPerm"</span>,
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Principal"</span>: <span class="hljs-string">"*"</span>,
            <span class="hljs-attr">"Action"</span>: <span class="hljs-string">"s3:GetObject"</span>,
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::crafty-products/*"</span>
        }
    ]
}
</code></pre>
<h3 id="cloudfront">CloudFront</h3>
<p>Using a CDN to serve static assets is a no-brainer, so we'll create a new CloudFront distribution (if we don't have one already). There are 2 main things to pay attention to when setting CloudFront for lazy image processing. </p>
<h4 id="origin-domain">Origin Domain</h4>
<p>Enter the S3 <strong>bucket website endpoint</strong> URL, don't select the suggested S3 options from the dropdown list, should look something like this:</p>
<p><code>crafty-products.s3-website-eu-west-1.amazonaws.com</code></p>
<h4 id="behaviors">Behaviors</h4>
<p>Having CloudFront in front of S3 brings one extra challenge when requesting an image that is not found in S3: the not found response is cached, and the user will end up in a redirect loop.</p>
<p>We need to define a behavior for all the image types that will be resized, in this case, jpg, jpeg &amp; png.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1628581700969/eQlW5yJlH0.png" alt="Screenshot 2021-08-10 at 10.48.05.png" /></p>
<h3 id="lambda-config">Lambda config</h3>
<p>Once the CloudFront distribution is deployed, </p>
<ul>
<li><p>if you created the Lambda using AWS Console you have to go back into the Lambda config and update the CDN_URL environment variable</p>
</li>
<li><p>if you used Serverless update the CDN_URL variable and deploy the function again <code>sls deploy --f resizer</code></p>
</li>
</ul>
<p>That's all, it seems a bit complicated than it really is.</p>
<p>I wrote this post to illustrate the pattern and explain the mechanics of on-the-fly image processing. AWS has a <a target="_blank" href="https://aws.amazon.com/solutions/implementations/serverless-image-handler/">Serverless Image Handler</a> solution available which can be deployed via an AWS CloudFormation template.</p>
]]></content:encoded></item><item><title><![CDATA[Why I decided to open-source my SaaS]]></title><description><![CDATA[DiffHub makes it really easy to create automated tests for websites and APIs. 
It helps you avoid those embarrassing situations where a minor change creates a big mess in production, helps you spend less time doing boring stuff like manual testing, h...]]></description><link>https://blog.stefanolaru.com/why-i-decided-to-open-source-my-saas</link><guid isPermaLink="true">https://blog.stefanolaru.com/why-i-decided-to-open-source-my-saas</guid><category><![CDATA[SaaS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[Testing]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Tue, 03 Aug 2021 12:29:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1627993676581/pKhWlBf1k.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://diffhub.com">DiffHub</a> makes it really easy to create automated tests for websites and APIs. </p>
<p>It helps you avoid those embarrassing situations where a minor change creates a big mess in production, helps you spend less time doing boring stuff like manual testing, helps you create automated tests, visually, with no code.</p>
<hr />
<p>I started DiffHub as a hobby side project, to scratch my own itch.</p>
<p>It quickly became a challenge, I really wanted to build it, also wanted it to solve problems for my consulting clients, wanted to be scalable, and wanted to hone my Serverless &amp; AWS skills. <em>Why not turn it into a <a target="_blank" href="https://en.wikipedia.org/wiki/Software_as_a_service">SaaS</a> then?</em> </p>
<p>I put off some consulting projects and got head down, waking up at 4 am (without alarm) to code, and sacrificed many weekends.</p>
<p>I probably made all the possible mistakes that can send a product to the SaaS graveyard:</p>
<ul>
<li>started coding without a clearly defined scope</li>
<li>didn't talk to the users enough</li>
<li>didn't write documentation</li>
<li>didn't have pricing so didn't charge anyone</li>
<li>didn't do much marketing</li>
<li>thought if I build it they will come (luckily, a bunch did)</li>
<li>put things on autopilot</li>
</ul>
<p>I'm really happy I did them, it was a joy to build DiffHub, a very fulfilling experience as a developer. I learned a lot, and not only on the dev side. I also received tons of extremely helpful feedback and got connected with a lot of smart people.</p>
<p>The product didn't do bad, private beta went live in late 2019, just a few weeks before Covid, beta requests started pouring in, I onboarded a dozen users, received feedback (generally positive), implemented, and repeated the process weekly.</p>
<p>I kept a product-building journal from the first commit, reading it now makes me chuckle, building a SaaS app is like a roller coaster, here are a few excerpts.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627914178087/21CIqaCfc.png" alt="diffhub-notes.png" /></p>
<p>But 667 hours 😈 and  700+ commits later I was starting to run out of steam.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627913634221/3MmEUdYXi.png" alt="diffhub-stats1.png" /></p>
<p>The motivation slowly faded as the big development challenge was pretty much completed, "just" needed to fix bugs, do marketing, sales, and write docs. The not-so-sexy things for a developer.</p>
<p>My main business is web development &amp; consulting, in early March 2020 times got very turbulent due to Covid, lockdowns, a lot of uncertainty, being quite risk-averse I decided to put the side projects on autopilot and focus on my main biz until things settle.</p>
<p>Autopilot is not suitable for a SaaS business, especially when running a service in this field you need to grind, <em>ship, talk, talk, ship, talk ...</em> Putting things on autopilot may work for a couple of weeks, but it will kill the momentum.</p>
<p>We, indie hackers, have some degree of <a target="_blank" href="https://en.wikipedia.org/wiki/Shiny_object_syndrome">shiny new object syndrome</a>, better not kill the momentum, eventually procrastinating with other pet projects.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627972983443/jADBGqRSC.gif" alt="imback.gif" /></p>
<p>A few months later, the consulting business was thankfully doing very well. I started to think about how to push DiffHub forward, but realized this product is a bigger hat than I could wear by myself, in order to move the needle I had to either jump all-in in the SaaS roller-coaster, bring someone in and look for funding.</p>
<blockquote>
<p>Running a SaaS product as a (part-time) solo founder is almost a mission impossible. Very few nail it, hats off to them!</p>
</blockquote>
<p>I have to admit that I also thought about shutting it down.
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627915624850/v635i3Ts2.png" alt="Screenshot 2021-08-02 at 17.46.26.png" /></p>
<p>I let it sit for a few more weeks before making a decision, wrote down the reasons I built it, running it as a SaaS business wasn't one of them. Seems I got caught in the SaaS frenzy, again.</p>
<h3 id="so-why-make-it-open-source">So, why make it open source?</h3>
<p>I fancy the idea of building in public and give back to the community, <strong>is there a better way to do that than releasing and maintaining an open-source project</strong>?</p>
<p>By making it open-source:</p>
<ul>
<li>it continues to help people save time by automating the boring repetitive tasks</li>
<li>allows running automated tests in your AWS account for free (depends on usage, but it's easy to fit within AWS free tier, and pay-as-you-go afterwards)</li>
<li>eliminates the <a target="_blank" href="https://en.wikipedia.org/wiki/Bus_factor">bus factor</a> risk of a side-project ran by a solo founder</li>
<li>gives users a comfort level as they can have a full picture of how their sensitive data is stored and used</li>
<li>allows others to contribute to the project, improve &amp; expand it</li>
<li>allows me to give back to the community, share my learnings and learn from others</li>
</ul>
<p>I'm very aware that open-source software is aimed at enthusiasts, self-hosting requires some expertise, and it adds additional effort in maintaining, managing, monitoring.</p>
<h3 id="about-the-stack">About the stack</h3>
<p>The backend is built on the <a target="_blank" href="https://www.serverless.com">Serverless</a> framework and runs in AWS and makes use of services such as API Gateway, Lambda, DynamoDB, or SES. The tests are run using <a target="_blank" href="https://axios-http.com/">Axios</a> for the API testing and <a target="_blank" href="https://pptr.dev/">Puppeteer</a> for the browser tests. The test assertions are using <a target="_blank" href="https://jestjs.io/docs/expect">Jest</a>.</p>
<p>The front end is a <a target="_blank" href="https://vuejs.org/">Vue.js</a> app.</p>
<p>DiffHub wasn't built to run on-premise, so I'll have to adapt a few things, migrate the functionality from the SaaS app into the open-source repo.</p>
<p>The <a target="_blank" href="https://github.com/stefanolaru/diffhub/blob/main/serverless.yml">serverless.yml</a> contains a CloudFormation template that will deploy the entire service &amp; resources without headaches. You can have it up and running in less than a minute, it also features a powerful API that can be easily integrated into CI/CD pipelines.</p>
<p>The UI won't be included in the initial open-source release, tests can be managed via API for now.</p>
<p>The code will be available on <a target="_blank" href="https://github.com/stefanolaru/diffhub">GitHub</a>, I won't mind if you star the repo :)</p>
<p>If you want to stay in the loop with the progress please request private beta access on <a target="_blank" href="https://diffhub.com/">DiffHub</a> homepage or <a target="_blank" href="https://twitter.com/messages/compose?recipient_id=14350623">DM me on Twitter</a>.</p>
<p>Let's see where all this goes!</p>
]]></content:encoded></item><item><title><![CDATA[Automatically deploy Serverless applications on AWS using GitHub Actions]]></title><description><![CDATA[I use serverless a lot. 🤷🏽‍♂️
Most of the projects I have created lately are serverless, but I also use it to extend existing apps running in classic infrastructures and offload the resource-intensive tasks (i.e. image processing). 
I use AWS as th...]]></description><link>https://blog.stefanolaru.com/deploy-aws-serverless-apps-using-github-actions</link><guid isPermaLink="true">https://blog.stefanolaru.com/deploy-aws-serverless-apps-using-github-actions</guid><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[automation]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Tue, 27 Jul 2021 04:21:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1627297291575/0LgPtO1Fz.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I use serverless a lot. 🤷🏽‍♂️</p>
<p>Most of the projects I have created lately are serverless, but I also use it to extend existing apps running in classic infrastructures and offload the resource-intensive tasks (i.e. image processing). </p>
<p>I use AWS as the serverless provider and Node.js to code the Lambda functions.</p>
<p>In the early days, I did everything manually from the AWS console, but it's cumbersome, not very scalable, and configuration drifts can happen so subtly that it’s very easy to lose track.</p>
<blockquote>
<p>AWS best practices recommend deploying apps using Infrastructure-as-Code (IAC).</p>
</blockquote>
<p>Using a framework like <a target="_blank" href="https://www.serverless.com">Serverless</a>, in combination with version control, makes it really easy to track every change that's made to the app code &amp; infrastructure. </p>
<p>The Serverless framework enables us to build AWS CloudFormation templates to describe the resources to be deployed into the cloud and uses the AWS CLI to deploy them. It makes the deployment as easy as typing:</p>
<p><code>sls deploy</code></p>
<p>Ok, I have the app &amp; infra code already in a GitHub repository, why not automate the deployment process?</p>
<h2 id="serverless-overview">Serverless overview</h2>
<p>The foundation of a serverless application consists of a <strong>serverless.yml</strong> file and an <strong>env.yml</strong> file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627288368853/-i_4cQIdU.png" alt="Screenshot 2021-07-26 at 11.32.19.png" /></p>
<p>The <strong>serverless.yml</strong> is the heart of a serverless application, it describes the entire infrastructure, all the way from the programming language to resource access.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">service:</span> <span class="hljs-string">my-service-name</span>

<span class="hljs-attr">custom:</span>
    <span class="hljs-attr">defaultStage:</span> <span class="hljs-string">dev</span>
    <span class="hljs-attr">environment:</span> <span class="hljs-string">${file(env.yml):${self:provider.stage},</span> <span class="hljs-string">file(env.yml):dev}</span>

<span class="hljs-attr">provider:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">aws</span>
    <span class="hljs-attr">stage:</span> <span class="hljs-string">${opt:stage,</span> <span class="hljs-string">self:custom.defaultStage}</span>
    <span class="hljs-attr">runtime:</span> <span class="hljs-string">nodejs14.x</span>

<span class="hljs-attr">functions:</span>
    <span class="hljs-comment"># on the fly image resizer function</span>
    <span class="hljs-attr">resizer:</span>
        <span class="hljs-attr">handler:</span> <span class="hljs-string">src/resizer.handler</span>
        <span class="hljs-attr">memorySize:</span> <span class="hljs-number">1024</span>
        <span class="hljs-attr">timeout:</span> <span class="hljs-number">15</span>
        <span class="hljs-attr">environment:</span>
            <span class="hljs-attr">AWS_KEY:</span> <span class="hljs-string">${self:custom.environment.AWS_KEY}</span>
            <span class="hljs-attr">AWS_SECRET:</span> <span class="hljs-string">${self:custom.environment.AWS_SECRET}</span>
            <span class="hljs-attr">BUCKET:</span> <span class="hljs-string">${self:custom.environment.S3_BUCKET}</span>
            <span class="hljs-attr">CDN_URL:</span> <span class="hljs-string">${self:custom.environment.CDN_URL}</span>
        <span class="hljs-attr">events:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">http:</span>
                  <span class="hljs-attr">path:</span> <span class="hljs-string">/resizer</span>
                  <span class="hljs-attr">method:</span> <span class="hljs-string">GET</span>

<span class="hljs-comment">#optional plugins</span>
<span class="hljs-attr">plugins:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">serverless-offline</span>
</code></pre>
<p>The <strong>env.yml</strong> file stores the environment variables that are used in the application code (i.e. API keys)</p>
<blockquote>
<p>add env.yml to .gitignore, don't commit it to the repo. </p>
</blockquote>
<p>Some developers prefer to store the environment variables into JSON files instead of YML, the workflow is similar.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">prod:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">"crafty"</span>
    <span class="hljs-attr">AWS_KEY:</span> <span class="hljs-string">"MYAWSKEY"</span> <span class="hljs-comment"># add your AWS Key</span>
    <span class="hljs-attr">AWS_SECRET:</span> <span class="hljs-string">"MYAWSSECRET"</span> <span class="hljs-comment"># add your AWS Secret</span>
    <span class="hljs-attr">S3_BUCKET:</span> <span class="hljs-string">"crafty-products"</span>
    <span class="hljs-attr">CDN_URL:</span> <span class="hljs-string">"https://mycdnurl.com"</span>

<span class="hljs-attr">dev:</span>
    <span class="hljs-attr">name:</span> <span class="hljs-string">"DEV - crafty"</span>
    <span class="hljs-attr">AWS_KEY:</span> <span class="hljs-string">"MYAWSKEY"</span> <span class="hljs-comment"># add your AWS Key</span>
    <span class="hljs-attr">AWS_SECRET:</span> <span class="hljs-string">"MYAWSSECRET"</span> <span class="hljs-comment"># add your AWS Secret</span>
    <span class="hljs-attr">S3_BUCKET:</span> <span class="hljs-string">"crafty-products-dev"</span>
    <span class="hljs-attr">CDN_URL:</span> <span class="hljs-string">"https://mycdnurl.com"</span>
</code></pre>
<blockquote>
<p>Tip: I use the <a target="_blank" href="https://www.serverless.com/plugins/serverless-offline">Serverless Offline</a> plugin which emulates AWS Lambda and API Gateway on my local machine, allows me to test stuff locally, and speeds up the development cycles. </p>
</blockquote>
<p>We'll assume we have a basic Serverless app ready to be deployed so let's jump right into how to automate this workflow.</p>
<h2 id="create-the-iam-user">Create the IAM user</h2>
<p>There are two different IAM entities (users or roles) that you need to worry about when working with the Serverless framework:</p>
<ol>
<li><p>The IAM user used by the Framework to deploy our Serverless service (the Framework user)</p>
</li>
<li><p>The IAM role used by a Lambda function when it's executed (a function role).</p>
</li>
</ol>
<p>Below we'll create the IAM user that's needed by the framework to deploy our app &amp; service, the framework user.</p>
<ol>
<li><p>Log in to the AWS account and go to the <a target="_blank" href="https://console.aws.amazon.com/iam/home#/home">Identity &amp; Access Management (IAM)</a></p>
</li>
<li><p>Go to Users and click "Add users" &gt; pick a username that'll be easier to remember, I prefer to prefix users for GitHub actions  with <strong>github_sls</strong> &gt; check "Programmatic access" 
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627290380914/wfWIqb9wX.png" alt="Screenshot 2021-07-26 at 12.01.51.png" /></p>
</li>
<li><p>Click "Next" to go through to the Permissions page, click on "Attach existing policies" directly, and select <strong>AdministratorAccess</strong>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627290410727/acoM6x37m.png" alt="Screenshot 2021-07-26 at 12.02.22.png" /></p>
</li>
<li><p>View and copy the API Key &amp; Secret to a temporary place, we'll need them shortly.</p>
</li>
</ol>
<p>To set more restrictive access and follow the <a target="_blank" href="https://en.wikipedia.org/wiki/Principle_of_least_privilege">principle of least privilege</a>, I recommend checking this post of <a target="_blank" href="https://michael-timbs.medium.com/creating-a-serverless-deploy-user-with-aws-iam-b2053227534">Michael Timbs</a> and read more about the minimum IAM roles for deploying a serverless project.</p>
<h2 id="github-actions">GitHub Actions</h2>
<p>First, we have to store the AWS Key and AWS Secret that have AdministratorAccess and will be used by Serverless for deployment.</p>
<p>Go into GitHub repository Settings &gt; Secrets:</p>
<ol>
<li><p>add the newly created AWS API Key &amp; Secret as AWS_KEY_DEPLOY &amp; AWS_SECRET_DEPLOY</p>
</li>
<li><p>add the content of the <strong>env.yml</strong> file in the ENV secret</p>
</li>
</ol>
<p>Keep in mind that all the secrets stored in Github are encrypted, we won't able to view them after creation, just to update the value or remove them.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627290790676/AYhLK8o1m.png" alt="Screenshot 2021-07-26 at 12.10.38.png" /></p>
<p>Next, we have to create a YAML file to define the action workflow configuration, which can be done in two ways: </p>
<ol>
<li>from the GitHub repo, click Actions and create a workflow </li>
<li>define YAML files and store them in .github/workflows in your application root</li>
</ol>
<p>This is a sample YAML used to describe the GitHub action workflow that deploys the app on every commit in the <strong>main</strong> branch.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span> <span class="hljs-string">Production</span>
<span class="hljs-comment"># on main branch updates</span>
<span class="hljs-attr">on:</span>
    <span class="hljs-attr">push:</span>
        <span class="hljs-attr">branches:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>
<span class="hljs-attr">jobs:</span>
    <span class="hljs-attr">deploy:</span>
        <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
        <span class="hljs-attr">steps:</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v1</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v1</span>
              <span class="hljs-attr">with:</span>
                  <span class="hljs-attr">node-version:</span> <span class="hljs-string">"12.x"</span>
              <span class="hljs-comment">#install serverless</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">Serverless</span> <span class="hljs-string">Framework</span>
              <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span> <span class="hljs-string">-g</span> <span class="hljs-string">serverless</span>
              <span class="hljs-comment"># authentication</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Serverless</span> <span class="hljs-string">AWS</span> <span class="hljs-string">authentication</span>
              <span class="hljs-attr">run:</span> <span class="hljs-string">sls</span> <span class="hljs-string">config</span> <span class="hljs-string">credentials</span> <span class="hljs-string">--provider</span> <span class="hljs-string">aws</span> <span class="hljs-string">--key</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_KEY_DEPLOY</span> <span class="hljs-string">}}</span> <span class="hljs-string">--secret</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.AWS_SECRET_DEPLOY</span> <span class="hljs-string">}}</span>
              <span class="hljs-comment"># create ENV, use the shell `cat` command to create env.yml from secrets</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Create</span> <span class="hljs-string">env</span> <span class="hljs-string">file</span>
              <span class="hljs-attr">run:</span> <span class="hljs-string">|
                  cat &gt; env.yml &lt;&lt; EOF
                  ${{ secrets.ENV }}
                  EOF
</span>              <span class="hljs-comment">#install node modules</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Install</span> <span class="hljs-string">NPM</span> <span class="hljs-string">dependencies</span>
              <span class="hljs-attr">run:</span> <span class="hljs-string">npm</span> <span class="hljs-string">install</span>
              <span class="hljs-comment">#serverless deploy</span>
            <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Deploy</span>
              <span class="hljs-attr">run:</span> <span class="hljs-string">sls</span> <span class="hljs-string">deploy</span> <span class="hljs-string">-s</span> <span class="hljs-string">prod</span>
</code></pre>
<p>And this is how the workflow log looks like.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1627292534359/7knx-SEIH.png" alt="Screenshot 2021-07-26 at 12.41.25.png" /></p>
<p>Not that hard, isn't it?</p>
]]></content:encoded></item><item><title><![CDATA[How to make the honeypot field more effective]]></title><description><![CDATA[The honeypot field is an antispam technique, a bait form input used to lure spam bots, they'll "see" a field and fill it up with their junk. 
This input is hidden from normal users, humans will leave it empty because they won't see it, so it's easy t...]]></description><link>https://blog.stefanolaru.com/how-to-make-the-honeypot-field-more-effective</link><guid isPermaLink="true">https://blog.stefanolaru.com/how-to-make-the-honeypot-field-more-effective</guid><category><![CDATA[HTML]]></category><category><![CDATA[beginner]]></category><category><![CDATA[forms]]></category><category><![CDATA[best practices]]></category><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Tue, 20 Jul 2021 05:03:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1626757139634/6khwx9tR8.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The honeypot field is an antispam technique, a bait form input used to lure spam bots, they'll "see" a field and fill it up with their junk. </p>
<p>This input is hidden from normal users, humans will leave it empty because they won't see it, so it's easy to tell when a form submission is spam or not.</p>
<p>The honeypot will help you catch the majority of the dumb spam bots, however, bots get smarter and harder to trick, so it's not bullet-proof.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"POST"</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"form-handler.php"</span>&gt;</span>
    <span class="hljs-comment">&lt;!--  ... form fields ... --&gt;</span>
    <span class="hljs-comment">&lt;!-- this is the honeypot field --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"position: absolute; left: -9999px"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"website-url"</span>&gt;</span>Your website<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"website-url"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"url"</span> <span class="hljs-attr">tabindex</span>=<span class="hljs-string">"-1"</span> <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"nope"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!--  ... form fields ... --&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<h2 id="how-to-get-it-right">How to get it right?</h2>
<p>Adding a honeypot requires getting your hands dirty with some code, update the form HTML and backend code. Below are my tips on how to make the honeypot more effective and avoid silly mistakes.</p>
<h3 id="1-make-it-juicy">1. Make it juicy</h3>
<p>Make the field look as legit as possible so even the smarter bots will be tricked into filling it.</p>
<p>✅ set the input <strong>type="text"</strong> or <strong>type="email"</strong></p>
<p>✅ use a common name attribute like email, phone, address, i.e. <strong>name="url"</strong></p>
<h3 id="2-hide-it-well">2. Hide it well</h3>
<p>Bots get smarter every day, they can recognize honeypot patterns and we don't want that.</p>
<p>❌ don't use <strong>type="hidden"</strong></p>
<p>❌ don't use <strong>name="honeypot"</strong> or another name that would give a hint it's a trap</p>
<p>❌ don't use <strong>"display: none"</strong> - most bots won't fill the CSS hidden inputs</p>
<p>❌ don't use <strong>"hidden"</strong> or similar in the class name if you're using that instead of inline styles</p>
<p>✅ Wrap the input &amp; its label in a div tag and position it outside the viewport.</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- honeypot field --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"url-field-wrapper"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"website-url"</span>&gt;</span>Your website<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"website-url"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"url"</span> <span class="hljs-attr">tabindex</span>=<span class="hljs-string">"-1"</span> <span class="hljs-attr">autocomplete</span>=<span class="hljs-string">"nope"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-comment">&lt;!-- the rest of the form --&gt;</span>
</code></pre>
<pre><code class="lang-css"><span class="hljs-selector-class">.url-field-wrapper</span> {
    <span class="hljs-attribute">position</span>: absolute; 
    <span class="hljs-attribute">left</span>: -<span class="hljs-number">9999px</span>;
}
</code></pre>
<h3 id="3-dont-shoot-yourself-in-the-foot">3. Don't shoot yourself in the foot</h3>
<p>Browsers have functionalities that can be easily neglected and make legit users get caught by our spam bait input. Yep, false positives. There are 2 input attributes we have to set to avoid:</p>
<p>🚨 set <strong>tabindex="-1"</strong>, tech-savvy visitors will use the tab key to navigate through form fields, if the tabindex attribute is not set they'll be able to focus and accidentally fill in the honeypot</p>
<p>🚨 set <strong>autocomplete="nope"</strong>, avoid browsers' autocomplete feature to fill in the honeypot field, especially when using a common name attribute</p>
<h3 id="4-fake-happiness">4. Fake happiness</h3>
<p>If the backend validation fails because the honeypot was filled, don't throw an error, silently discard the submissions and show a success message. Make the spammer think their junk went through.</p>
<pre><code class="lang-php"><span class="hljs-comment">// require the honeypot field to be present but empty</span>
<span class="hljs-keyword">if</span>(!<span class="hljs-keyword">isset</span>($_REQUEST[<span class="hljs-string">'url'</span>]) || !<span class="hljs-keyword">empty</span>($_REQUEST[<span class="hljs-string">'url'</span>])) {
    <span class="hljs-comment">// hello spammer!</span>
    <span class="hljs-keyword">echo</span> <span class="hljs-string">'Thank you for submission!'</span>;
}
</code></pre>
<h2 id="additional-techniques">Additional techniques</h2>
<p>As mentioned previously, the honeypot field won't stop all the SPAM submissions but will help to catch at least half of it. If you struggle with spam submissions you'll want to use the honeypot in conjunction with other methods.</p>
<p>The general go-to solution is to add a CAPTCHA, I recommend against it, until it really hurts.</p>
<p>It's effective, but <strong>why punish our users because we have a problem with SPAM?</strong> It's our problem, not theirs, let's try to address it.</p>
<p>I've had excellent results filtering the form submissions through <a target="_blank" href="https://akismet.com">Akismet</a>, try that before CAPTCHA.</p>
<p><em>Shameless plug</em>: I run a service that will help you deal with form processing &amp; SPAM protection</p>
<p>👉 <a target="_blank" href="https://www.post2.io">POST2</a></p>
]]></content:encoded></item><item><title><![CDATA[echo 'Hello world'; 👋]]></title><description><![CDATA[Hi, I'm Stefan, a full-stack web developer.
TL;DR - I'm a web developer with more than a decade of experience, I love to build projects, break them and learn from the process. I'll share on this blog some of the most important learnings.
I built the ...]]></description><link>https://blog.stefanolaru.com/echo-hello-world</link><guid isPermaLink="true">https://blog.stefanolaru.com/echo-hello-world</guid><dc:creator><![CDATA[Stefan Olaru]]></dc:creator><pubDate>Mon, 19 Jul 2021 11:36:47 GMT</pubDate><content:encoded><![CDATA[<p>Hi, I'm Stefan, a full-stack web developer.</p>
<p><code>TL;DR - I'm a web developer with more than a decade of experience, I love to build projects, break them and learn from the process. I'll share on this blog some of the most important learnings.</code></p>
<p>I built the first website back in '98 but started to do it for a living in 2006.</p>
<p>I studied architecture, dabbled with 3d modeling, worked as a graphic designer, then as a web designer. </p>
<p>I can say I have a sharp eye for detail and I occasionally piss off my designer colleagues.</p>
<p>So, I'm a self-taught web developer with some design skills and no CS degree.</p>
<p>My web developer journey started when the web designer in me got kind of bored and wanted more control ...</p>
<p>2006</p>
<ul>
<li>built the first PHP site, using <a target="_blank" href="https://www.smarty.net/">Smarty templates</a></li>
</ul>
<p>2007</p>
<ul>
<li>proposed &amp; developed a custom e-commerce system for my employer (a car parts distributor) </li>
<li>built &amp; launched a local students community on <a target="_blank" href="https://www.joomla.org/">Joomla</a> - my first side-project</li>
</ul>
<p>2008</p>
<ul>
<li>the WordPress &amp; jQuery era, some <a target="_blank" href="https://www.oscommerce.com/">OSCommerce</a></li>
</ul>
<p>2009</p>
<ul>
<li>got obsessed with learning MySQL and writing optimized queries</li>
</ul>
<p>2010</p>
<ul>
<li>developed a few commercial WordPress themes</li>
<li>learned MVC &amp; OOP, started using CakePHP</li>
<li>launched a local marketplace <a target="_blank" href="https://www.crafty.ro/">Crafty</a> - my first SaaS</li>
<li>left my full-time job</li>
</ul>
<p>2011</p>
<ul>
<li>focused on frontend development, learned vanilla JS (I only knew jQuery), CSS preprocessors, started working with LESS</li>
<li>launched <a target="_blank" href="https://debugle.com">Debugle</a> - personal project</li>
</ul>
<p>2011 - 2017</p>
<ul>
<li>worked as an independent full-stack dev contractor for web agencies, built various SaaS products or websites, some for Fortune 500 companies</li>
<li>joined the project I currently work as Head of Development</li>
<li>launched a bunch of side-projects, killed'em all</li>
</ul>
<p>2018</p>
<ul>
<li>launched another side-project <a target="_blank" href="https://sift.email">sift.email</a> (Laravel)</li>
<li>learned NodeJS &amp; Serverless framework</li>
<li>started to work with VueJS</li>
</ul>
<p>2019 - now</p>
<ul>
<li>honed my NodeJS, AWS, Serverless skills</li>
<li>caught up with tools &amp; modern frontend development workflows</li>
<li>DevOps, CI&amp;CD</li>
<li>launched a handful of side-projects such as <a target="_blank" href="https://diffhub.com">DiffHub</a>, <a target="_blank" href="https://stratocart.com">Stratocart</a>, <a target="_blank" href="https://post2.io">POST2</a></li>
</ul>
<p>I love to learn something new every day and I often get excited about the creative solutions I find on complex problems, but more often ashamed by the dumb solutions I find for the simplest things.</p>
<p>I started this blog to:</p>
<ul>
<li>share my most important learnings with you</li>
<li>show that a senior dev still has things to learn and will check the docs for simplest things, and that's OK</li>
<li>get decent at writing &amp; improve my self-discipline (lack of consistency)</li>
</ul>
<p><strong>I'll publish a post on a dev-related topic every Tuesday until the end of 2021</strong> <em>(at least, hopefully)</em>.</p>
<hr />
<p>I have to admit I also have some <strong>no code</strong> problems, but won't blog about them.</p>
<p>I'm an outdoor freak (huh!), I enjoy hiking, cycling (sometimes I take myself too seriously), mushroom hunting, open-fire cooking, and DIY (mostly woodworking).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1626705394518/a0pjELbGf.jpeg" alt="mtb-stefan-olaru" /></p>
]]></content:encoded></item></channel></rss>