<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oluwademilade Oyekanmi</title>
    <description>The latest articles on DEV Community by Oluwademilade Oyekanmi (@msoluwademilade).</description>
    <link>https://dev.to/msoluwademilade</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F886672%2Fee04c93f-b8cf-40d2-93c0-66d01a236abe.png</url>
      <title>DEV Community: Oluwademilade Oyekanmi</title>
      <link>https://dev.to/msoluwademilade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/msoluwademilade"/>
    <language>en</language>
    <item>
      <title>Build Your First Serverless App on AWS — Step by Step</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Wed, 03 Dec 2025 08:03:17 +0000</pubDate>
      <link>https://dev.to/msoluwademilade/-4jf9</link>
      <guid>https://dev.to/msoluwademilade/-4jf9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; You've written some code before and you're curious about AWS, but you've never touched serverless. By the end of this guide you'll have a real, deployed API with zero servers to manage.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What is "serverless" really?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Serverless" doesn't mean there are no servers. It means &lt;em&gt;you&lt;/em&gt; don't have to think about them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traditionally, if you wanted your code to run on the internet, you had to rent a server, an EC2 instance on AWS, for example, keep it running 24/7, patch it, monitor it, and pay for it whether anyone was using your app or not. That's a lot of overhead.&lt;/p&gt;

&lt;p&gt;With serverless, you just upload your code. AWS handles provisioning, scaling, and maintenance. Your code runs only when it's triggered, when someone makes an API call, uploads a file, or inserts a record. When there's nothing to do, nothing runs, and nothing charges you.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Traditional server&lt;/th&gt;
&lt;th&gt;Serverless&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Always-on, always billed&lt;/td&gt;
&lt;td&gt;✅ Pay only when code runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You manage OS patches&lt;/td&gt;
&lt;td&gt;✅ AWS manages all of that&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual scaling&lt;/td&gt;
&lt;td&gt;✅ Auto-scales to thousands of requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complex setup&lt;/td&gt;
&lt;td&gt;✅ Deploy in minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;⚠️ Cold starts (small latency on first run)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The AWS Free Tier is generous.&lt;/strong&gt; Lambda gives you 1 million free requests per month and 400,000 GB-seconds of compute, forever, not just for 12 months. The project we're building today will cost you essentially nothing.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Our project: a URL shortener
&lt;/h2&gt;

&lt;p&gt;We'll build a minimal URL shortener. You send it a long URL, it gives you back a short code. You visit that code, it redirects you to the original URL.&lt;/p&gt;

&lt;p&gt;Simple enough to build in one sitting, but real enough to teach you how the pieces actually connect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Services we'll use:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;strong&gt;API Gateway&lt;/strong&gt; — receives HTTP requests from the internet&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;AWS Lambda&lt;/strong&gt; — runs your business logic&lt;/li&gt;
&lt;li&gt;🗄 &lt;strong&gt;DynamoDB&lt;/strong&gt; — stores the short code → URL mapping&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Here's how the three services talk to each other. Read top to bottom, that's the path a request takes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User's browser or curl
    ↓  POST /shorten  { url: "https://example.com/very/long/path" }

[ API Gateway ]
  → validates the request, forwards to Lambda
  → you never expose Lambda directly to the internet

    ↓ triggers

[ Lambda function — Node.js 20 ]
  → generates a short code (e.g. "abc123")
  → writes { code → url } to DynamoDB
  → returns the short URL to the user

    ↓ reads / writes

[ DynamoDB table — "urls" ]
  partition key: shortCode (String)
  attribute:     originalUrl (String)
  TTL:           expiresAt (auto-deletes old records = saves money)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why API Gateway in front of Lambda?&lt;/strong&gt; It lets you add rate limiting, authentication, and HTTPS without touching your function code. Lambda stays focused on business logic, the gateway handles the door.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step-by-step build
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 — Create the DynamoDB table
&lt;/h3&gt;

&lt;p&gt;Go to the AWS Console → &lt;strong&gt;DynamoDB&lt;/strong&gt; → &lt;strong&gt;Create table&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Table name:&lt;/strong&gt; &lt;code&gt;urls&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partition key:&lt;/strong&gt; &lt;code&gt;shortCode&lt;/code&gt; (String)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capacity mode:&lt;/strong&gt; On-demand (you pay per read/write, not per hour. Perfect for low or unpredictable traffic)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Leave everything else as default and hit Create.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2 — Create an IAM role for Lambda
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;IAM → Roles → Create role&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trusted entity:&lt;/strong&gt; Lambda&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attach managed policy:&lt;/strong&gt; &lt;code&gt;AWSLambdaBasicExecutionRole&lt;/code&gt; (gives Lambda permission to write logs to CloudWatch)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add an inline policy&lt;/strong&gt; for DynamoDB (see below, don't skip this step)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The IAM inline policy (least privilege):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only give Lambda permission to do &lt;em&gt;exactly&lt;/em&gt; what it needs on &lt;em&gt;exactly&lt;/em&gt; your table, nothing else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"dynamodb:PutItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"dynamodb:GetItem"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:dynamodb:us-east-1:YOUR_ACCOUNT_ID:table/urls"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Replace &lt;code&gt;YOUR_ACCOUNT_ID&lt;/code&gt; with your actual 12-digit AWS account ID, and &lt;code&gt;us-east-1&lt;/code&gt; with whichever region you created your table in. These must match exactly.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Step 3 — Create the Lambda function
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Lambda → Create function&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; Node.js 20.x&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution role:&lt;/strong&gt; Use the IAM role you created in Step 2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory:&lt;/strong&gt; 128 MB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout:&lt;/strong&gt; 5 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste the following code into the inline editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PutItemCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GetItemCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Reuse the client across invocations — keeps cold starts short&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;urls&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routeKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathParameters&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// POST /shorten — create a new short URL&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routeKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST /shorten&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// TTL: auto-delete this record after 90 days (cost saving)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;shortCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;N&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;short&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// GET /{code} — resolve a short URL&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routeKey&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET /{code}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetItemCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shortCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Location&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 4 — Create the API Gateway endpoint
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;API Gateway → Create API → HTTP API&lt;/strong&gt; (simpler and ~70% cheaper than REST API).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add two routes:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /shorten&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /{code}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Integration:&lt;/strong&gt; Point both routes to your Lambda function&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Deploy&lt;/strong&gt; to a stage called &lt;code&gt;prod&lt;/code&gt;
&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Copy the URL it gives you. That's your live API endpoint.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5 — Test it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Shorten a URL&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/shorten &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://example.com/a/very/long/path"}'&lt;/span&gt;

&lt;span class="c"&gt;# Response: {"short": "/abc123"}&lt;/span&gt;

&lt;span class="c"&gt;# Resolve it (your browser will follow the redirect automatically)&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com/abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see a redirect to your original URL, you just shipped a serverless app. No server. No Docker. No SSH session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security best practices
&lt;/h2&gt;

&lt;p&gt;Security in serverless is different from traditional server security, there's no OS to harden, no SSH port to close. The risks shift, so the defences shift too.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Principle of least privilege (IAM)
&lt;/h3&gt;

&lt;p&gt;Your Lambda function's IAM role is its identity. Only give it permission to do exactly what it needs. In the policy above, we granted only &lt;code&gt;PutItem&lt;/code&gt; and &lt;code&gt;GetItem&lt;/code&gt; on one specific table, not &lt;code&gt;DeleteItem&lt;/code&gt;, not &lt;code&gt;Scan&lt;/code&gt;, not access to any other table or service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; if your function doesn't need a permission, don't give it that permission. An attacker who compromises your function can only do what the function is allowed to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Never hardcode secrets in your code
&lt;/h3&gt;

&lt;p&gt;If your function needs an API key or a password, don't paste it in the source code. Use &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; or &lt;strong&gt;Parameter Store&lt;/strong&gt;. Your function fetches the secret at runtime using its IAM role, no secrets in code, no secrets in environment variables that show up in console screenshots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SSMClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;GetParameterCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-ssm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ssm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SSMClient&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;param&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ssm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetParameterCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/myapp/api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;WithDecryption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;// decrypts KMS-encrypted values automatically&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;param&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Parameter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Validate all input
&lt;/h3&gt;

&lt;p&gt;Your Lambda is reachable from the internet via API Gateway. Anyone can send it anything. Always validate the shape and content of incoming data before using it, check that the URL is actually a URL, that required fields exist, that strings aren't suspiciously long.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Enable CloudWatch logs and set a retention period
&lt;/h3&gt;

&lt;p&gt;Lambda logs every invocation to CloudWatch automatically. But by default, logs are kept forever (and charged per GB stored). Set a retention period: 7 or 30 days is usually enough.&lt;/p&gt;

&lt;p&gt;In the AWS Console: &lt;strong&gt;CloudWatch → Log groups → your function's log group → Actions → Edit retention setting&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping costs near zero
&lt;/h2&gt;

&lt;p&gt;Here's what this project actually costs at light traffic (~10,000 requests/month):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;What you're billed for&lt;/th&gt;
&lt;th&gt;Estimated cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;td&gt;10k requests × 128 MB × ~100ms&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.00&lt;/strong&gt; (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Gateway (HTTP API)&lt;/td&gt;
&lt;td&gt;$1.00 per million requests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.01&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB (on-demand)&lt;/td&gt;
&lt;td&gt;~$0.00025 per read/write unit&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.01&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.02 / month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Cost-saving choices we made and why
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;HTTP API over REST API in API Gateway&lt;/strong&gt;&lt;br&gt;
HTTP API costs 70% less than the older REST API and is more than sufficient for most use cases. REST API has additional features (request transformation, WAF integration) but you probably don't need them yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DynamoDB on-demand capacity&lt;/strong&gt;&lt;br&gt;
With provisioned capacity, you pay for throughput 24/7 even when idle. On-demand billing means you pay per actual read and write, perfect for apps with unpredictable or low traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DynamoDB TTL for automatic cleanup&lt;/strong&gt;&lt;br&gt;
We set an &lt;code&gt;expiresAt&lt;/code&gt; timestamp in the Lambda function. DynamoDB reads this attribute and automatically deletes old records for free, no scheduled jobs, no manual cleanup, no growing table size eating into your storage bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;128 MB Lambda memory&lt;/strong&gt;&lt;br&gt;
Lambda charges for memory × duration. 128 MB is the minimum, and for a simple DynamoDB read/write, it's more than enough. Only increase memory if your function is slow; more memory also means more CPU allocated to your function.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to build next
&lt;/h2&gt;

&lt;p&gt;You now have a working, secured, cost-optimised serverless app. Here are natural next steps, each one introduces a new piece of the AWS serverless ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add a frontend with S3 + CloudFront&lt;/strong&gt;&lt;br&gt;
Upload a simple HTML form to an S3 bucket and serve it through CloudFront (AWS's CDN). Now your users have a UI instead of a curl command. S3 static hosting costs cents per GB stored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add authentication with Cognito&lt;/strong&gt;&lt;br&gt;
Require users to sign in before shortening URLs. API Gateway can validate Cognito JWTs automatically, your Lambda function never sees unauthenticated requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Move to infrastructure-as-code with AWS CDK or SAM&lt;/strong&gt;&lt;br&gt;
Right now you've clicked through the console. The next level is writing your infrastructure as code so you can deploy it repeatably, version it in git, and tear everything down cleanly when you're done experimenting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Here's what we built, and why each decision was made:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; keeps Lambda off the public internet and gives you rate limiting and HTTPS for free&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; runs only when triggered, no idle billing, no patching, no servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; on-demand scales with your traffic and charges nothing when nobody's using your app&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Least-privilege IAM&lt;/strong&gt; means a compromised function can only do what the function is supposed to do&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL on DynamoDB records&lt;/strong&gt; means old data cleans itself up automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the serverless model in its purest form. No servers. No patching. No idle billing. One Lambda function, two AWS services, and a handful of IAM rules.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this helpful? Follow along. The next post covers adding a frontend with S3 and CloudFront.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambda</category>
      <category>beginners</category>
    </item>
    <item>
      <title>From Flask App to Production on AWS EKS: A Complete CI/CD Walkthrough</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Mon, 28 Apr 2025 11:23:00 +0000</pubDate>
      <link>https://dev.to/msoluwademilade/-1n6</link>
      <guid>https://dev.to/msoluwademilade/-1n6</guid>
      <description>&lt;p&gt;I recently deployed a Flask REST API to Amazon EKS with a full CI/CD pipeline, PostgreSQL on Kubernetes, and Prometheus + Grafana monitoring — all wired together with GitHub Actions. This post is a step-by-step walkthrough of exactly how I did it.&lt;/p&gt;

&lt;p&gt;The app itself is a Titanic passenger API (a classic dataset) but the architecture is production-grade: containerized app, ECR image registry, EKS cluster, persistent storage, and alerting to Slack. Let me walk you through it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;Here's the full stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App:&lt;/strong&gt; Python Flask REST API with PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containerization:&lt;/strong&gt; Docker + Amazon ECR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration:&lt;/strong&gt; Amazon EKS (Kubernetes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD:&lt;/strong&gt; GitHub Actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring:&lt;/strong&gt; Prometheus + Grafana via Helm (kube-prometheus-stack)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting:&lt;/strong&gt; Slack via Alertmanager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source code is at: &lt;a href="https://github.com/MsOluwademilade/titanic-test-app" rel="noopener noreferrer"&gt;github.com/MsOluwademilade/titanic-test-app&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Application
&lt;/h2&gt;

&lt;p&gt;The app is a CRUD REST API built with Flask and SQLAlchemy. It manages a &lt;code&gt;people&lt;/code&gt; table seeded with Titanic passenger data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Endpoints:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /people&lt;/code&gt; — list all passengers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /people/&amp;lt;uuid&amp;gt;&lt;/code&gt; — get one passenger&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /people&lt;/code&gt; — add a passenger&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PUT /people/&amp;lt;uuid&amp;gt;&lt;/code&gt; — update a passenger&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DELETE /people/&amp;lt;uuid&amp;gt;&lt;/code&gt; — remove a passenger&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data model (&lt;code&gt;src/models/person.py&lt;/code&gt;) maps to a PostgreSQL table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;people&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;survived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;passengerClass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;sex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;siblingsOrSpousesAboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parentsOrChildrenAboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app factory pattern in &lt;code&gt;src/app.py&lt;/code&gt; supports both &lt;code&gt;development&lt;/code&gt; and &lt;code&gt;production&lt;/code&gt; configs, pulling &lt;code&gt;DATABASE_URL&lt;/code&gt; from environment variables — which makes it Kubernetes-friendly from day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Dockerizing the App
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; uses a &lt;code&gt;python:3.12-slim&lt;/code&gt; base image to keep things lean.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    build-essential &lt;span class="se"&gt;\
&lt;/span&gt;    libpq-dev &lt;span class="se"&gt;\
&lt;/span&gt;    python3-dev &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nv"&gt;Flask&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2.2.5 &lt;span class="nv"&gt;SQLAlchemy&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.4.49 &lt;span class="nv"&gt;setuptools&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;70.0.0 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    pip uninstall &lt;span class="nt"&gt;-y&lt;/span&gt; psycopg2 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; psycopg2-binary&lt;span class="o"&gt;==&lt;/span&gt;2.9.9

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DATABASE_URL=postgresql+psycopg2://user:password@db:5432/postgres&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "run.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We explicitly uninstall &lt;code&gt;psycopg2&lt;/code&gt; and reinstall &lt;code&gt;psycopg2-binary&lt;/code&gt; — this avoids compilation errors in slim images that don't have full build toolchains.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;DATABASE_URL&lt;/code&gt; env var set in the Dockerfile is just a default. In Kubernetes, we'll override it per-deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Testing locally with Docker Compose:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before pushing to EKS, test the full stack locally with &lt;code&gt;compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5000:5000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+psycopg2://user:password@db:5432/postgres&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;condition: service_healthy&lt;/code&gt; on the db dependency is important — it ensures Postgres is actually ready to accept connections before Flask tries to connect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit &lt;code&gt;http://localhost:5000/people&lt;/code&gt; to confirm it's working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Push to Amazon ECR
&lt;/h2&gt;

&lt;p&gt;Before setting up the full pipeline, create your ECR repository manually (you only do this once):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr create-repository &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-name&lt;/span&gt; titanic-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;your-region&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the repository URI, you'll need it as a GitHub secret.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Provision the EKS Cluster
&lt;/h2&gt;

&lt;p&gt;Create your cluster with &lt;code&gt;eksctl&lt;/code&gt; (the easiest way to get started):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eksctl create cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; titanic-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;your-region&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nodegroup-name&lt;/span&gt; titanic-nodes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-type&lt;/span&gt; t3.medium &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--nodes&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provisions the control plane, worker nodes, and configures your local &lt;code&gt;kubeconfig&lt;/code&gt; automatically. The CI/CD pipeline will later call &lt;code&gt;aws eks update-kubeconfig&lt;/code&gt; to do the same in GitHub Actions.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why t3.medium?&lt;/strong&gt; The kube-prometheus-stack (Prometheus + Grafana + Alertmanager) is resource-hungry. t3.small nodes will struggle. Budget for at least t3.medium if you're running monitoring.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4: The Kubernetes Manifests
&lt;/h2&gt;

&lt;p&gt;All Kubernetes configs live in the &lt;code&gt;k8s/&lt;/code&gt; folder. Here's what each file does:&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL Setup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;k8s/postgres-configmap.yaml&lt;/code&gt;&lt;/strong&gt; — Holds the SQL init script that creates the &lt;code&gt;people&lt;/code&gt; table and seeds initial data. Kubernetes mounts this as a volume into the Postgres container at &lt;code&gt;/docker-entrypoint-initdb.d/&lt;/code&gt;, which Postgres automatically runs on first start.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ConfigMap&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;titanic-sql&lt;/span&gt;
&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;init.sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;CREATE TABLE IF NOT EXISTS people (&lt;/span&gt;
      &lt;span class="s"&gt;uuid VARCHAR(255) PRIMARY KEY DEFAULT gen_random_uuid()::text,&lt;/span&gt;
      &lt;span class="s"&gt;survived INTEGER NOT NULL,&lt;/span&gt;
      &lt;span class="s"&gt;...&lt;/span&gt;
    &lt;span class="s"&gt;);&lt;/span&gt;
    &lt;span class="s"&gt;INSERT INTO people (...) VALUES (...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;k8s/postgres-deployment.yaml&lt;/code&gt;&lt;/strong&gt; — Deploys Postgres with a PersistentVolumeClaim for durable storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-pvc&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;accessModes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1Gi&lt;/span&gt;
  &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gp2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;storageClassName: gp2&lt;/code&gt; tells EKS to provision an AWS EBS volume. This is what survives pod restarts and gives you actual persistence — without it, every time Postgres restarts you'd lose your data.&lt;/p&gt;

&lt;p&gt;The deployment mounts both the PVC (for data) and the ConfigMap (for init scripts):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres-data&lt;/span&gt;
    &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/pgdata&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;init-script&lt;/span&gt;
    &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/docker-entrypoint-initdb.d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;k8s/postgres-service.yaml&lt;/code&gt;&lt;/strong&gt; — Exposes Postgres internally as a &lt;code&gt;ClusterIP&lt;/code&gt; service on port 5432. The Flask app reaches it via the service name &lt;code&gt;postgres&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flask App
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;k8s/app-deployment.yaml&lt;/code&gt;&lt;/strong&gt; — Deploys the Flask app. Notice the &lt;code&gt;IMAGE_URI_PLACEHOLDER&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;titanic-app&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IMAGE_URI_PLACEHOLDER&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+psycopg2://user:password@postgres:5432/postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CI/CD pipeline replaces &lt;code&gt;IMAGE_URI_PLACEHOLDER&lt;/code&gt; with the actual ECR image URI using &lt;code&gt;sed&lt;/code&gt; before applying the manifest. This keeps the manifest clean in version control while allowing dynamic image URIs in the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;k8s/app-service.yaml&lt;/code&gt;&lt;/strong&gt; — Exposes the Flask app as a &lt;code&gt;LoadBalancer&lt;/code&gt; service. EKS provisions an AWS ELB automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LoadBalancer&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traffic hits port 80 on the load balancer and gets forwarded to port 5000 on the Flask container.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Monitoring with Prometheus &amp;amp; Grafana
&lt;/h2&gt;

&lt;p&gt;The monitoring stack lives in &lt;code&gt;monitoring/&lt;/code&gt;. Rather than writing raw Kubernetes manifests for Prometheus, we use the &lt;code&gt;kube-prometheus-stack&lt;/code&gt; Helm chart — it bundles Prometheus, Grafana, and Alertmanager and pre-configures them to scrape Kubernetes metrics out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;monitoring/prometheus-pvcs.yaml&lt;/code&gt;&lt;/strong&gt; — Creates PVCs for Prometheus and Alertmanager data persistence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Prometheus: 8Gi on gp2 EBS&lt;/span&gt;
&lt;span class="c1"&gt;# Alertmanager: 2Gi on gp2 EBS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;monitoring/prometheus-values.yaml&lt;/code&gt;&lt;/strong&gt; — This is where the interesting configuration lives.&lt;/p&gt;

&lt;p&gt;Grafana gets a LoadBalancer service so it's accessible externally, and persistence so dashboards survive pod restarts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;grafana&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;adminPassword&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-password"&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LoadBalancer&lt;/span&gt;
  &lt;span class="na"&gt;persistence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;storageClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gp2&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alertmanager is configured to route all alerts to Slack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;alertmanager&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;receiver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;slack-notifications'&lt;/span&gt;
    &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;slack-notifications'&lt;/span&gt;
        &lt;span class="na"&gt;slack_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;api_url_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/etc/alertmanager/secrets/alertmanager-slack-webhook/url&lt;/span&gt;
            &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#all-titanic-alert'&lt;/span&gt;
            &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;🚨&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Kubernetes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Alert&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;.CommonLabels.alertname&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}'&lt;/span&gt;
            &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;{{ range .Alerts }}&lt;/span&gt;
              &lt;span class="s"&gt;*Alert:* {{ .Labels.alertname }}&lt;/span&gt;
              &lt;span class="s"&gt;*Severity:* {{ .Labels.severity }}&lt;/span&gt;
              &lt;span class="s"&gt;*Summary:* {{ .Annotations.summary }}&lt;/span&gt;
              &lt;span class="s"&gt;{{ end }}&lt;/span&gt;
            &lt;span class="na"&gt;send_resolved&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Slack webhook URL is mounted from a Kubernetes secret (not hardcoded) — the &lt;code&gt;api_url_file&lt;/code&gt; path points to a mounted secret volume.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;defaultRules&lt;/code&gt; section enables pre-built alert rules covering etcd, Kubernetes API server, node resources, pod health, storage, and more — all without writing a single PromQL rule yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: The CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;This is where everything ties together. The GitHub Actions workflow in &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt; triggers on every push to &lt;code&gt;main&lt;/code&gt; and handles the full deploy sequence.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. AWS Authentication
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
    &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_REGION }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS credentials are stored as GitHub repository secrets — never hardcoded.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Build &amp;amp; Push to ECR
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Amazon ECR&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;login-ecr&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/amazon-ecr-login@v2&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build, tag, and push image to ECR&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ECR_REGISTRY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.login-ecr.outputs.registry }}&lt;/span&gt;
    &lt;span class="na"&gt;ECR_REPOSITORY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ECR_REPOSITORY }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;IMAGE_URI=$ECR_REGISTRY/$ECR_REPOSITORY:latest&lt;/span&gt;
    &lt;span class="s"&gt;docker build -t $IMAGE_URI .&lt;/span&gt;
    &lt;span class="s"&gt;docker push $IMAGE_URI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;login-ecr&lt;/code&gt; step outputs the registry URL, which we capture and use to construct the full image URI.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configure kubectl for EKS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update kubeconfig for EKS&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;aws eks update-kubeconfig \&lt;/span&gt;
      &lt;span class="s"&gt;--name ${{ secrets.EKS_CLUSTER_NAME }} \&lt;/span&gt;
      &lt;span class="s"&gt;--region ${{ secrets.AWS_REGION }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This authenticates the GitHub Actions runner to your EKS cluster.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Deploy the Monitoring Stack
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create monitoring namespace&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;kubectl get namespace monitoring &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || \&lt;/span&gt;
    &lt;span class="s"&gt;kubectl create namespace monitoring&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply Prometheus PVCs&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl apply -f monitoring/prometheus-pvcs.yaml&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Prometheus Stack with Helm&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;helm repo add prometheus-community \&lt;/span&gt;
      &lt;span class="s"&gt;https://prometheus-community.github.io/helm-charts&lt;/span&gt;
    &lt;span class="s"&gt;helm repo update&lt;/span&gt;
    &lt;span class="s"&gt;helm upgrade --install prometheus \&lt;/span&gt;
      &lt;span class="s"&gt;prometheus-community/kube-prometheus-stack \&lt;/span&gt;
      &lt;span class="s"&gt;--namespace monitoring \&lt;/span&gt;
      &lt;span class="s"&gt;--values monitoring/prometheus-values.yaml \&lt;/span&gt;
      &lt;span class="s"&gt;--wait \&lt;/span&gt;
      &lt;span class="s"&gt;--timeout 10m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;helm upgrade --install&lt;/code&gt; is idempotent — it installs on first run and upgrades on subsequent runs. The &lt;code&gt;--wait --timeout 10m&lt;/code&gt; flags make the pipeline block until the Helm release is healthy before proceeding.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Grafana with IP Restriction
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Grafana LoadBalancer Service with source restriction&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;cat &amp;lt;&amp;lt;EOF | kubectl apply -f -&lt;/span&gt;
    &lt;span class="s"&gt;apiVersion: v1&lt;/span&gt;
    &lt;span class="s"&gt;kind: Service&lt;/span&gt;
    &lt;span class="s"&gt;metadata:&lt;/span&gt;
      &lt;span class="s"&gt;name: prometheus-grafana&lt;/span&gt;
      &lt;span class="s"&gt;namespace: monitoring&lt;/span&gt;
      &lt;span class="s"&gt;annotations:&lt;/span&gt;
        &lt;span class="s"&gt;service.beta.kubernetes.io/aws-load-balancer-source-ranges: "${{ secrets.SOURCE_IP }}/32"&lt;/span&gt;
    &lt;span class="s"&gt;spec:&lt;/span&gt;
      &lt;span class="s"&gt;type: LoadBalancer&lt;/span&gt;
      &lt;span class="s"&gt;selector:&lt;/span&gt;
        &lt;span class="s"&gt;app.kubernetes.io/name: grafana&lt;/span&gt;
    &lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;aws-load-balancer-source-ranges&lt;/code&gt; annotation tells AWS to restrict inbound traffic to a specific IP — so Grafana isn't publicly accessible to the entire internet. The allowed IP is stored as a GitHub secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Deploy the App
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Replace image placeholder with actual ECR URI&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;IMAGE_URI="$ECR_REGISTRY/$ECR_REPOSITORY:latest"&lt;/span&gt;
    &lt;span class="s"&gt;sed -i "s|IMAGE_URI_PLACEHOLDER|$IMAGE_URI|g" k8s/app-deployment.yaml&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Delete existing postgres deployment&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;kubectl delete deployment postgres --ignore-not-found=true&lt;/span&gt;
    &lt;span class="s"&gt;kubectl wait --for=delete pod -l app=postgres --timeout=60s || true&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy application to EKS&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kubectl apply -f k8s/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sed&lt;/code&gt; command swaps the placeholder with the real ECR image URI before applying. The Postgres deletion step forces a clean redeployment so the init script runs fresh on every deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Required GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;Before the pipeline runs, configure these secrets in your GitHub repo settings under &lt;strong&gt;Settings → Secrets and variables → Actions&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IAM user access key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IAM user secret key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_REGION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;e.g. &lt;code&gt;us-east-1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ECR_REPOSITORY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ECR repo name (e.g. &lt;code&gt;titanic-app&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EKS_CLUSTER_NAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your EKS cluster name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SOURCE_IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your IP for Grafana access restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The IAM user needs permissions for ECR (push images) and EKS (describe cluster + apply manifests).&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Verify the Deployment
&lt;/h2&gt;

&lt;p&gt;After the pipeline completes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check pods are running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Get the app's load balancer URL:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get svc titanic-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hit the &lt;code&gt;EXTERNAL-IP&lt;/code&gt; in your browser — you should see the welcome message. Then test the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all passengers&lt;/span&gt;
curl http://&amp;lt;EXTERNAL-IP&amp;gt;/people

&lt;span class="c"&gt;# Add a passenger&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;EXTERNAL-IP&amp;gt;/people &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "survived": 1,
    "passengerClass": 1,
    "name": "Miss. Test User",
    "sex": "female",
    "age": 28.0,
    "siblingsOrSpousesAboard": 0,
    "parentsOrChildrenAboard": 0,
    "fare": 71.28
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Access Grafana:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get svc prometheus-grafana &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to &lt;code&gt;http://&amp;lt;EXTERNAL-IP&amp;gt;&lt;/code&gt; — Grafana will be pre-loaded with Kubernetes dashboards showing cluster health, pod resource usage, and any firing alerts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub Push → GitHub Actions
                  ├── Build Docker image
                  ├── Push to Amazon ECR
                  ├── Deploy monitoring stack (Helm → EKS)
                  │       ├── Prometheus (8Gi EBS)
                  │       ├── Grafana (5Gi EBS, LoadBalancer, IP-restricted)
                  │       └── Alertmanager (2Gi EBS) → Slack
                  └── Deploy app manifests → EKS
                            ├── Flask app (LoadBalancer, port 80 → 5000)
                            └── PostgreSQL (ClusterIP, 1Gi EBS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;gp2 storage class matters.&lt;/strong&gt; Every PVC in this setup uses &lt;code&gt;storageClassName: gp2&lt;/code&gt; — the default EKS storage class backed by AWS EBS. Without it, PVCs stay in &lt;code&gt;Pending&lt;/code&gt; state indefinitely and your pods never start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health checks prevent race conditions.&lt;/strong&gt; Without &lt;code&gt;condition: service_healthy&lt;/code&gt; in Docker Compose (and equivalent readiness probes in Kubernetes), your app will try to connect before Postgres is ready and crash on startup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;helm upgrade --install&lt;/code&gt; is pipeline-friendly.&lt;/strong&gt; It handles first-time install and subsequent upgrades in one idempotent command — no need to check whether a release already exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IP-restrict your admin interfaces.&lt;/strong&gt; Using &lt;code&gt;aws-load-balancer-source-ranges&lt;/code&gt; to lock down Grafana is a simple but effective security measure. Never expose monitoring dashboards to the public internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep secrets out of manifests.&lt;/strong&gt; The &lt;code&gt;IMAGE_URI_PLACEHOLDER&lt;/code&gt; pattern keeps image URIs out of version control. Combined with GitHub secrets for AWS credentials, nothing sensitive lives in the repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eks/" rel="noopener noreferrer"&gt;Amazon EKS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack" rel="noopener noreferrer"&gt;kube-prometheus-stack Helm Chart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws-actions/configure-aws-credentials" rel="noopener noreferrer"&gt;aws-actions/configure-aws-credentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/MsOluwademilade/titanic-test-app" rel="noopener noreferrer"&gt;Project Source Code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this walkthrough helped you, drop a reaction or a comment, I'd love to hear how you're approaching Kubernetes deployments on AWS. And if you spot something that could be improved in the architecture, let's discuss it below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>🚀 Deploying a Flask API on AWS EC2 with Nginx &amp; Gunicorn: My Journey from Zero to Production</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Mon, 14 Apr 2025 12:56:48 +0000</pubDate>
      <link>https://dev.to/msoluwademilade/deploying-a-flask-api-on-aws-ec2-with-nginx-gunicorn-my-journey-from-zero-to-production-3g37</link>
      <guid>https://dev.to/msoluwademilade/deploying-a-flask-api-on-aws-ec2-with-nginx-gunicorn-my-journey-from-zero-to-production-3g37</guid>
      <description>&lt;h2&gt;
  
  
  🚀 The Beginning: A Daunting Challenge
&lt;/h2&gt;

&lt;p&gt;If you had told me a few months ago that I’d be deploying a Flask API on an AWS EC2 instance, setting up a reverse proxy with Nginx, and configuring Gunicorn like a pro, I would have laughed and probably asked, &lt;em&gt;"Me? Deploying a full-fledged API? Abeg, be serious."&lt;/em&gt; 😅  &lt;/p&gt;

&lt;p&gt;But here we are. I did it. And I’m still in awe.  &lt;/p&gt;

&lt;p&gt;I recently started learning Python, and honestly, taking on this project felt like staring at a mountain with no climbing gear. It was intimidating. The thought of wiring everything together—Flask, EC2, Systemd, Gunicorn, Nginx—felt like an impossible task. But if there's anything I've learned, it's that when something looks impossible, you just have to dive in, make mistakes, consult AI (a lot! 😂), and smash bugs until it works.  &lt;/p&gt;

&lt;p&gt;This is the story of how I locked in, barely remembered to eat, and refused to sleep until I got my &lt;strong&gt;FunNumberAPI&lt;/strong&gt; up and running!  &lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Full source code available on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/MsOluwademilade/FunNumberAPI" rel="noopener noreferrer"&gt;FunNumberAPI&lt;/a&gt;  &lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Mission: Build &amp;amp; Deploy a Number Classification API
&lt;/h2&gt;

&lt;p&gt;The goal was simple—at least, in theory.  &lt;/p&gt;

&lt;p&gt;I wanted to build an API that could take a number and tell you fun stuff about it:&lt;br&gt;&lt;br&gt;
✅ Is it &lt;strong&gt;Prime&lt;/strong&gt;?&lt;br&gt;&lt;br&gt;
✅ Is it &lt;strong&gt;Perfect&lt;/strong&gt;?&lt;br&gt;&lt;br&gt;
✅ Is it an &lt;strong&gt;Armstrong number&lt;/strong&gt;? (Yes, I had to Google what that was. 😅)&lt;br&gt;&lt;br&gt;
✅ Is it &lt;strong&gt;Odd or Even&lt;/strong&gt;?&lt;br&gt;&lt;br&gt;
✅ What's its &lt;strong&gt;digit sum&lt;/strong&gt;?&lt;br&gt;&lt;br&gt;
✅ Can we fetch a &lt;strong&gt;random fun fact&lt;/strong&gt; about it?  &lt;/p&gt;

&lt;p&gt;Sounds fun, right? Well, it was—until I had to &lt;strong&gt;deploy it&lt;/strong&gt;. That’s where the real battle began.  &lt;/p&gt;


&lt;h2&gt;
  
  
  🔥 The Struggle: A Battle with Errors
&lt;/h2&gt;

&lt;p&gt;Everything that could go wrong, &lt;strong&gt;went wrong.&lt;/strong&gt;  &lt;/p&gt;

&lt;p&gt;First, Flask was fine on my local machine, but when I tried to deploy it… boom. &lt;strong&gt;Errors left, right, and centre.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Then Gunicorn started misbehaving.&lt;br&gt;&lt;br&gt;
Then systemd refused to start my service.&lt;br&gt;&lt;br&gt;
Then NGINX acted like it had never met me before.  &lt;/p&gt;

&lt;p&gt;At some point, I asked myself, &lt;em&gt;"Why am I doing this again?"&lt;/em&gt; But the stubborn part of me refused to give up. So I dug deep, Googled endlessly, consulted AI (shoutout to my robotic mentor, ChatGPT 😆), and slowly started making progress.  &lt;/p&gt;


&lt;h2&gt;
  
  
  🔧 The Breakthrough: Step-by-Step Deployment
&lt;/h2&gt;

&lt;p&gt;After hours of debugging, frustration, and moments of victory, I finally got my Flask app &lt;strong&gt;LIVE&lt;/strong&gt; on an AWS EC2 instance! 🎉  &lt;/p&gt;

&lt;p&gt;Here’s how I did it:  &lt;/p&gt;
&lt;h3&gt;
  
  
  🛠 &lt;strong&gt;Step 1: Building the API&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I built the backend using Flask, ensuring that it could classify numbers correctly and fetch fun facts. The full implementation, including all helper functions, can be found in my GitHub repository:  &lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;API Implementation:&lt;/strong&gt; &lt;a href="https://github.com/MsOluwademilade/FunNumberAPI/blob/main/app.py" rel="noopener noreferrer"&gt;FunNumberAPI/app.py&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;At this point, the API worked locally. But making it accessible to the world? That’s where the real battle began.&lt;/p&gt;
&lt;h3&gt;
  
  
  ☁ Step 2: Deploying to AWS EC2
&lt;/h3&gt;

&lt;p&gt;I launched an &lt;strong&gt;Ubuntu&lt;/strong&gt; EC2 instance, opened inbound rules for &lt;strong&gt;22 (SSH), 80 (HTTP), and 5000 (Custom TCP)&lt;/strong&gt; and connected to the instance via SSH  &lt;/p&gt;

&lt;p&gt;Next, I set up the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y # Update system
sudo apt install python3-pip nginx -y # Install Python &amp;amp; Pip
python3 -m venv .venv # Create a virtual environment
source .venv/bin/activate # Activate the virtual environment
pip install flask gunicorn flask_cors requests # Install required libraries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I cloned my repo&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/MsOluwademilade/FunNumberAPI.git
cd FunNumberAPI
python3 app.py 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I ran the app&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gunicorn --bind 0.0.0.0:5000 app:app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tested it: &lt;code&gt;http://&amp;lt;public-ip-of-ec2&amp;gt;:5000/api/classify-number?number=371&lt;/code&gt; ✅  &lt;/p&gt;

&lt;h3&gt;
  
  
  ⚙ Step 3: Setting Up systemd for Service Management
&lt;/h3&gt;

&lt;p&gt;To keep the API running in the background and restart it on failures, I created a systemd service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/systemd/system/flask-app.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit] Description=Gunicorn instance to serve Flask app After=network.target

[Service] 
User=ubuntu 
WorkingDirectory=/home/ubuntu/FunNumberAPI ExecStart=/home/ubuntu/.venv/bin/gunicorn --workers 3 --bind 127.0.0.1:5000 app:app 
Restart=always

[Install] 
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, enabled and started the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl start flask-app
sudo systemctl enable flask-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, even if the server rebooted, my API would start automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌐 Step 4: Configuring NGINX as a Reverse Proxy
&lt;/h3&gt;

&lt;p&gt;To expose my API on port 80 and avoid manually specifying port 5000, I configured NGINX:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/nginx/sites-available/default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replaced the contents with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    listen 80;
    server_name &amp;lt;public-ip-of-ec2&amp;gt;;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restarted NGINX&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nginx -t
sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, my API was live on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;public-ip-of-ec2&amp;gt;/api/classify-number?number=371
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏆 Acknowledgements
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Huge thanks to the HNG12 DevOps mentors for introducing this daunting yet rewarding challenge—it pushed me beyond my limits and taught me so much.&lt;/li&gt;
&lt;li&gt;Flask for making Python APIs a breeze 🍃&lt;/li&gt;
&lt;li&gt;Numbers API for the fun facts 🔢&lt;/li&gt;
&lt;li&gt;You, for checking out this project! 🎉&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🗨️ Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Deploying this API wasn’t just a technical challenge—it was a mental endurance test. There were moments when I felt stuck, times when I considered rewriting the entire thing, but pushing through taught me invaluable lessons about Flask, deployment, debugging, and perseverance.&lt;/p&gt;

&lt;p&gt;🚀 If you’re new to backend development and cloud deployment, take on projects that scare you. You’ll learn more than you ever thought possible.&lt;/p&gt;

&lt;p&gt;If you found this helpful, let’s connect! Drop a comment, share your own deployment struggles, or hit me up on &lt;a href="https://github.com/MsOluwademilade/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Let’s keep building! 🔥&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Building an Isolated Application Environment on Linux (Without Docker)</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Sun, 09 Feb 2025 18:46:47 +0000</pubDate>
      <link>https://dev.to/aws-builders/deploying-a-flask-api-on-aws-ec2-with-nginx-gunicorn-my-journey-from-zero-to-production-4de9</link>
      <guid>https://dev.to/aws-builders/deploying-a-flask-api-on-aws-ec2-with-nginx-gunicorn-my-journey-from-zero-to-production-4de9</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;In this guide, we will walk through the process of manually creating an isolated application environment on a Linux server without using Docker. By leveraging Linux namespaces, cgroups, chroot, and other system-level isolation techniques, we can simulate the core functionality of containers. These isolation mechanisms are essential for understanding how containers work and provide us with the ability to isolate applications from the host system.&lt;/p&gt;

&lt;p&gt;By the end of this, you will have created a lightweight, container-like environment on your Linux system, suitable for running applications securely without affecting the host system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Objectives
&lt;/h2&gt;

&lt;p&gt;By the end of this guide, you will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up a custom isolated environment for running applications.&lt;/li&gt;
&lt;li&gt;Use Linux namespaces to isolate processes, networking, and file systems.&lt;/li&gt;
&lt;li&gt;Use cgroups to limit CPU, memory, and disk usage.&lt;/li&gt;
&lt;li&gt;Use chroot or pivot_root to create a separate filesystem for applications.&lt;/li&gt;
&lt;li&gt;Ensure networking isolation so applications do not interfere with the host.&lt;/li&gt;
&lt;li&gt;Validate each step with relevant commands and screenshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pre-requisites
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An AWS Ubuntu EC2 instance &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Make sure you have two terminals open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One for running commands on the host.&lt;/li&gt;
&lt;li&gt;One for running commands inside the container.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install the required tools inside the container before isolating the network:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt update
apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; python3 seccomp iproute2 iputils-ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Process Isolation with Namespaces
&lt;/h2&gt;

&lt;p&gt;The first step is to isolate processes within a custom namespace, making sure they don’t interfere with processes running on the host system.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Creating an Isolated Namespace
&lt;/h3&gt;

&lt;p&gt;To create an isolated environment for the processes, use the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;unshare &lt;span class="nt"&gt;--pid&lt;/span&gt; &lt;span class="nt"&gt;--fork&lt;/span&gt; &lt;span class="nt"&gt;--mount-proc&lt;/span&gt; &lt;span class="nt"&gt;--mount&lt;/span&gt; /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What does the above command do?:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The unshare command in Linux is used to isolate a process from the host namespace, effectively creating a separate environment for the process to run.&lt;/li&gt;
&lt;li&gt;When the --pid flag is used, it creates a new PID namespace, meaning that the process will have its own process ID tree, isolated from the host's process IDs.&lt;/li&gt;
&lt;li&gt;The --fork flag ensures that a new shell is forked within this isolated namespace, allowing the user to interact with it.&lt;/li&gt;
&lt;li&gt;Additionally, the --mount-proc flag mounts a new /proc filesystem, which reflects the process information within the new namespace rather than the host, ensuring complete process isolation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why is this useful?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It ensures that processes inside this new environment will not affect or be affected by processes outside of it on the host system. This is a key concept in containerisation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To verify that the new namespace is active, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsns | &lt;span class="nb"&gt;grep &lt;/span&gt;pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a new PID namespace with a different process ID. This confirms that your new isolated process environment is working as expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Filesystem Isolation with Chroot
&lt;/h2&gt;

&lt;p&gt;Next, we’ll set up a minimal filesystem within the isolated environment. This filesystem will allow us to run applications as if they are in a completely separate environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Setting Up the Root Filesystem
&lt;/h3&gt;

&lt;p&gt;Create a directory to serve as the root filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/my_container/rootfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we use the debootstrap tool to create a minimal Debian-based system inside this directory. This tool allows us to install the most basic set of packages needed to run a system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;debootstrap &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;debootstrap &lt;span class="nt"&gt;--variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;minbase stable ~/my_container/rootfs http://deb.debian.org/debian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What do the above commands do?&lt;/strong&gt;&lt;br&gt;
The debootstrap tool sets up a minimal Debian-based system within a specified directory, providing a lightweight and isolated environment. By using the minbase variant, debootstrap includes only the most essential packages, significantly reducing the size of the setup while maintaining a functional minimal system. This approach is useful when creating isolated environments or containers where minimalism is crucial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check the contents of the root filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; ~/my_container/rootfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see essential directories like &lt;code&gt;bin&lt;/code&gt;, &lt;code&gt;lib&lt;/code&gt;, &lt;code&gt;etc&lt;/code&gt;, and &lt;code&gt;usr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpeutlhlht0wip4ymb6du.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpeutlhlht0wip4ymb6du.png" alt="Image description" width="800" height="29"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Mounting Required System Directories
&lt;/h3&gt;

&lt;p&gt;To make the system fully functional, we need to mount certain directories like &lt;code&gt;/proc&lt;/code&gt;, &lt;code&gt;/sys&lt;/code&gt;, and &lt;code&gt;/dev&lt;/code&gt; inside the container's root filesystem. These directories are required for processes to function correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/my_container/rootfs/proc
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/my_container/rootfs/sys
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/my_container/rootfs/dev
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/my_container/rootfs/dev/pts

&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-t&lt;/span&gt; proc proc ~/my_container/rootfs/proc
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;--rbind&lt;/span&gt; /sys ~/my_container/rootfs/sys
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;--rbind&lt;/span&gt; /dev ~/my_container/rootfs/dev
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;--rbind&lt;/span&gt; /dev/pts ~/my_container/rootfs/dev/pts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;These commands create the necessary directories and mount them inside the container's root filesystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;br&gt;
Check active mounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mount | &lt;span class="nb"&gt;grep&lt;/span&gt; ~/my_container/rootfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command lists the active mounts. You should see the &lt;code&gt;/proc&lt;/code&gt;, &lt;code&gt;/sys&lt;/code&gt;, &lt;code&gt;/dev&lt;/code&gt;, and &lt;code&gt;/dev/pts&lt;/code&gt; directories mounted correctly inside the container's root filesystem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4rwlostytt1rvukc7jvf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4rwlostytt1rvukc7jvf.png" alt="Image description" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3: Entering the Chroot Environment
&lt;/h2&gt;

&lt;p&gt;Now, we enter the chroot environment, which allows us to interact with the container as though it were a separate system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chroot&lt;/span&gt; ~/my_container/rootfs /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the following command to confirm that you're inside the isolated environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qijna58dsz0wyroeuph.png" alt="Image description" width="800" height="94"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Step 3: Resource Management with Cgroups
&lt;/h2&gt;

&lt;p&gt;Cgroups allow us to limit the resources (like CPU, memory, and disk usage) that processes inside the container can use.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Limiting CPU Usage
&lt;/h3&gt;

&lt;p&gt;To limit the CPU usage of our isolated container, we use the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /sys/fs/cgroup
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /sys/fs/cgroup/my_container
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"50000 100000"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /sys/fs/cgroup/my_container/cpu.max
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$$&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /sys/fs/cgroup/my_container/cgroup.procs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What do these commands do?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mkdir -p /sys/fs/cgroup&lt;/code&gt;: Creates a directory for cgroups if it doesn't already exist.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo "50000 100000" | tee /sys/fs/cgroup/my_container/cpu.max&lt;/code&gt;: Limits CPU usage to 50% of one CPU core by setting a CPU quota.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo $$ | tee /sys/fs/cgroup/my_container/cgroup.procs&lt;/code&gt;: Places the current process (the shell) into the cgroup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;br&gt;
To check the CPU limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/my_container/cpu.max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the output 50000 100000, which confirms that the CPU limit has been applied.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9yiniqbleg237agktiyv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9yiniqbleg237agktiyv.png" alt="Image description" width="783" height="43"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Setting Memory Limits
&lt;/h3&gt;

&lt;p&gt;Now, let's limit the amount of memory the container can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /sys/fs/cgroup/my_container
&lt;span class="nb"&gt;echo &lt;/span&gt;268435456 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/my_container/memory.max
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$$&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /sys/fs/cgroup/my_container/cgroup.procs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;br&gt;
To check the memory limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/my_container/memory.max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see268435456 as the output, confirming the memory limit is set correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5l69mzl8eng3l33p8ew5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5l69mzl8eng3l33p8ew5.png" alt="Image description" width="800" height="44"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Restricting Disk I/O
&lt;/h3&gt;

&lt;p&gt;To restrict disk I/O for a specific process, use the following command. (This command should be run in the host, not in the container, so run exit to go into the host and run the following commands)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ionice -c 2 -n 7 -p &amp;lt;PID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This limits the disk I/O priority for the process identified by .&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to find the PID:&lt;/strong&gt;&lt;br&gt;
To find the process ID (PID) of the container, open a new terminal, SSH into your instance, run sudo su, and use the following command to find the PID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ps aux | grep &amp;lt;container-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;br&gt;
To verify the disk I/O restrictions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ionice -p &amp;lt;PID&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output indicating the disk I/O priority for the specified process: &lt;code&gt;best-effort: prio 7&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5hygbs490f745x32ko10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5hygbs490f745x32ko10.png" alt="Image description" width="800" height="95"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Security Hardening
&lt;/h2&gt;

&lt;p&gt;In this step, we will enhance the security of our isolated environment by restricting certain system calls using seccomp. Seccomp (short for Secure Computing Mode) is a Linux kernel feature that allows us to filter and block specific system calls, thereby minimizing the attack surface of the container.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Restricting System Calls (Seccomp)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Install Seccop&lt;/strong&gt;&lt;br&gt;
First, we need to install the seccomp package, which provides the necessary tools to create and apply system call filters. Run the following command to install the seccomp package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apt install seccomp -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command ensures that the seccomp tools are available for use in our container, allowing us to restrict system calls and improve security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Create the Seccomp Profile Script&lt;/strong&gt;&lt;br&gt;
Next, we will create a Python script that will apply the seccomp profile to our container environment. The script will load the seccomp profile from a file and enforce the rules we define, such as blocking certain system calls like &lt;code&gt;ptrace&lt;/code&gt;.&lt;br&gt;
Run the following command to create and edit the script &lt;code&gt;apply_seccomp.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat &amp;gt; apply_seccomp.py &amp;lt;&amp;lt; EOF
#!/usr/bin/python3
import seccomp
import sys
import json

# Load the profile
with open('seccomp_profile.json', 'r') as f:
    profile = json.load(f)

# Create a seccomp filter
f = seccomp.SyscallFilter(seccomp.ALLOW)

# Add the rules from our profile
for syscall in profile.get('syscalls', []):
    if syscall['action'] == 'SCMP_ACT_KILL':
        f.add_rule(seccomp.KILL, syscall['name'])

# Apply the filter
f.load()

# Execute the command provided as arguments
if len(sys.argv) &amp;gt; 1:
    import os
    os.execvp(sys.argv[1], sys.argv[1:])
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What does this script do?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The script loads a seccomp profile from the seccomp_profile.json file, which contains the system call restrictions.&lt;/li&gt;
&lt;li&gt;It creates a seccomp filter and sets the default action to allow all system calls.&lt;/li&gt;
&lt;li&gt;The script then adds the specific rules from the profile. In this case, any system call marked with SCMP_ACT_KILL will be blocked and cause the process to terminate.&lt;/li&gt;
&lt;li&gt;The script then applies the filter to restrict the system calls.&lt;/li&gt;
&lt;li&gt;Finally, it executes the command provided as arguments to the script (if any), with the system call restrictions applied.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Make the script executable&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chmod +x apply_seccomp.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command changes the file permissions of the script to allow it to be executed.&lt;br&gt;
&lt;strong&gt;4. Run the Seccomp Script&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./apply_seccomp.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happens here?&lt;/strong&gt;&lt;br&gt;
When you run the script, it will apply the seccomp profile and restrict system calls based on the rules defined in the seccomp_profile.json file. If the profile indicates that a specific system call, like ptrace, should be blocked, any attempt to use ptrace will result in the process being killed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;br&gt;
To test if ptrace is blocked, try to run the following command inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strace -e ptrace ls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an error message like: &lt;code&gt;Bad system call (core dumped)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv13eisckcbu8q2vm3zwf.png" alt="Image description" width="793" height="49"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Step 5: Networking Isolation
&lt;/h2&gt;

&lt;p&gt;Networking isolation is an essential part of setting up a secure, isolated application environment. In this section, we will walk through how to create a network namespace, establish virtual network interfaces, and isolate networking between two different environments using Linux networking tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NB&lt;/strong&gt;: Until stated otherwise, the commands in this section should be run on the host machine&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create a Network Namespace
&lt;/h3&gt;

&lt;p&gt;In this first step, we create a network namespace named my_net. A network namespace is a separate network environment where we can manage network interfaces, IP addresses, and routing tables independently of the host system. The command sudo ip netns add my_net ensures that a new isolated network environment is created.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ip netns add my_net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command lists all the network namespaces present on the system. After creating my_net, it should appear in the list. This helps to verify that the namespace was successfully created.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ip netns list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsafkwvx0l8159u5ax1mj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsafkwvx0l8159u5ax1mj.png" alt="Image description" width="726" height="68"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create Virtual Ethernet Interfaces
&lt;/h3&gt;

&lt;p&gt;We now create a pair of virtual Ethernet interfaces, veth0 and veth1. These interfaces act as a bridge between the host system and the newly created network namespace. The command creates a virtual network interface (veth0) and its peer (veth1). These interfaces can communicate with each other, simulating network communication between the host system and the namespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link &lt;/span&gt;add veth0 &lt;span class="nb"&gt;type &lt;/span&gt;veth peer name veth1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands display the details of the virtual interfaces veth0 and veth1. It helps us ensure that the interfaces are created correctly and are available for use. You should see output showing these interfaces and their current states (e.g., UP or DOWN).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ip link show veth0
ip link show veth1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Move veth0 into my_net
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth0 netns my_net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Assign IP Addresses
&lt;/h3&gt;

&lt;p&gt;In this step, we assign IP addresses to the network interfaces, enabling them to communicate within the isolated network. These interfaces will allow the isolated network namespace to interact with other networks or devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assigning IP Address to veth0 Inside the Network Namespace&lt;/strong&gt;&lt;br&gt;
Next, we bring the veth0 interface up inside the my_net network namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ip netns &lt;span class="nb"&gt;exec &lt;/span&gt;my_net ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth0 up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sudo ip netns exec my_net&lt;/code&gt;: Executes the command within the my_net network namespace.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ip link set veth0 up&lt;/code&gt;: This command activates the veth0 interface, allowing it to participate in network communication. Until the interface is brought up, it cannot send or receive packets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This command assigns an IP address (&lt;code&gt;192.168.1.1/24&lt;/code&gt;) to the interface &lt;code&gt;veth0&lt;/code&gt; within the &lt;code&gt;my_net&lt;/code&gt; network namespace. By using &lt;code&gt;sudo ip netns exec my_net&lt;/code&gt;, we execute the command inside the &lt;code&gt;my_net&lt;/code&gt; namespace. This ensures that &lt;code&gt;veth0&lt;/code&gt; gets the specified IP address within the isolated environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ip netns exec my_net ip addr add 192.168.1.1/24 dev veth0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we bring the interface veth0 up inside the my_net namespace. This makes the interface active and able to participate in network communication.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ip netns exec my_net ip link set veth0 up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Assigning IP Address to &lt;code&gt;veth1&lt;/code&gt; on the Host System&lt;/strong&gt;&lt;br&gt;
On the host side, we assign the IP address &lt;code&gt;192.168.1.2/24&lt;/code&gt; to the peer interface &lt;code&gt;veth1&lt;/code&gt;. This allows &lt;code&gt;veth1&lt;/code&gt; communicate with &lt;code&gt;veth0&lt;/code&gt; (and thus with the &lt;code&gt;my_net&lt;/code&gt; namespace) through the network bridge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ip addr add 192.168.1.2/24 dev veth1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We activate veth1 by bringing it up. This allows the host system to use the interface to communicate with the isolated network namespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ip link set veth1 up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Test the Network Connectivity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ip netns &lt;span class="nb"&gt;exec &lt;/span&gt;my_net ping &lt;span class="nt"&gt;-c&lt;/span&gt; 3 192.168.1.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that the interfaces are set up, we test the connectivity between the &lt;code&gt;my_net&lt;/code&gt; namespace and the host system by using the ping command. This sends three ping requests (&lt;code&gt;-c 3&lt;/code&gt;) from the &lt;code&gt;my_net&lt;/code&gt; namespace to the IP address &lt;code&gt;192.168.1.2&lt;/code&gt; (which is assigned to &lt;code&gt;veth1&lt;/code&gt;). If the setup is correct, you should see successful ping responses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1v93bp2mi8hcgbz472h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu1v93bp2mi8hcgbz472h.png" alt="Image description" width="800" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Verify Network Interfaces Inside the Namespace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ip netns exec my_net ip a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command displays the IP address and other network details for all interfaces inside the my_net network namespace. It should show veth0 with the IP address &lt;code&gt;192.168.1.1/24&lt;/code&gt; as expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Linking to a Container Network Namespace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ln -s /proc/&amp;lt;PID&amp;gt;/ns/net /var/run/netns/my_container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this step, we link the network namespace of a running process to the container's network namespace. The &lt;code&gt;ln -s&lt;/code&gt; command creates a symbolic link to the network namespace in the &lt;code&gt;/var/run/netns/&lt;/code&gt; directory, making it accessible for further network configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Enter the Container with Network Namespace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nsenter --net=/var/run/netns/my_net -- chroot /root/my_container/rootfs /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we use nsenter to enter the network namespace of the container. This command allows us to run a shell (&lt;code&gt;/bin/bash&lt;/code&gt;) within the isolated network environment of the container. The &lt;code&gt;--net&lt;/code&gt; option tells &lt;code&gt;nsenter&lt;/code&gt; to use the network namespace we linked earlier, and the &lt;code&gt;chroot&lt;/code&gt; command changes the root directory to the container's root filesystem (&lt;code&gt;/root/my_container/rootfs&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Verify Network Configuration Inside the Container
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ip a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the container, running &lt;code&gt;ip a&lt;/code&gt; should show the network interfaces of the container, including the virtual interface (&lt;code&gt;veth0&lt;/code&gt;) that connects it to the network namespace. The IP address assigned to &lt;code&gt;veth0&lt;/code&gt; (&lt;code&gt;192.168.1.1/24&lt;/code&gt;) should also be visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jm6g3somycvopgdfw0x.png" alt="Image description" width="800" height="141"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Step 6: Deploying an Application
&lt;/h2&gt;

&lt;p&gt;In this step, we'll deploy a simple application within our isolated environment and test its accessibility both from the host and within the isolated network. We will set up a basic Python web server to demonstrate this process.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Installing Dependencies
&lt;/h3&gt;

&lt;p&gt;Before running the application, recall that at the beginning of our setup, we installed Python in our container and its required dependencies inside the container. This ensures that Python is available to run our application inside the isolated environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Running the Web Server
&lt;/h3&gt;

&lt;p&gt;To start the web server inside the isolated environment, run the following command within the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What does this command do?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;python3 -m http.server: This command starts a simple HTTP server using Python's built-in library. It listens for incoming HTTP requests.&lt;/li&gt;
&lt;li&gt;8080: This specifies that the web server should listen on port 8080.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Python web server will now run inside the isolated container, serving files on port 8080.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj258op3ktx6kp3tjnmmi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj258op3ktx6kp3tjnmmi.png" alt="Image description" width="800" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is important:&lt;/strong&gt;&lt;br&gt;
By running the server in this isolated environment, we can observe how the container handles networking and whether the isolation works as expected.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Verifying the Web Server from the Host
&lt;/h3&gt;

&lt;p&gt;To verify that the web server is accessible from the host system (i.e., the machine running the container), run the following command on the host&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl 192.168.1.1:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt;: This command is used to transfer data from or to a server using various protocols, in this case, HTTP.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;192.168.1.1:8080&lt;/code&gt;: This is the IP address of &lt;code&gt;veth0&lt;/code&gt; inside the my_net network namespace, and &lt;code&gt;8080&lt;/code&gt; is the port where our web server is running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgko5z4903b0fejxm0p8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbgko5z4903b0fejxm0p8.png" alt="Image description" width="531" height="615"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The command should display the HTML content served by the Python web server, confirming that the server inside the isolated container is accessible from the host system. You should see a webpage or content indicating the server is running.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Verifying Network Isolation
&lt;/h3&gt;

&lt;p&gt;Next, to test the network isolation, run the following command on the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What does this command do?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;curl localhost:8080&lt;/code&gt;: This command tries to access the web server by targeting the &lt;code&gt;localhost&lt;/code&gt; address (i.e., the host system itself) on port 8080.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command should not access the web server. This is because the web server is running inside the isolated network namespace (&lt;code&gt;my_net&lt;/code&gt;), which has been configured with network isolation. Therefore, localhost on the host system does not have access to the container's network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhoohap6a1erp8zuk4a5u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhoohap6a1erp8zuk4a5u.png" alt="Image description" width="800" height="36"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  This confirms that our network isolation is working properly and the application is isolated within the container environment.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This guide demonstrated how to manually create an isolated application environment on Linux using namespaces, cgroups, chroot, and network isolation. By understanding these underlying concepts, you gain valuable insights into how containers like Docker work under the hood. &lt;/p&gt;

&lt;h2&gt;
  
  
  Thank You for Reading!
&lt;/h2&gt;

&lt;p&gt;If you found this guide helpful, don’t forget to &lt;strong&gt;like&lt;/strong&gt;, &lt;strong&gt;comment&lt;/strong&gt;, and &lt;strong&gt;share&lt;/strong&gt;! Let me know if you have any questions or need further assistance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy isolating!&lt;/strong&gt; 🚀&lt;/p&gt;

</description>
      <category>aws</category>
    </item>
    <item>
      <title>A Comprehensive Guide to AWS Monitoring with Prometheus and Grafana</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Sun, 09 Feb 2025 18:46:19 +0000</pubDate>
      <link>https://dev.to/aws-builders/-386o</link>
      <guid>https://dev.to/aws-builders/-386o</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;In the fast-paced world of DevOps, monitoring and observability are essential for ensuring system reliability and cost efficiency. This guide will walk you through the process of configuring Prometheus and Grafana for AWS monitoring, setting up AWS-specific alerts and integrating AWS Cost Exporter. Whether you’re a beginner or an intermediate DevOps engineer, this guide is designed to be easy to follow and implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is this Guide for?
&lt;/h2&gt;

&lt;p&gt;This guide is tailored for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Beginners: Those new to DevOps and monitoring tools.&lt;/li&gt;
&lt;li&gt;Intermediate Engineers: Those looking to deepen their understanding of Prometheus, Grafana, and DORA metrics.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What You’ll Learn
&lt;/h2&gt;

&lt;p&gt;By the end of this guide, you’ll be able to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy Prometheus and Grafana on a cloud server.&lt;/li&gt;
&lt;li&gt;Set up Node Exporter and Blackbox Exporter for system and uptime monitoring.&lt;/li&gt;
&lt;li&gt;Configure DORA metrics tracking for CI/CD pipelines.&lt;/li&gt;
&lt;li&gt;Set up an alerting system with Slack notifications.&lt;/li&gt;
&lt;li&gt;Let’s dive in!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Part 1: Set Up Prometheus and Monitoring Tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Setting Up Prometheus
&lt;/h3&gt;

&lt;p&gt;Prometheus is an open-source monitoring system that collects and stores metrics as time series data. It’s like having a system constantly monitor your infrastructure, collecting performance data and alerting you before problems occur. Prometheus uses a pull-based model, scraping metrics from configured targets via HTTP endpoints.&lt;/p&gt;

&lt;p&gt;In this section, we’ll install Prometheus, configure it to collect data from different sources, and ensure it’s properly storing and retrieving metrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create User&lt;/strong&gt;&lt;br&gt;
We need to create a dedicated user for Prometheus. This enhances security by limiting the permissions and access of the Prometheus service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo useradd --no-create-home --shell /bin/false prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create Directories&lt;/strong&gt;&lt;br&gt;
These directories will store Prometheus configuration files and data. Organizing them separately helps in managing and backing up data efficiently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mkdir -p /etc/prometheus /var/lib/prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Download Prometheus&lt;/strong&gt;&lt;br&gt;
This step involves downloading and extracting the Prometheus binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /tmp
wget https://github.com/prometheus/prometheus/releases/download/v2.45.0/prometheus-2.45.0.linux-amd64.tar.gz
tar xvf prometheus-2.45.0.linux-amd64.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Copy Binaries&lt;/strong&gt;&lt;br&gt;
Copy the binaries to /usr/local/bin, making them easily accessible and executable from anywhere in the system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cp prometheus-2.45.0.linux-amd64/prometheus /usr/local/bin/
sudo cp prometheus-2.45.0.linux-amd64/promtool /usr/local/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Copy the Configuration Files&lt;/strong&gt;&lt;br&gt;
These files contain the necessary configurations and libraries for Prometheus to function correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cp -r prometheus-2.45.0.linux-amd64/consoles /etc/prometheus
sudo cp -r prometheus-2.45.0.linux-amd64/console_libraries /etc/prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Configure Prometheus&lt;/strong&gt;&lt;br&gt;
Edit &lt;code&gt;/etc/prometheus/prometheus.yml&lt;/code&gt; and define scrape jobs for monitoring. This configuration defines how often Prometheus scrapes data and what endpoints it monitors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node_exporter'
    static_configs:
      - targets: ['node_exporter:9100']

  # Blackbox Exporter for HTTP endpoint checks
  - job_name: 'blackbox-http'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - "https://example1.com"  # &amp;lt;your-url-here&amp;gt;
          - "https://example2.com"  # &amp;lt;your-url-here&amp;gt;
          - "https://example3.com"  # &amp;lt;your-url-here&amp;gt;
          - "https://example4.com"  # &amp;lt;your-url-here&amp;gt;
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115

  - job_name: 'github-exporter'
    static_configs:
      - targets: ['github-exporter:9118']
    labels:
      environment: 'production'

alerting:
  alertmanagers:
    - static_configs:
        - targets: ["alertmanager:9093"]

rule_files:
  - "alert_rules.yml"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Add the Alert Rules&lt;/strong&gt;&lt;br&gt;
Add the rules in the /etc/prometheus/alert_rules.yml file&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;groups:
  - name: blackbox_exporter_alerts
    rules:
      # Alert when an endpoint is down
      - alert: "Endpoint Down"
        expr: probe_success == 0
        for: 1m
        labels:
          severity: critical
          target: "{{ $labels.instance }}"
        annotations:
          summary: "A Resolve Vote URL is Down" 
          description: "{{ $labels.instance }} URL is unreachable." # (job: {{ $labels.job }})."

      # Alert when an endpoint is back up
      #- alert: EndpointUp
      #  expr: probe_success == 1
      #  for: 1m
      #  labels:
      #    severity: info
      #    target: "{{ $labels.instance }}"
      #  annotations:
      #    summary: "Endpoint is Back Online"
      #    description: "{{ $labels.instance }} is now reachable (job: {{ $labels.job }})."

      # High latency alert
      - alert: "Latency"
        expr: probe_duration_seconds &amp;gt; 1
        for: 1m
        labels:
          severity: warning
          target: "{{ $labels.instance }}"
        annotations:
          summary: "High Latency"
          description: "{{ $labels.instance }} has high latency: {{ $value }}s."

      # SSL Certificate Expiry Alert (less than 7 days)
      - alert: "SSL Expiry"
        expr: probe_ssl_earliest_cert_expiry{job="blackbox-http"} - time() &amp;lt; 86400 * 7
        for: 1m
        labels:
          severity: warning
          target: "{{ $labels.instance }}"
        annotations:
          summary: "SSL Certificate Expiry Warning"
          description: "SSL certificate for {{ $labels.instance }} expires in less than 7 days."

  - name: node_exporter_alerts
    rules:
      # High CPU Usage Alert
      - alert: High CPU Usage
        expr: avg(rate(node_cpu_seconds_total{mode="user"}[2m])) * 100 &amp;gt; 80
        for: 1m
        labels:
          severity: critical
          target: "{{ $labels.instance }}"
        annotations:
          summary: "High CPU Usage on {{ $labels.instance }}"
          description: "CPU usage has exceeded 80% for over 2 minutes."

      # High Memory Usage Alert
      - alert: HighMemoryUsage
        expr: (node_memory_Active_bytes / node_memory_MemTotal_bytes) * 100 &amp;gt; 80
        for: 1m
        labels:
          severity: critical
          target: "{{ $labels.instance }}"
        annotations:
          summary: "High Memory Usage on {{ $labels.instance }}"
          description: "Memory usage ({{ $value | humanizePercentage }}%) exceeds 80% for 2m."

      # High Disk Usage Alert
      - alert: HighDiskUsage
        expr: (node_filesystem_avail_bytes{fstype!~"tmpfs"} / node_filesystem_size_bytes{fstype!~"tmpfs"}) * 100 &amp;lt; 20
        for: 1m
        labels:
          severity: warning
          target: "{{ $labels.instance }}"
        annotations:
          summary: "High Disk Usage on {{ $labels.instance }}"
          description: "Disk space usage is critically high, less than 20% available."

      # High System Load Alert
      - alert: HighSystemLoad
        expr: node_load1 &amp;gt; (count(node_cpu_seconds_total{mode="user"}) * 1.5)
        for: 1m
        labels:
          severity: warning
          target: "{{ $labels.instance }}"
        annotations:
          summary: "High System Load on {{ $labels.instance }}"
          description: "System load ({{ $value }}) is too high compared to available CPU cores."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Set Permissions for Prometheus User&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This step ensures that the Prometheus user has the necessary access to its configuration and data files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo chown -R prometheus:prometheus /etc/prometheus /var/lib/prometheus
sudo chown prometheus:prometheus /usr/local/bin/prometheus /usr/local/bin/promtool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;9. Create Systemd Service&lt;/strong&gt;&lt;br&gt;
Create a systemd service file at &lt;code&gt;/etc/systemd/system/prometheus.service&lt;/code&gt;. This service file ensures Prometheus runs as a background process and starts automatically on boot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=Prometheus
Wants=network-online.target
After=network-online.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/bin/prometheus \
    --config.file=/etc/prometheus/prometheus.yml \
    --storage.tsdb.path=/var/lib/prometheus/ \
    --web.console.templates=/etc/prometheus/consoles \
    --web.console.libraries=/etc/prometheus/console_libraries

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;10. Enable and Start Prometheus&lt;/strong&gt;&lt;br&gt;
These commands reload the systemd manager configuration, enable Prometheus to start on boot, and start the Prometheus service immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable prometheus
sudo systemctl start prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;11. Check Prometheus Status&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl status prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wcwc7obd8vs56k7rlqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wcwc7obd8vs56k7rlqd.png" alt="Image description" width="800" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Setting Up Node Exporter
&lt;/h3&gt;

&lt;p&gt;Your computer or server is always running various processes — handling CPU load, managing memory, reading and writing to disks. But how do you keep track of these activities? Node Exporter acts as a sensor, continuously collecting system health data and making it available for Prometheus to analyze. It is a Prometheus exporter that provides detailed system metrics, including CPU, memory, disk I/O, and network statistics.&lt;/p&gt;

&lt;p&gt;We’ll install Node Exporter, connect it to Prometheus, and visualize key metrics like CPU usage, memory consumption, and disk space. This will help in spotting performance issues before they impact your system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a Node Exporter User&lt;/strong&gt;&lt;br&gt;
Similar to what we did with Prometheus, creating a dedicated user for Node Exporter enhances security.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo useradd --no-create-home --shell /bin/false node_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Download Node Exporter&lt;/strong&gt;&lt;br&gt;
Gets the latest version of Node Exporter and extracts the files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /tmp
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
tar xvf node_exporter-1.6.1.linux-amd64.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Copy Binary&lt;/strong&gt;&lt;br&gt;
This step moves the executable to a system-wide directory and sets appropriate ownership.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cp node_exporter-1.6.1.linux-amd64/node_exporter /usr/local/bin/
sudo chown node_exporter:node_exporter /usr/local/bin/node_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Create Systemd Service&lt;/strong&gt;&lt;br&gt;
Create /etc/systemd/system/node_exporter.service. This ensures Node Exporter runs as a background service and starts on boot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=Node Exporter
Wants=network-online.target
After=network-online.target

[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Enable and Start Node Exporter&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable node_exporter
sudo systemctl start node_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Check Node Exporter Status&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl status node_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplls86jzgdlmkxpv00j0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fplls86jzgdlmkxpv00j0.png" alt="Image description" width="800" height="189"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Setting Up Blackbox Exporter
&lt;/h3&gt;

&lt;p&gt;Monitoring the health of your internal system is excellent, but what about services that users interact with, like websites and APIs? Blackbox Exporter is a tool that helps test whether these external services are reachable and responding correctly. It does this by simulating user interactions, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checking if a website is online and loading correctly&lt;/li&gt;
&lt;li&gt;Measuring how long it takes for a webpage to respond&lt;/li&gt;
&lt;li&gt;Verifying whether a database or application can be reached over the network&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll set up Blackbox Exporter to monitor critical services and ensure they stay accessible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Create a System User for Blackbox&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo useradd --no-create-home --shell /bin/false blackbox_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Download Blackbox Exporter&lt;/strong&gt;&lt;br&gt;
Blackbox Exporter is used to monitor the availability and response time of network services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /tmp
wget https://github.com/prometheus/blackbox_exporter/releases/download/v0.24.0/blackbox_exporter-0.24.0.linux-amd64.tar.gz
tar -xvf blackbox_exporter-0.24.0.linux-amd64.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Copy Binary and Set Permissions&lt;/strong&gt;&lt;br&gt;
This step moves the executable to a system-wide directory and sets appropriate ownership.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cp blackbox_exporter-0.24.0.linux-amd64/blackbox_exporter /usr/local/bin/
sudo chown blackbox_exporter:blackbox_exporter /usr/local/bin/blackbox_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Create Blackbox Config Directory&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mkdir -p /etc/blackbox_exporter
sudo chown blackbox_exporter:blackbox_exporter /etc/blackbox_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Create Configuration&lt;/strong&gt;&lt;br&gt;
This configuration file defines how Blackbox Exporter should probe different types of services. Add the below config to &lt;code&gt;/etc/blackbox_exporter/blackbox.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      method: GET
      preferred_ip_protocol: "ip4"
  http_post_2xx:
    prober: http
    timeout: 5s
    http:
      method: POST
      headers:
        Content-Type: application/json
      body: '{}' 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;/etc/systemd/system/blackbox_exporter.service&lt;/code&gt;. This ensures Blackbox Exporter runs as a background service and starts on boot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=Blackbox Exporter
Wants=network-online.target
After=network-online.target

[Service]
User=blackbox_exporter
Group=blackbox_exporter
Type=simple
ExecStart=/usr/local/bin/blackbox_exporter --config.file=/etc/blackbox_exporter/blackbox.yml

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Enable and Start Blackbox Exporter&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable blackbox_exporter
sudo systemctl start blackbox_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Check Blackbox Exporter Status&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl status blackbox_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0fvuzvrxnwl5w1a3vha.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0fvuzvrxnwl5w1a3vha.png" alt="Image description" width="800" height="116"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Setting Up Grafana
&lt;/h3&gt;

&lt;p&gt;Staring at rows of numbers can be overwhelming — Grafana turns those numbers into beautiful, easy-to-read dashboards. It connects to Prometheus and helps visualize performance trends, making it easier to understand what’s happening in your system at a glance.&lt;/p&gt;

&lt;p&gt;In this section, we’ll install Grafana, configure it to pull data from Prometheus and create dashboards that display critical system and application performance metrics. By the end, you’ll have real-time, interactive charts showing exactly how your infrastructure is performing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Import Grafana GPG Key&lt;/strong&gt;&lt;br&gt;
Importing the GPG key ensures the authenticity of the Grafana packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mkdir -p /etc/apt/keyrings/
wget -q -O - https://apt.grafana.com/gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/grafana.gpg &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Add Repository&lt;/strong&gt;&lt;br&gt;
Adding the Grafana repository allows you to install Grafana using apt-get&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Update and Install Grafana&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install grafana-enterprise
sudo apt-get install grafana
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Enable and Start Grafana&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable grafana-server
sudo systemctl start grafana-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Check Grafana Status&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl status grafana-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcd88zbngr4u4amy82b4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftcd88zbngr4u4amy82b4.png" alt="Image description" width="800" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Setting Up GitHub Exporter for DORA Metrics
&lt;/h3&gt;

&lt;p&gt;If you’re building software, you want to know how efficiently your team delivers updates. That’s where DORA (DevOps Research and Assessment) metrics come in. These four key metrics help measure software delivery performance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Deployment Frequency (DF) — How often new code is deployed&lt;/li&gt;
&lt;li&gt;Lead Time for Changes (LTC) — How long it takes for a code change to go live&lt;/li&gt;
&lt;li&gt;Change Failure Rate (CFR) — How often deployments cause problems&lt;/li&gt;
&lt;li&gt;Mean Time to Recovery (MTTR) — How quickly issues are fixed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;GitHub doesn’t provide these insights directly, so we use GitHub Exporter, which collects data from GitHub repositories and makes it available to Prometheus. We’ll set up GitHub Exporter, connect it to Prometheus, and visualize DORA metrics in Grafana to track and improve software delivery speed and reliability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Install dependencies&lt;/strong&gt;&lt;br&gt;
These dependencies are necessary for running the GitHub Exporter script&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt-get install -y python3.12 python3-pip
sudo mkdir -p /opt/github_exporter
cd /opt/github_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create GitHub Exporter Script&lt;/strong&gt;&lt;br&gt;
Edit:&lt;code&gt;/opt/github_exporter/github_exporter.py&lt;/code&gt;. This script fetches deployment data from GitHub and exposes it as Prometheus metrics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import time
import requests
from datetime import datetime, timedelta
from prometheus_client import start_http_server, Gauge, Counter
import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[logging.StreamHandler()]
)
logger = logging.getLogger('github-metrics-exporter')

# GitHub API Configuration with hardcoded token
GITHUB_TOKEN = "your_github_token_here"  # Replace with your actual GitHub token

# GitHub repositories to monitor
REPOS = [
    {"owner": "&amp;lt;repo-owner&amp;gt;", "repo": "&amp;lt;repo-name&amp;gt;"},
    {"owner": "&amp;lt;repo-owner&amp;gt;", "repo": "&amp;lt;repo-name&amp;gt;"}
]

# API headers
HEADERS = {
    'Authorization': f'token {GITHUB_TOKEN}',
    'Accept': 'application/vnd.github.v3+json'
}

# Prometheus metrics
deployment_frequency = Counter('github_deployment_frequency_total', 
                             'Total number of deployments', 
                             ['repository'])

lead_time_for_changes = Gauge('github_lead_time_for_changes_seconds', 
                             'Time from commit to production in seconds', 
                             ['repository'])

change_failure_rate = Gauge('github_change_failure_rate_percent', 
                           'Percentage of deployments that failed', 
                           ['repository'])

mean_time_to_restore = Gauge('github_mean_time_to_restore_seconds', 
                            'Mean time to recover from failures in seconds', 
                            ['repository'])

class GitHubMetricsCollector:
    def __init__(self, repos, headers):
        self.repos = repos
        self.headers = headers

    def get_workflows(self, owner, repo):
        """Get all workflows for a repository"""
        url = f"https://api.github.com/repos/{owner}/{repo}/actions/workflows"
        response = requests.get(url, headers=self.headers)
        if response.status_code != 200:
            logger.error(f"Failed to get workflows: {response.status_code}, {response.text}")
            return []
        return response.json().get('workflows', [])

    def get_workflow_runs(self, owner, repo, workflow_id, time_period_days=30):
        """Get workflow runs for a specific workflow"""
        since_date = (datetime.now() - timedelta(days=time_period_days)).isoformat()
        url = f"https://api.github.com/repos/{owner}/{repo}/actions/workflows/{workflow_id}/runs?created=&amp;gt;{since_date}&amp;amp;per_page=100"
        response = requests.get(url, headers=self.headers)
        if response.status_code != 200:
            logger.error(f"Failed to get workflow runs: {response.status_code}, {response.text}")
            return []
        return response.json().get('workflow_runs', [])

    def get_commit_data(self, owner, repo, sha):
        """Get data for a specific commit"""
        url = f"https://api.github.com/repos/{owner}/{repo}/commits/{sha}"
        response = requests.get(url, headers=self.headers)
        if response.status_code != 200:
            logger.error(f"Failed to get commit data: {response.status_code}, {response.text}")
            return None
        return response.json()

    def calculate_deployment_frequency(self, owner, repo):
        """Calculate deployment frequency"""
        workflows = self.get_workflows(owner, repo)
        deployment_workflows = [w for w in workflows if 'deploy' in w.get('name', '').lower()]

        total_deployments = 0
        for workflow in deployment_workflows:
            runs = self.get_workflow_runs(owner, repo, workflow['id'])
            successful_deployments = [r for r in runs if r['conclusion'] == 'success']
            total_deployments += len(successful_deployments)

        deployment_frequency.labels(repository=f"{owner}/{repo}").inc(total_deployments)
        logger.info(f"[{owner}/{repo}] Deployment Frequency: {total_deployments} deployments")
        return total_deployments

    def calculate_lead_time_for_changes(self, owner, repo):
        """Calculate lead time for changes"""
        workflows = self.get_workflows(owner, repo)
        deployment_workflows = [w for w in workflows if 'deploy' in w.get('name', '').lower()]

        lead_times = []
        for workflow in deployment_workflows:
            runs = self.get_workflow_runs(owner, repo, workflow['id'])
            successful_deployments = [r for r in runs if r['conclusion'] == 'success']

            for run in successful_deployments:
                commit_sha = run.get('head_sha')
                if not commit_sha:
                    continue

                commit_data = self.get_commit_data(owner, repo, commit_sha)
                if not commit_data:
                    continue

                commit_time = datetime.strptime(commit_data['commit']['author']['date'], 
                                               "%Y-%m-%dT%H:%M:%SZ")
                deployment_time = datetime.strptime(run['updated_at'], 
                                                  "%Y-%m-%dT%H:%M:%SZ")

                lead_time = (deployment_time - commit_time).total_seconds()
                lead_times.append(lead_time)

        if lead_times:
            avg_lead_time = sum(lead_times) / len(lead_times)
            lead_time_for_changes.labels(repository=f"{owner}/{repo}").set(avg_lead_time)
            logger.info(f"[{owner}/{repo}] Lead Time for Changes: {avg_lead_time:.2f} seconds")
            return avg_lead_time
        return 0

    def calculate_change_failure_rate(self, owner, repo):
        """Calculate change failure rate"""
        workflows = self.get_workflows(owner, repo)
        deployment_workflows = [w for w in workflows if 'deploy' in w.get('name', '').lower()]

        total_deployments = 0
        failed_deployments = 0

        for workflow in deployment_workflows:
            runs = self.get_workflow_runs(owner, repo, workflow['id'])
            total_deployments += len(runs)
            failed_deployments += len([r for r in runs if r['conclusion'] == 'failure'])

        if total_deployments &amp;gt; 0:
            failure_rate = (failed_deployments / total_deployments) * 100
            change_failure_rate.labels(repository=f"{owner}/{repo}").set(failure_rate)
            logger.info(f"[{owner}/{repo}] Change Failure Rate: {failure_rate:.2f}%")
            return failure_rate
        return 0

    def calculate_mttr(self, owner, repo):
        """Calculate Mean Time to Restore"""
        workflows = self.get_workflows(owner, repo)
        deployment_workflows = [w for w in workflows if 'deploy' in w.get('name', '').lower()]

        recovery_times = []

        for workflow in deployment_workflows:
            runs = self.get_workflow_runs(owner, repo, workflow['id'])
            runs.sort(key=lambda x: datetime.strptime(x['created_at'], "%Y-%m-%dT%H:%M:%SZ"))

            # Find failure-success sequences
            for i in range(1, len(runs)):
                if runs[i-1]['conclusion'] == 'failure' and runs[i]['conclusion'] == 'success':
                    failure_time = datetime.strptime(runs[i-1]['updated_at'], "%Y-%m-%dT%H:%M:%SZ")
                    recovery_time = datetime.strptime(runs[i]['updated_at'], "%Y-%m-%dT%H:%M:%SZ")

                    time_to_restore = (recovery_time - failure_time).total_seconds()
                    recovery_times.append(time_to_restore)

        if recovery_times:
            mttr = sum(recovery_times) / len(recovery_times)
            mean_time_to_restore.labels(repository=f"{owner}/{repo}").set(mttr)
            logger.info(f"[{owner}/{repo}] Mean Time to Restore: {mttr:.2f} seconds")
            return mttr
        return 0

    def collect_metrics(self):
        """Collect all metrics for all repositories"""
        for repo_info in self.repos:
            owner = repo_info['owner']
            repo = repo_info['repo']

            logger.info(f"Collecting metrics for {owner}/{repo}")

            try:
                self.calculate_deployment_frequency(owner, repo)
                self.calculate_lead_time_for_changes(owner, repo)
                self.calculate_change_failure_rate(owner, repo)
                self.calculate_mttr(owner, repo)
            except Exception as e:
                logger.error(f"Error collecting metrics for {owner}/{repo}: {str(e)}")

def main():
    # Start Prometheus HTTP server
    port = 9118
    start_http_server(port)
    logger.info(f"Server started on port {port}")

    collector = GitHubMetricsCollector(REPOS, HEADERS)

    # Collect metrics every 15 minutes
    collection_interval = 15 * 60  # 15 minutes in seconds

    while True:
        collector.collect_metrics()
        logger.info(f"Metrics collection completed. Next collection in {collection_interval} seconds")
        time.sleep(collection_interval)

if __name__ == "__main__":
    main()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Install Required Python Packages&lt;/strong&gt;&lt;br&gt;
These packages are necessary for the GitHub Exporter script to function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo pip3 install requests prometheus_client pytz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update
sudo apt install python3-requests python3-prometheus-client python3-tz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Create Systemd Service&lt;/strong&gt;&lt;br&gt;
Create /etc/systemd/system/github_exporter.service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=GitHub Metrics Exporter
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/github_exporter/github_exporter.py
Restart=always

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Enable and Start GitHub Exporter&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable github_exporter
sudo systemctl start github_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Check GitHub Exporter Status&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl status github_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facszpgyenzwkjpctuf45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facszpgyenzwkjpctuf45.png" alt="Image description" width="800" height="133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Setting Up AlertManager
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Install Binaries&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget https://github.com/prometheus/alertmanager/releases/download/v0.21.0/alertmanager-0.21.0.linux-amd64.tar.gz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create User&lt;/strong&gt;&lt;br&gt;
A dedicated user enhances security by limiting permissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo groupadd -f alertmanager
sudo useradd -g alertmanager --no-create-home --shell /bin/false alertmanager
sudo mkdir -p /etc/alertmanager/templates
sudo mkdir /var/lib/alertmanager
sudo chown alertmanager:alertmanager /etc/alertmanager
sudo chown alertmanager:alertmanager /var/lib/alertmanager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Unpack Prometheus AlertManager Binary&lt;/strong&gt;&lt;br&gt;
Untar and move the downloaded Prometheus AlertManager binary&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tar -xvf alertmanager-0.21.0.linux-amd64.tar.gz
mv alertmanager-0.21.0.linux-amd64 alertmanager-files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Install Prometheus AlertManager&lt;/strong&gt;&lt;br&gt;
Copying the alertmanager and amtool binaries tousr/bin makes them globally accessible on your system. Changing ownership to the alertmanager user ensures that the AlertManager runs with the appropriate permissions, enhancing security.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cp alertmanager-files/alertmanager /usr/bin/
sudo cp alertmanager-files/amtool /usr/bin/
sudo chown alertmanager:alertmanager /usr/bin/alertmanager
sudo chown alertmanager:alertmanager /usr/bin/amtool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Install Prometheus AlertManager Configuration File&lt;/strong&gt;&lt;br&gt;
Move the &lt;code&gt;alertmanager.yml&lt;/code&gt; file from alertmanager-files to the etc/alertmanager folder and change the ownership to alertmanager user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cp alertmanager-files/alertmanager.yml /etc/alertmanager/alertmanager.yml
sudo chown alertmanager:alertmanager /etc/alertmanager/alertmanager.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Setup Prometheus AlertManager Service&lt;/strong&gt;&lt;br&gt;
Create the alertmanager service file at &lt;code&gt;/usr/lib/systemd/system/alertmanager.service&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo vi /usr/lib/systemd/system/alertmanager.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=AlertManager
Wants=network-online.target
After=network-online.target

[Service]
User=alertmanager
Group=alertmanager
Type=simple
ExecStart=/usr/bin/alertmanager \
    --config.file /etc/alertmanager/alertmanager.yml \
    --storage.path /var/lib/alertmanager/

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Set File Permissions&lt;/strong&gt;&lt;br&gt;
Setting the correct permissions ensures that the system can read and execute the service file without being modified by unauthorised users.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo chmod 664 /usr/lib/systemd/system/alertmanager.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Create Configuration File&lt;/strong&gt;&lt;br&gt;
Edit configuration file &lt;code&gt;/etc/alertmanager/alertmanager.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;global:
resolve_timeout: 1m

route:
receiver: 'slack-notifications'
group_by: ['alertname', 'job']
repeat_interval: 1h

receivers:
- name: 'slack-notifications'
slack_configs:
- channel: '#&amp;lt;channel-name-here&amp;gt;'
  send_resolved: true
  icon_url: https://avatars3.githubusercontent.com/u/3380462
  api_url: 'https://hooks.slack.com/services/&amp;lt;api-url-here&amp;gt;'
  title: |-
    [{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
    {{- if gt (len .CommonLabels) (len .GroupLabels) -}}
      (
      {{- with .CommonLabels.Remove .GroupLabels.Names }}
        {{- range $index, $label := .SortedPairs -}}
          {{ if $index }}, {{ end }}
          {{- $label.Name }}="{{ $label.Value -}}"
        {{- end }}
      {{- end }}
      )
    {{- end }}
  text: &amp;gt;-
    {{ range .Alerts -}}
    *Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - `{{ .Labels.severity }}`{{ end }}
    *Description:* {{ .Annotations.description }}
    *Details:*
      {{ range .Labels.SortedPairs }} • *{{ .Name }}:* `{{ .Value }}`
      {{ end }}
    {{ end }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;9. Reload Systemd and Start AlertManager&lt;/strong&gt;&lt;br&gt;
Reloading systemd ensures that it recognizes the new AlertManager service file. Starting the service ensures AlertManager is running and ready to handle alerts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable alertmanager
sudo systemctl start alertmanager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;10. Check AlertManager Service Status&lt;/strong&gt;&lt;br&gt;
Checking the status ensures that AlertManager is running without errors. If there are issues, the status output will provide clues for troubleshooting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl status alertmanager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzbd570wi2i92paurmky2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzbd570wi2i92paurmky2.png" alt="Image description" width="800" height="146"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By following these steps, you’ve successfully set up Prometheus AlertManager and configured it to send alerts to Slack. You’ve also prepared Grafana for visualising metrics and monitoring your system. This setup ensures that your team is notified of critical issues in real time, improving system reliability and efficiency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Configure Grafana Dashboards
&lt;/h2&gt;

&lt;p&gt;Once you’ve set up Prometheus, Node Exporter, Blackbox Exporter, and AlertManager, the next step is to visualize your metrics using Grafana. Grafana is a powerful interface tool for visualising metrics and creating dashboards that help you monitor your system and CI/CD pipeline performance. By connecting it to Prometheus, you can monitor system performance, track DORA metrics, and set up alerts for critical issues. Here’s how to configure dashboards for Node Exporter, Blackbox Exporter, and DORA metrics.&lt;/p&gt;

&lt;p&gt;Once you have all the components installed and running, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Access Grafana at &lt;code&gt;http://your-server-ip:3000&lt;/code&gt; with the default credentials&lt;/li&gt;
&lt;li&gt;Log in with the default credentials (username: admin, password: admin).&lt;/li&gt;
&lt;li&gt;Set up dashboards to visualise metrics from Prometheus, Node Exporter, and Blackbox Exporter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh39yj4yuf7c87l3l1u5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh39yj4yuf7c87l3l1u5t.png" alt="Image description" width="800" height="738"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Configuring Node Exporter Dashboard&lt;/strong&gt;&lt;br&gt;
The Node Exporter dashboard provides insights into system metrics like CPU usage, memory usage, disk usage, and more. Here’s how to set it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Create a New Dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click on the “Create” button (plus icon) in the left sidebar.&lt;/li&gt;
&lt;li&gt;Select “Import” from the dropdown menu.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Import Node Exporter Dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the “Import via grafana.com” textbox, enter the Node Exporter dashboard UID: 1860.&lt;/li&gt;
&lt;li&gt;Click “Load”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyu5hytlac0jzsikpt3lj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyu5hytlac0jzsikpt3lj.png" alt="Image description" width="800" height="840"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Select Data Source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose Prometheus as your data source&lt;/li&gt;
&lt;li&gt;Click "Import"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;View Your Dashboard&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You'll now see a fully configured Node Exporter dashboard with panels for CPU, memory, disk, and other system metrics.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1gkdafycmt5xee5jaf2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1gkdafycmt5xee5jaf2.png" alt="Image description" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Configuring Blackbox Exporter Dashboard&lt;/strong&gt;&lt;br&gt;
The Blackbox Exporter dashboard helps you monitor uptime, HTTP response times, and SSL certificate expiration. Here's how to set it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a New Dashboard:

&lt;ul&gt;
&lt;li&gt;Click on the "Create" button (plus icon) in the left sidebar.&lt;/li&gt;
&lt;li&gt;Select "Import" from the dropdown menu.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Import Blackbox Exporter Dashboard:

&lt;ul&gt;
&lt;li&gt;Same as you did with Node Exporter, in the "Import via grafana.com" textbox, enter the Blackbox Exporter dashboard UID: 7587.&lt;/li&gt;
&lt;li&gt;Click "Load"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Select Data Source:

&lt;ul&gt;
&lt;li&gt;Choose Prometheus as your data source&lt;/li&gt;
&lt;li&gt;Click "Import"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;View Your Dashboard

&lt;ul&gt;
&lt;li&gt;You'll now see a fully configured Blackbox Exporter dashboard with panels for CPU, memory, disk, and other system metrics.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn231lmkylr8mhvb1y9m2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn231lmkylr8mhvb1y9m2.png" alt="Image description" width="800" height="623"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Configuring DORA Metrics Dashboard&lt;/strong&gt;&lt;br&gt;
The DORA metrics dashboard tracks key CI/CD performance indicators like Deployment Frequency, Lead Time for Changes, Change Failure Rate, and Mean Time to Restore. Here's how to set it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a New Dashboard:

&lt;ul&gt;
&lt;li&gt;Click on the "Create" button (plus icon) in the left sidebar.&lt;/li&gt;
&lt;li&gt;Select "New Dashboar" from the dropdown menu.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Add Panels for DORA Metrics:

&lt;ul&gt;
&lt;li&gt;Click on the "Add" dropdown and select "Visualization".&lt;/li&gt;
&lt;li&gt;In the Queries panel, select the following metrics:
     github_deployment_frequency_total 
     github_change_failure_rate_percent
     github_mean_time_to_restore_seconds
     github_lead_time_for_changes_seconds&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save Your Dashboard:

&lt;ul&gt;
&lt;li&gt;Click "Save" to save your dashboard.&lt;/li&gt;
&lt;li&gt;Give it a meaningful name, like "DORA Metrics Dashboard".&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;View and Customize:

&lt;ul&gt;
&lt;li&gt;You can now view your DORA metrics in real time.&lt;/li&gt;
&lt;li&gt;Feel free to edit, move panels around, or change visualization types (e.g., graphs, gauges, tables).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Customizing Your Dashboards&lt;/strong&gt;&lt;br&gt;
Grafana is highly customizable, so don't be afraid to get creative! Here are some tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Edit Panels: Click on a panel title and select "Edit" to change the visualization type or query.&lt;/li&gt;
&lt;li&gt;Move Panels: Drag and drop panels to rearrange them.&lt;/li&gt;
&lt;li&gt;Add Alerts: Set up alerts directly from Grafana panels to notify your team of critical issues.&lt;/li&gt;
&lt;li&gt;Save Changes: Always save your dashboard after making changes.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Part 3: Implementing AWS Cost Exporter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Create Cost Exporter Directory&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mkdir -p /opt/cost_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create the Python Cost Exporter Script&lt;/strong&gt;&lt;br&gt;
Create the file at &lt;code&gt;/opt/cost_exporter/cost_exporter.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import time
import boto3
from datetime import datetime, timedelta
from prometheus_client import start_http_server, Gauge
import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[logging.StreamHandler()]
)
logger = logging.getLogger('aws-cost-exporter')

# Create metrics
aws_service_cost = Gauge('aws_service_cost_dollars', 'Cost in dollars by AWS service', ['service'])
aws_total_cost = Gauge('aws_total_cost_dollars', 'Total AWS cost in dollars', [])
aws_budget_usage = Gauge('aws_budget_usage_percent', 'Budget usage percentage', ['budget_name'])

def collect_cost_metrics():
    """Collect AWS cost metrics and update Prometheus gauges"""
    ce_client = boto3.client('ce')
    budgets_client = boto3.client('budgets')

    # Get current date and start of month
    end_date = datetime.utcnow().strftime('%Y-%m-%d')
    start_date = datetime(datetime.utcnow().year, datetime.utcnow().month, 1).strftime('%Y-%m-%d')

    try:
        # Get cost by service
        response = ce_client.get_cost_and_usage(
            TimePeriod={
                'Start': start_date,
                'End': end_date
            },
            Granularity='MONTHLY',
            Metrics=['UnblendedCost'],
            GroupBy=[
                {
                    'Type': 'DIMENSION',
                    'Key': 'SERVICE'
                }
            ]
        )

        total_cost = 0

        # Process service costs
        for group in response['ResultsByTime'][0]['Groups']:
            service_name = group['Keys'][0]
            cost = float(group['Metrics']['UnblendedCost']['Amount'])
            total_cost += cost
            aws_service_cost.labels(service=service_name).set(cost)
            logger.info(f"Service: {service_name}, Cost: ${cost:.2f}")

        # Set total cost
        aws_total_cost.set(total_cost)
        logger.info(f"Total Cost: ${total_cost:.2f}")

        # Get budgets and their usage
        try:
            budgets_response = budgets_client.describe_budgets(
                AccountId=boto3.client('sts').get_caller_identity().get('Account')
            )

            for budget in budgets_response.get('Budgets', []):
                budget_name = budget['BudgetName']
                calculated_spend = float(budget.get('CalculatedSpend', {}).get('ActualSpend', {}).get('Amount', 0))
                budget_limit = float(budget.get('BudgetLimit', {}).get('Amount', 0))

                if budget_limit &amp;gt; 0:
                    usage_percent = (calculated_spend / budget_limit) * 100
                    aws_budget_usage.labels(budget_name=budget_name).set(usage_percent)
                    logger.info(f"Budget: {budget_name}, Usage: {usage_percent:.2f}%")
        except Exception as e:
            logger.error(f"Error getting budget information: {str(e)}")

    except Exception as e:
        logger.error(f"Error collecting cost metrics: {str(e)}")

def main():
    # Start up the server to expose the metrics.
    port = 9108
    start_http_server(port)
    logger.info(f"AWS Cost Exporter started on port {port}")

    # Update metrics every hour
    while True:
        collect_cost_metrics()
        time.sleep(3600)  # 1 hour

if __name__ == '__main__':
    main()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Install Required Python Packages&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt install python3-boto3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Create a Systemd Service for the Cost Exporter&lt;/strong&gt;&lt;br&gt;
Create /etc/systemd/system/cost_exporter.service file and add the following to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=AWS Cost Exporter
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/cost_exporter/cost_exporter.py
Restart=always

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Start the Cost Exporter&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable cost_exporter
sudo systemctl start cost_exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Update Prometheus Configuration&lt;/strong&gt;&lt;br&gt;
Add the following to &lt;code&gt;/etc/prometheus/prometheus.yml&lt;/code&gt; under the scrape_configs section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- job_name: 'aws_cost'
  static_configs:
    - targets: ['localhost:9108']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Restart Prometheus&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl restart prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 4: Setting Up AWS-Specific Alerts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Add AWS Alert Rules to Prometheus&lt;/strong&gt;&lt;br&gt;
Add the following to &lt;code&gt;/etc/prometheus/alert_rules.yml&lt;/code&gt; under a new group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- name: aws_alerts
  rules:
    - alert: HighEC2CPUUsage
      expr: avg(aws_ec2_cpuutilization_average{instance=~".*"}) by (instance) &amp;gt; 80
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "High CPU Usage on EC2 Instance {{ $labels.instance }}"
        description: "EC2 Instance {{ $labels.instance }} has high CPU usage ({{ $value }}%) for 5 minutes."

    - alert: RDSHighCPUUsage
      expr: aws_rds_cpuutilization_average &amp;gt; 80
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "High CPU Usage on RDS Instance {{ $labels.dbinstance_identifier }}"
        description: "RDS Instance {{ $labels.dbinstance_identifier }} has high CPU usage ({{ $value }}%) for 5 minutes."

    - alert: LambdaErrors
      expr: increase(aws_lambda_errors_sum[1h]) &amp;gt; 10
      labels:
        severity: warning
      annotations:
        summary: "High Error Rate on Lambda Function {{ $labels.function_name }}"
        description: "Lambda Function {{ $labels.function_name }} has more than 10 errors in the past hour."

    - alert: BudgetNearLimit
      expr: aws_budget_usage_percent &amp;gt; 90
      labels:
        severity: warning
      annotations:
        summary: "Budget Usage Approaching Limit"
        description: "Budget {{ $labels.budget_name }} is at {{ $value | printf \"%.2f\" }}% of its limit."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Restart Prometheus&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl restart prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwry06am9euc0x76703y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjwry06am9euc0x76703y.png" alt="Image description" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By following these steps, you've successfully configured Grafana dashboards for Node Exporter, Blackbox Exporter, DORA metrics, AWS-specific alerts and integrated AWS Cost Exporter into Prometheus and Grafana. These dashboards provide a clear view of your system's performance and CI/CD pipeline efficiency, helping you make data-driven decisions.&lt;br&gt;
Don't forget to explore Grafana's extensive library of pre-built dashboards and plugins to further enhance your monitoring setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank You for Reading!
&lt;/h2&gt;

&lt;p&gt;If you found this guide helpful, don't forget to &lt;strong&gt;like, comment, and share&lt;/strong&gt;! Let me know if you have any questions or need further assistance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Happy monitoring!&lt;/strong&gt; 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://grafana.com/grafana/dashboards/1860-node-exporter-full/" rel="noopener noreferrer"&gt;https://grafana.com/grafana/dashboards/1860-node-exporter-full/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/grafana/dashboards/7587-prometheus-blackbox-exporter/" rel="noopener noreferrer"&gt;https://grafana.com/grafana/dashboards/7587-prometheus-blackbox-exporter/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://shishirkh.medium.com/monitoring-stack-setup-part-4-blackbox-exporter-f7ba54d0ed99" rel="noopener noreferrer"&gt;https://shishirkh.medium.com/monitoring-stack-setup-part-4-blackbox-exporter-f7ba54d0ed99&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.couchbase.com/tutorial-configure-alertmanager" rel="noopener noreferrer"&gt;https://developer.couchbase.com/tutorial-configure-alertmanager&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>monitoring</category>
      <category>aws</category>
      <category>prometheus</category>
    </item>
    <item>
      <title>NGINX Configuration: My HNG DevOps Stage 0 Experience</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Wed, 29 Jan 2025 02:36:16 +0000</pubDate>
      <link>https://dev.to/aws-builders/nginx-configuration-my-hng-devops-stage-0-experience-31je</link>
      <guid>https://dev.to/aws-builders/nginx-configuration-my-hng-devops-stage-0-experience-31je</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As part of the HNG DevOps Stage 0 task, I was required to set up and configure NGINX on a fresh Ubuntu server using any cloud platform of my choice, ensuring it serves a custom HTML page. This exercise aimed to demonstrate my ability to configure a web server and deploy a simple static site.&lt;/p&gt;

&lt;p&gt;At first glance, this may seem like a simple task—after all, installing a web server and displaying a basic page shouldn’t be that difficult, right? But as with most things in DevOps, the real value isn’t in blindly following steps; it’s in understanding why each step matters, how to troubleshoot issues efficiently, and how to apply these concepts in real-world environments.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk through my approach to completing this task, the challenges I encountered, how I resolved them, and the broader lessons this exercise reinforced about working as a DevOps engineer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps Taken
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Provisioning an AWS EC2 Instance&lt;/strong&gt;&lt;br&gt;
A good DevOps engineer must be comfortable working with cloud infrastructure. I chose AWS as my cloud provider. I launched an EC2 instance using the Ubuntu AMI, carefully configuring security groups to allow HTTP traffic (port 80) and SSH access (port 22).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Downloading the Key and Logging In&lt;/strong&gt;&lt;br&gt;
After the instance was provisioned, I downloaded the private key (.pem file) and logged in using SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -i my-key.pem ubuntu@&amp;lt;server-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, an error message could have slowed me down. I initially forgot where my key file was saved, but a quick check of my download directory saved me from unnecessary frustration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Updating the System Packages&lt;/strong&gt;&lt;br&gt;
Before installing NGINX,  I made sure my package list was up to date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is essential because outdated package lists can cause unexpected installation errors—a common oversight that can lead to unnecessary debugging later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Installing NGINX&lt;/strong&gt;&lt;br&gt;
With the system updated, I installed NGINX by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt install nginx -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. Verifying NGINX Installation&lt;/strong&gt;&lt;br&gt;
To confirm that NGINX was installed and running correctly, I checked its status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl status nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it was not running, I would have started it manually with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;6. Locating and Modifying the Default HTML File&lt;/strong&gt;&lt;br&gt;
Here’s where I encountered one of my biggest challenges.&lt;/p&gt;

&lt;p&gt;I wasn’t immediately sure where the default NGINX HTML file was. Was it &lt;code&gt;/usr/share/nginx/html/index.html&lt;/code&gt; or &lt;code&gt;/var/www/html/index.html&lt;/code&gt;? I debated between both paths before finally checking the NGINX configuration in &lt;code&gt;/etc/nginx/sites-enabled/default&lt;/code&gt;. A simple look there earlier would have saved me time!&lt;/p&gt;

&lt;p&gt;Once I confirmed the correct location (&lt;code&gt;/var/www/html/index.html&lt;/code&gt;), I edited the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd /var/www/html
sudo nano index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I replaced the existing content with the required message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;Welcome&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;h1&amp;gt;Welcome to DevOps Stage 0 - [Your name here]/[Your username here]&amp;lt;/h1&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;7. Restarting NGINX&lt;/strong&gt;&lt;br&gt;
After saving the file, I restarted NGINX to apply the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;8. Testing the Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The final step was to confirm everything worked as expected. I opened a browser and navigated to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;your-server-ip&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seeing my custom message confirmed that everything was set up correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and How I Overcame Them
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Misplacing the Private Key File&lt;/strong&gt;&lt;br&gt;
At first, I couldn’t log in via SSH because my terminal couldn’t locate the .pem file. The solution? Checking the correct directory and rerunning the command. This was a reminder that even small errors can waste time if not caught quickly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Uncertainty About the NGINX Default Directory&lt;/strong&gt;&lt;br&gt;
I spent unnecessary time debating between different file paths. A quick check of /etc/nginx/sites-enabled/default would have clarified it instantly. The lesson here? Read instructions carefully before jumping into troubleshooting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Permission Errors While Editing HTML&lt;/strong&gt;&lt;br&gt;
While modifying index.html, I ran into permission errors. Using sudo solved the problem, but it reinforced the importance of understanding file permissions and privilege management.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Personal Growth
&lt;/h2&gt;

&lt;p&gt;This task may have seemed straightforward, but the growth it sparked was immense. Every step reinforced the importance of resilience, adaptability, and problem-solving—qualities that go beyond just configuring a web server.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Patience and Attention to Detail&lt;/strong&gt; – I learnt the hard way that missing a simple detail, like the correct HTML file path, can lead to unnecessary troubleshooting. Slowing down and reading configurations properly saved me time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confidence in Debugging&lt;/strong&gt; – Every roadblock was an opportunity to sharpen my troubleshooting mindset. Instead of panicking, I approached problems systematically and solved them efficiently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Building Resilience&lt;/strong&gt; – I faced errors, hit dead ends, and had to restart processes, but that’s exactly what makes DevOps exciting. Overcoming challenges is part of the journey, and this experience solidified my ability to push through obstacles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Embracing a Growth Mindset&lt;/strong&gt; – DevOps isn’t about memorising commands; it’s about constantly learning, adapting, and improving. This task was just one step, but it reminded me that every challenge is an opportunity to grow.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ultimately, this wasn’t just about setting up NGINX, it was about becoming a better problem solver, a more confident engineer, and a more resilient individual.&lt;/p&gt;

&lt;p&gt;At this point, it’s clear that &lt;strong&gt;HNG doesn’t just teach you technical skills—it prepares you to think like a DevOps engineer&lt;/strong&gt;. But this task? It’s just the beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  HNG: A Proven Hub for Elite DevOps Talent
&lt;/h2&gt;

&lt;p&gt;This project, while valuable, barely scratches the surface of what HNG engineers are capable of.&lt;/p&gt;

&lt;p&gt;HNG isn’t just another training program—it’s an intensive, real-world simulation that pushes engineers beyond basic configurations. It tests problem-solving skills, adaptability, and the ability to function in high-stakes DevOps environments.&lt;/p&gt;

&lt;p&gt;HNG has produced top-tier engineers who have mastered automation, cloud-native technologies, infrastructure management, security, and scalability—all essential for today’s evolving tech landscape. These graduates emerge with battle-tested expertise, making them an asset to any organisation looking for skilled professionals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking to Hire the Best? Start with HNG!
&lt;/h2&gt;

&lt;p&gt;If you're a company seeking top DevOps, Cloud, and Infrastructure Engineering talent, hiring from HNG should be your &lt;strong&gt;first priority&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;HNG alumni have demonstrated their ability to thrive in high-pressure environments, adapt to industry trends, and deliver production-ready solutions.&lt;/p&gt;

&lt;p&gt;Explore elite HNG talents available for hire:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hng.tech/hire/devops-engineers" rel="noopener noreferrer"&gt;DevOps Engineers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hng.tech/hire/site-reliability-engineers" rel="noopener noreferrer"&gt;Site Reliability Engineers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a track record of producing world-class engineers, HNG remains the premier destination for companies seeking elite tech talent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So, if you're looking for talents who can truly deliver, start your search here.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>nginx</category>
    </item>
    <item>
      <title>Deploying the Spring PetClinic Sample Application to an EKS Cluster with ECR</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Wed, 20 Mar 2024 23:34:10 +0000</pubDate>
      <link>https://dev.to/aws-builders/deploying-the-spring-petclinic-sample-application-to-an-eks-cluster-with-ecr-3n6p</link>
      <guid>https://dev.to/aws-builders/deploying-the-spring-petclinic-sample-application-to-an-eks-cluster-with-ecr-3n6p</guid>
      <description>&lt;p&gt;As indicated by the title, our objective is to set up the well-known Spring PetClinic Sample Application on EKS starting from the very beginning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;An AWS account&lt;/li&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Maven&lt;/li&gt;
&lt;li&gt;A knowledge of Kubernetes&lt;/li&gt;
&lt;li&gt;kubectl&lt;/li&gt;
&lt;li&gt;eksctl&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;AWS CLI&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Cloning and running the Pet Clinic Application.&lt;/li&gt;
&lt;li&gt;Creating a container image from the application.&lt;/li&gt;
&lt;li&gt;Upload a custom image to the Elastic Container Registry (ECR).&lt;/li&gt;
&lt;li&gt;Create an Elastic Kubernetes Service (EKS) cluster.&lt;/li&gt;
&lt;li&gt;Deploy the created ECR image to the EKS cluster.&lt;/li&gt;
&lt;li&gt;Delete all resources.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Clone the application
&lt;/h2&gt;

&lt;p&gt;For the first step, head over to Github to clone the application&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/spring-projects/spring-petclinic.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, run the application&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd spring-petclinic
./mvnw package
java -jar target/*.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating a container image from the application.
&lt;/h2&gt;

&lt;p&gt;First, create a file; we'll name it Dockerfile. Note that it should be created in the root of the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM eclipse-temurin:17-jdk-jammy
WORKDIR /app
COPY .mvn/ .mvn 
COPY mvnw pom.xml ./
RUN ./mvnw dependency:resolve
COPY src ./src
CMD ["./mvnw", "spring-boot:run"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker build -t petclinic .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command builds Docker images from the Dockerfile we created.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjfrpyx2pdgmu3r6gc7t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpjfrpyx2pdgmu3r6gc7t.png" alt="Image description" width="800" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker run -p 8080:8080 petclinic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command runs a command in a new container, pulls the image if needed, and starts the container.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjleogqo6fu1npbso05eb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjleogqo6fu1npbso05eb.png" alt="Image description" width="800" height="162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this step, you should see the app running in your browser using &lt;strong&gt;localhost:8080&lt;/strong&gt;. If, by chance, port 8080 is busy, you can change the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload a custom image to the Elastic Container Registry (ECR)
&lt;/h2&gt;

&lt;p&gt;To begin, you'll want to set up an ECR registry. Start by logging into the AWS console and locating ECR using the search bar. Once you've found it, click on the "Create repository" button, which will switch the interface to a form where you'll need to provide more details.&lt;/p&gt;

&lt;p&gt;Now, assign a name to your repository. I recommend setting it to private for added security. Finally, proceed by clicking on "Create repository" using the default settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjcgir1pa96grn2oig3rk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjcgir1pa96grn2oig3rk.png" alt="Image description" width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the next step, we login to the newly created private repository using this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws ecr get-login-password --region _your-chosen-region_ | docker login --username AWS --password-stdin _your-account-id_.dkr.ecr.us-east-1.amazonaws.com/_your-repository-name_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now use the following command to modify the tag of our local image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker tag 87fe7e888a17 _your-account-id_.dkr.ecr.us-east-1.amazonaws.com/_your-repository-name:v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;87fe7e888a17&lt;/strong&gt; represents the local image tag; you can find it using the &lt;code&gt;docker images&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Finally, we can push this image using the following command. Remember, you have the flexibility to utilize any tag in place of v1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker push _your-account-id_.dkr.ecr.us-east-1.amazonaws.com/_your-repository-name:v1_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftgu51eulshkhc2gzd2mk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftgu51eulshkhc2gzd2mk.png" alt="Image description" width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create an Elastic Kubernetes Service (EKS) cluster
&lt;/h2&gt;

&lt;p&gt;With the previous steps, it's time to create an EKS cluster and use the image created in the previous step.&lt;/p&gt;

&lt;p&gt;To create a cluster, run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eksctl create cluster --name _your-cluster-name_ --region _your-chosen-region_ --node-type t2.medium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcvlxw7d4jdgxu938gmzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcvlxw7d4jdgxu938gmzo.png" alt="Image description" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, create a namespace. (A Kubernetes namespace is a logical abstraction used to organise and partition resources within a Kubernetes cluster.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl create namespace petclinic-one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy the created ECR image to the EKS cluster
&lt;/h2&gt;

&lt;p&gt;To add a Kubernetes resource. We will use two files.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A deployment file pointing to our image of the ECR&lt;/li&gt;
&lt;li&gt;An AWS application load balancer that is automatically created for us by a service of the type LoadBalancer and network load balancer; we will point to port 8080 since that is where our application runs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;petclinic-deployment.yaml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: apps/v1
kind: Deployment
metadata:
  name: petclinic
  namespace: petclinic-one
  labels:
    name: petclinic
spec:
  replicas: 1
  selector: 
    matchLabels:
      app: petclinic
  template:
    metadata:
      labels:
        app: petclinic
    spec:
      containers:
        - name: petclinic
          image: _your-account-id_.dkr.ecr.us-east-1.amazonaws.com/_your-repository-name:v1_
          ports:
            - containerPort: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;petclinic-service.yaml&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apiVersion: v1
kind: Service
metadata:
  name: petclinic
  namespace: petclinic-one
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
    service.beta.kubernetes.io/aws-load-balancer-internal: "false"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
  type: LoadBalancer
  ports:
    - name: web
      port: 80
      targetPort: 8080
  selector:
    app: petclinic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, apply the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl apply -f petclinic-deployment.yaml
kubectl apply -f petclinic-service.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, check the status&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get pods -n eks-demo-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwe20tw1jtkgl4sd3dk27.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwe20tw1jtkgl4sd3dk27.png" alt="Image description" width="613" height="62"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get svc -n eks-demo-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdrhugr76dsq16vgh4g9g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdrhugr76dsq16vgh4g9g.png" alt="Image description" width="800" height="34"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now use that external IP in your browser to hit your API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete all resources
&lt;/h2&gt;

&lt;p&gt;To avoid a huge bill overnight, run the following command to delete the ECR repository EKS cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eksctl delete cluster --name petclinic --region _your-chosen-region_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>aws</category>
      <category>kubernetes</category>
      <category>spring</category>
    </item>
    <item>
      <title>Automating the Provisioning of AWS EKS Cluster Using Terraform and CircleCI</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Wed, 29 Mar 2023 21:22:20 +0000</pubDate>
      <link>https://dev.to/msoluwademilade/automating-the-provisioning-of-aws-eks-cluster-using-terraform-and-circleci-361p</link>
      <guid>https://dev.to/msoluwademilade/automating-the-provisioning-of-aws-eks-cluster-using-terraform-and-circleci-361p</guid>
      <description>&lt;h2&gt;
  
  
  What is CircleCI?
&lt;/h2&gt;

&lt;p&gt;CircleCI is one of the potent tools used for continuous integration and continuous deployment (CI/CD), for those who are unfamiliar. With the aid of CI/CD, a group of ideas, methods, and tools, consumers can receive software updates of any kind in a quick, effective, repeatable, and secure manner.&lt;/p&gt;

&lt;p&gt;In the link below, I was able to go into great detail about CI/CD, including what it is, why it is important, and various CI/CD tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/msoluwademilade/jenkins-vs-circleci-1md"&gt;Jenkins vs CircleCI&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Terraform?
&lt;/h2&gt;

&lt;p&gt;Terraform is a provisioning management tool that enables infrastructure automation for provisioning, compliance, and management of any cloud, datacenter, and service. &lt;/p&gt;

&lt;p&gt;Allocating resources from a cloud service provider to a client is known as cloud provisioning. It specifies how a client acquires resources and cloud services from a supplier, making it a crucial part of cloud computing. It may also be described as the process of automating the management of cloud computing service installation, configuration, and management.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Needed
&lt;/h2&gt;

&lt;p&gt;For this tutorial you'll need the following&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An AWS account&lt;/li&gt;
&lt;li&gt;An access key and secret access key file&lt;/li&gt;
&lt;li&gt;An Integrated Development Environment (IDE) of your choice.&lt;/li&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;li&gt;A CircleCI account. You can sign up for a CircleCI account using your GitHub account if you don't already have one.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 1: Clone this repository
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;https://github.com/MsOluwademilade/learn-terraform-eks-circleci&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Review the CircleCI configuration file
&lt;/h3&gt;

&lt;p&gt;The CircleCI configuration (which was gotten from &lt;a href="https://github.com/hashicorp/learn-terraform-circleci" rel="noopener noreferrer"&gt;here&lt;/a&gt;) will complete four jobs during the process of automating the Terraform workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The “plan-apply” Job&lt;/strong&gt;: &lt;br&gt;
The image "hashicorp/terraform: light," which, according to HashiCorp, includes a Terraform binary, will be used by this job. The "terraform init" command and the checkout procedures are carried out by this task. A file named "tfapply" will also be created by executing the "terraform plan -out" command.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnreezb1egskj0ppbyl25.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnreezb1egskj0ppbyl25.png" alt="Image description" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The “apply” Job&lt;/strong&gt;: &lt;br&gt;
The "attach_workspace" step in the apply job loads the previously persisted workspace in the "plan" job, according to HashiCorp's description of this task. The "apply" job will execute the "terraform apply" command, which we created in the previous step, to run "tfapply." &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8t9dmyvfjilbk96ypa14.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8t9dmyvfjilbk96ypa14.png" alt="Image description" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The “plan-destroy” Job&lt;/strong&gt;: &lt;br&gt;
A strategy to destroy the distributed infrastructure is generated by this job.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvgubojkt6hf43bjn9txn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvgubojkt6hf43bjn9txn.png" alt="Image description" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The “destroy” Job&lt;/strong&gt;: &lt;br&gt;
The strategy to destroy the infrastructure is carried out in this step of the procedure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NB&lt;/strong&gt;: Users are advised by HashiCorp to monitor the "plan-destroy" and "destroy" tasks carefully while they are in operation. Users will be expected to keep tabs on the status of each job despite the possibility of interruptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The final element of this CircleCI setup is the &lt;strong&gt;workflow&lt;/strong&gt;. It coordinates the sequencing and specifications for each pipeline task. According to HashiCorp, the "plan_approve_apply" workflow follows a sequential process that creates applications for each stage along the way. Therefore, the prior job must have run successfully in order for the current job to run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step Three: Set up the project in CircleCI
&lt;/h3&gt;

&lt;p&gt;Since our GitHub and CircleCI accounts are connected, you can already see the forked repository displayed under "Projects." Choose the blue "Set Up Project" button to move to the following page when attempting this project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F62xh99fsy3i8caxecyfq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F62xh99fsy3i8caxecyfq.png" alt="Image description" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select "Fastest" on the subsequent screen, then press the "Start Building" button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2pcy4ru6yl2tbsrwwz15.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2pcy4ru6yl2tbsrwwz15.png" alt="Image description" width="516" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CircleCI will try to execute the jobs inside the pipelines right away, but it will fail. That outcome happened as a consequence of the absence of two essential components for a smooth operation. To continue with our project, we must input our AWS Access Key ID and AWS Secret Access Key in the "Environment Variables" screen and save them. &lt;/p&gt;

&lt;p&gt;Use AWS_ACCESS_KEY_ID as the name and enter the value for the Access Key ID. Use AWS_SECRET_ACCESS_KEY as the name and enter the value for the Secret Access Key.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F32ve8h8zoxc8xfapkvyv.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F32ve8h8zoxc8xfapkvyv.jpeg" alt="Image description" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The name of the project that is currently in focus, the pipeline's job statuses, the workflow, the repo branch/commit, and settings are all shown on the accompanying screen by CircleCI. Start the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step Four: Destroy your resources
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1ltyh0gy5im237ae29d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm1ltyh0gy5im237ae29d.png" alt="Image description" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a later article, I'll decsribe how to access the deployed resources.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Jenkins vs CircleCI</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Mon, 30 Jan 2023 12:49:25 +0000</pubDate>
      <link>https://dev.to/msoluwademilade/jenkins-vs-circleci-1md</link>
      <guid>https://dev.to/msoluwademilade/jenkins-vs-circleci-1md</guid>
      <description>&lt;h2&gt;
  
  
  What are They?
&lt;/h2&gt;

&lt;p&gt;For those who don't already know, Jenkins and CircleCI are two out of the numerous continuous integration and continuous delivery (CI/CD) tools. Continuous Delivery (CD) is a software development technique where code changes are automatically prepared for a release to production. Continuous Integration (CI) is a technique for automating the integration of code changes from many developers into a single software project. Consequently, CI/CD may be described as a combination of ideas, methods, and tools that enables software updates of all kinds to reach users in a timely, effective, repeatable, and secure manner. It is a technique for regularly delivering apps to users by automating the various stages of app development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of CI/CD
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Detect Vulnerabilities in Security: security is a key concern in every industry. CI/CD immediately identifies any vulnerability in codes before they are deployed to production, as it even gives it more priority, thereby saving saving money by preventing humiliating and/or expensive security vulnerabilities&lt;/li&gt;
&lt;li&gt;Automated Rollback Triggered by Job Failure: There are occasions when a code will include problems after it has been deployed (put into use). Therefore, attempting to identify the error's cause can take minutes, hours, days, or even weeks. However, with CI/CD, there is a rapid way to return to the code's prior working state, protecting revenue and saving time&lt;/li&gt;
&lt;li&gt;Automate Infrastructure Creation: It automates the delivery of software or infrastructure-as-code from source code to production. This leads to less human error and faster deployments which eventually leads to avoidance of cost.&lt;/li&gt;
&lt;li&gt;Faster and More Frequent Production Deployments: A CI/CD procedure that runs without hiccups can enable numerous daily releases. Without much manual labour, teams may automatically create, test, and deliver features, hence new value-generating features get released more quickly and the efficiency of the team is released.&lt;/li&gt;
&lt;li&gt;Codes Get Deployed to Production Faster: Automating the integration and deployment of codes makes it possible to deploy code to the production state considerably, more quickly than with human checks. As a result, the development and operations teams spend less time reviewing and rechecking the code's quality, which boosts productivity and saves time, thereby boosting revenue&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  CI/CD Tools
&lt;/h2&gt;

&lt;p&gt;CI/CD tools include, but not limited to the following;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Jenkins&lt;/li&gt;
&lt;li&gt;CircleCI&lt;/li&gt;
&lt;li&gt;GitLab&lt;/li&gt;
&lt;li&gt;TravisCI&lt;/li&gt;
&lt;li&gt;TeamCity&lt;/li&gt;
&lt;li&gt;Bamboo&lt;/li&gt;
&lt;li&gt;Buddy&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Jenkins
&lt;/h2&gt;

&lt;p&gt;Jenkins provides a straightforward method for automating other common development chores as well as setting up a continuous integration or continuous delivery (CI/CD) system for virtually any collection of languages and source code repositories. Jenkins gives you a faster and more reliable means to integrate your full cycle of build, test, and deployment tools than you can easily develop yourself, even though it doesn't completely eliminate the need to write scripts for individual processes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages of Jenkins
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Easy Installation: Nobody enjoys going through a difficult installation. Jenkins is a standalone Java-based programme that comes with packages for Windows, Linux, macOS, and other Unix-like operating systems and may be used right away after installation.&lt;/li&gt;
&lt;li&gt;Easy Configuration: Jenkins' web interface enables straightforward setup and configuration thanks to its built-in tutorials and real-time error checking.&lt;/li&gt;
&lt;li&gt;Extensible: With a vast supply of community-contributed plugins, Jenkins is expandable and offers practically limitless potential uses.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  CircleCI
&lt;/h2&gt;

&lt;p&gt;CircleCI offers simple installation and upkeep without any problems. Because it is a cloud-based system, even for business customers, there is no requirement for a dedicated server and no need for server maintenance or administration with a free plan. CircleCI is compatible with GitHub, Amazon EC2, Appfog, dotCloud, and other platforms. &lt;/p&gt;

&lt;h3&gt;
  
  
  Advantages of CircleCI
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Allows developers to debug in the build using SSH: It can be difficult and time-consuming to debug code on resources and in settings other than a developer's typical development environment.&lt;/li&gt;
&lt;li&gt;Parallel builds are possible for rapid implementation of multiple tasks: Most frequently used build processes, including tests, are done sequentially, meaning that each step runs independently and only follows the one before it. The steps would be considered to be running concurrently or in parallel if we divided our lengthy single build process into several parts and executed them simultaneously.&lt;/li&gt;
&lt;li&gt;Allows Slack integration: Everyone on your team can stay informed about the status of your most recent builds thanks to CircleCI's chat alerts. When a build succeeds or fails, you can see which commit caused it as well as who was in charge of sending the code to GitHub. It's a terrific way to keep track of what your team members are working on as well as to identify and swiftly fix problematic builds.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Jenkins vs CircleCi
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open-Source vs Commercial:  CircleCI has a free version and premium plans, but Jenkins is an open-source technology.&lt;/li&gt;
&lt;li&gt;Complexity:  Because of its notoriously intricate configuration, Jenkins is best suited to seasoned developers. On the other hand, CircleCI is simpler to use and more user-friendly for beginners.&lt;/li&gt;
&lt;li&gt;Scalability: CircleCI is made to manage smaller projects, but Jenkins can handle large-scale projects.&lt;/li&gt;
&lt;li&gt;Community and Support: There is a sizable and vibrant community for Jenkins, and there are a tonne of plugins and resources available. Because it is a commercial solution, CircleCI offers official support and a smaller user base.&lt;/li&gt;
&lt;li&gt;Integrations: Both technologies support a variety of other programmes and infrastructure, including Github, Bitbucket, and AWS. In contrast to Jenkins, CircleCI provides a smaller selection of integrations.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;So, between CircleCI and Jenkins, which should one pick? Well, that depends on what you expect from a continuous integration tool in terms of convenience. We've seen the advantages and disadvantages of CircleCI and Jenkins; one can select a tool based on a project's needs, including its budget, timeline, and other factors. When selecting a CI/CD tool for your organisation, community support and resource availability are important considerations. Jenkins has a sizable community. On the other hand, CircleCI tries its best by offering comprehensive and useful content as well as events to address the majority of issues.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Deploying a Static Website on AWS</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Sun, 10 Jul 2022 12:56:53 +0000</pubDate>
      <link>https://dev.to/msoluwademilade/deploying-a-static-website-on-aws-4kj1</link>
      <guid>https://dev.to/msoluwademilade/deploying-a-static-website-on-aws-4kj1</guid>
      <description>&lt;p&gt;Static websites have predetermined content and can be created without the use of programming languages. It is built with HTML, CSS, and JavaScript and is the simplest type of website to develop. It consists of a number of HTML files, each of which represents a certain internet page physically.&lt;/p&gt;

&lt;p&gt;I've outlined below a step-by-step approach for deploying a static website using AWS, leveraging S3 Bucket and CloudFront.&lt;/p&gt;

&lt;h2&gt;
  
  
  I'll be breaking this into six steps.
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Creating the S3 bucket.&lt;/li&gt;
&lt;li&gt;Uploading the files to the created S3 bucket.&lt;/li&gt;
&lt;li&gt;Securing the bucket using IAM (Identity and Access Management).&lt;/li&gt;
&lt;li&gt;Configuring the S3 bucket.&lt;/li&gt;
&lt;li&gt;Distribute website using AWS CloudFront.&lt;/li&gt;
&lt;li&gt;Accessing website in web browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Creating the S3 Bucket
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; In the "Find Services" box, enter "S3," click it, and then select "Create Bucket."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwmyxtluihgloc8rp3rh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwmyxtluihgloc8rp3rh4.png" alt="Image description" width="800" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Your bucket name must be distinct for the name. It is suggested that you incorporate your 12-digit AWS account ID into the name of your bucket. Your bucket's name can be my-123456789012-bucket if your AWS account ID is 123456789012.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl77h1lm014zsjyyrqi4e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl77h1lm014zsjyyrqi4e.png" alt="Image description" width="799" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; In the "Bucket options for Block Public Access section," uncheck "Block all public access." The public will be able to access the bucket objects by using the S3 object URL. Which, in my opinion, is the main goal of having a website hosted in the first place.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkolnhm6bdmogvsml9qwv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkolnhm6bdmogvsml9qwv.png" alt="Image description" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; To create your S3 bucket, click "Next" and then "Create Bucket."&lt;/p&gt;

&lt;h2&gt;
  
  
  Uploading the Files to the Newly Created S3 Bucket.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Open the bucket and click on the 'Upload' button&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmjhk5fpj1vzflcrba4e6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmjhk5fpj1vzflcrba4e6.png" alt="Image description" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; To upload the files and folders of the website you wish to host from your local computer to the S3 bucket, click the "Add files" and "Add folder" buttons.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fknw4vfirpk24izxts9c3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fknw4vfirpk24izxts9c3.png" alt="Image description" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Click "Upload"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94ikjamwn7omw6r51yv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F94ikjamwn7omw6r51yv2.png" alt="Image description" width="800" height="907"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see this page after that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5ak1s7vp9g9tc8oa2s5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk5ak1s7vp9g9tc8oa2s5.png" alt="Image description" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then,&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrlgevu74qg8nddy998r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftrlgevu74qg8nddy998r.png" alt="Image description" width="800" height="725"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lastly,&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7tb24x3fw7ekk5dc5bt5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7tb24x3fw7ekk5dc5bt5.png" alt="Image description" width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing the bucket using IAM (Identity and Access Management).
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Open your S3 bucket and select the "Permissions" option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkj8mk48211hq200p81td.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkj8mk48211hq200p81td.png" alt="Image description" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Click "Edit" after scrolling down to "Bucket policy."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqe5k00f1fe9p2ymgwk9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqe5k00f1fe9p2ymgwk9.png" alt="Image description" width="800" height="639"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Enter the bucket policy listed below, which is written in the code block below. Keep in mind that you are substituting "my-123456789012-bucket" with the name of your bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
"Version":"2012-10-17",
"Statement":[
 {
   "Sid":"AddPerm",
   "Effect":"Allow",
   "Principal": "*",
   "Action":["s3:GetObject"],
   "Resource":["arn:aws:s3:::my-123456789012-bucket/*"]
 }
]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Click "Save changes"&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring the S3 Bucket.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Scroll down to the "Static website hosting" section of the "Properties" tab to make changes. It was the last item on the list as of the time of this documentation. When you do, select "Edit."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmnpr1i6e7afwdn6xmid.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbmnpr1i6e7afwdn6xmid.png" alt="Image description" width="800" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Enable hosting for static websites, then provide the name of your index and error document. Next, select "Save changes."&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frqqqawypqzbg5glb5oki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frqqqawypqzbg5glb5oki.png" alt="Image description" width="785" height="1151"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; For later usage, make a copy of the "Bucket website endpoint."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25ntbmx5bv1zjfrep4fd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25ntbmx5bv1zjfrep4fd.png" alt="Image description" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using AWS CloudFront, Distribute Website.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; In the text field labelled "Search for service, features, blogs, docs, and more", type "CloudFront."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa1cmhxl7aletkj0kykr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foa1cmhxl7aletkj0kykr.png" alt="Image description" width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Click "Create a CloudFront Distribution"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfcajz8lckf4b31oua9b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfcajz8lckf4b31oua9b.png" alt="Image description" width="800" height="123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; You now need to select the "Origin domain." NOTE: Don't make a selection from the drop-down menu. Instead, substitute the endpoint for hosting static websites in the format "[bucket-name].s3-website-region.amazonaws.com."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdwqf9fur38fdrcsf6zp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdwqf9fur38fdrcsf6zp.png" alt="Image description" width="758" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Click "Create Distribution" after changing the "Viewer protocol policy" to "Redirect HTTP to HTTPS."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdlqvyrlje43zat2q6yp5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdlqvyrlje43zat2q6yp5.png" alt="Image description" width="653" height="956"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessing Website in Web Browser.
&lt;/h2&gt;

&lt;p&gt;Your CloudFront domain name, S3 object URL, and bucket website-endpoint should all display the same index.html content as confirmation that you followed the correct procedure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CloudFront domain name&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvt7md8mmyjt5k42fidm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhvt7md8mmyjt5k42fidm.png" alt="Image description" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. S3 object URL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc2cf3a3a5hix57zrucs3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc2cf3a3a5hix57zrucs3.png" alt="Image description" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Bucket website-endpoint&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0dqzy7oa1rvncr235r5q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0dqzy7oa1rvncr235r5q.png" alt="Image description" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudcomputing</category>
    </item>
    <item>
      <title>Containerisation</title>
      <dc:creator>Oluwademilade Oyekanmi</dc:creator>
      <pubDate>Sun, 03 Jul 2022 22:57:15 +0000</pubDate>
      <link>https://dev.to/msoluwademilade/containerisation-1d67</link>
      <guid>https://dev.to/msoluwademilade/containerisation-1d67</guid>
      <description>&lt;p&gt;The process of containerisation comprises packing a software component, together with all of its dependencies, configuration, and environment, into a standalone container. This enables the uniform deployment of an application across all computing environments, including on-premises and cloud-based ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What is a Container?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A container is a standardised software component that wraps up code and all of its dependencies to ensure that an application will run swiftly and consistently in different computing environments.&lt;br&gt;
Anything from a small micro service or software process to a huge application could be operated inside of a single container. All required executables, binary code, libraries, and configuration files are contained inside a container. However, operating system images are not present in containers, unlike server or machine virtualisation methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Benefits of Containerisation&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Portability
There is a popular saying when it comes to containerisation “write once, run anywhere.” You may take your application almost anywhere without having to recompile it to take into account a different environment because a container bundles &lt;strong&gt;all&lt;/strong&gt; dependencies.&lt;/li&gt;
&lt;li&gt;Efficiency
One of the most effective virtualisation techniques for developers is containerisation. Efficiency by delivering greater computational resource utilisation and using significantly fewer resources than VMs. Applications may be scaled, patched, or deployed more quickly thanks to containers.&lt;/li&gt;
&lt;li&gt;Agility
Being agile means having the capacity to move swiftly. Containers may be quickly created, deployed to any environment, and utilised to address a wide range of DevOps concerns. The universality and usability of the development tools further encourages the quick creation, packaging, and deployment of containers across all operating systems.&lt;/li&gt;
&lt;li&gt;Greater speed
Containers aren't overwhelmed by extra overheads because they share a machine's operating system. With a slight increase in start-up speed, this lightweight construction improves server efficiency. The improved efficiency and performance also result in decreased server and licencing expenses.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Virtualisation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The process of creating a virtual version of something, such as an operating system, a server, a storage device, or network resources, as opposed to an actual one, is known as virtualisation. It most frequently refers to using many operating systems at once on a computer system. The operating system, libraries, and other programmes that make up the guest virtualisation system are distinct from the host operating system that runs below it, giving the impression to applications running on top of the virtualised machine that they are on their own dedicated computer.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Containerisation vs Virtualisation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KHKQeoDl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckuu8cwamrtyzea2m59j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KHKQeoDl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckuu8cwamrtyzea2m59j.png" alt="Image description" width="880" height="471"&gt;&lt;/a&gt;&lt;br&gt;
While virtualisation allows for total separation from the host operating system and other VMs, containerisation often offers minimal isolation from the host and other containers but lacks the same level of security as a virtual machine.&lt;/p&gt;

&lt;p&gt;With virtualisation, virtually any operating system can be run inside the virtual computer. But, containerisation utilises the same version of the operating system as the host.&lt;/p&gt;

&lt;p&gt;Virtualisation can imitate and represent your real hardware, such as CPU cores, memory, and discs, as a separate machine, while OS-level virtualisation is containerisation. As it only partially simulates the actual machine.&lt;/p&gt;

&lt;p&gt;Virtualisation is heavyweight, while containerisation is lightweight&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
