<?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: Shreyas Yadav</title>
    <description>The latest articles on DEV Community by Shreyas Yadav (@shreyas_yadav_e6fbf9ad3f6).</description>
    <link>https://dev.to/shreyas_yadav_e6fbf9ad3f6</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2755410%2F436c6739-5a53-4526-a270-b8006824466e.jpg</url>
      <title>DEV Community: Shreyas Yadav</title>
      <link>https://dev.to/shreyas_yadav_e6fbf9ad3f6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shreyas_yadav_e6fbf9ad3f6"/>
    <language>en</language>
    <item>
      <title>Adding MapReduce to My Go Distributed File System</title>
      <dc:creator>Shreyas Yadav</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:41:12 +0000</pubDate>
      <link>https://dev.to/shreyas_yadav_e6fbf9ad3f6/adding-mapreduce-to-my-go-distributed-file-system-2kd1</link>
      <guid>https://dev.to/shreyas_yadav_e6fbf9ad3f6/adding-mapreduce-to-my-go-distributed-file-system-2kd1</guid>
      <description>&lt;p&gt;I had already built a small HDFS-style distributed file system in Go. (&lt;a href="https://dev.to/shreyas_yadav_e6fbf9ad3f6/building-an-hdfs-style-distributed-file-system-in-go-48bj"&gt;Part I&lt;/a&gt;) It has a controller for metadata, storage nodes that hold replicated chunks, and a client. All three talk over TCP with Protocol Buffers. This post is about the next step: running MapReduce jobs on the data stored in it.&lt;/p&gt;

&lt;p&gt;Here's the design goal. The data is already split into chunks and spread across storage nodes, so the computation should go to the chunks, not the other way around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a course project, so the repository is private. If you’re interested, please comment below, and I’ll grant you viewer access.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The pieces
&lt;/h2&gt;

&lt;p&gt;MapReduce adds one new binary and extends two existing ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Computation manager&lt;/strong&gt; (new): accepts jobs, schedules map and reduce tasks, tracks progress, and merges the final output. It's a separate process from the controller and asks the controller for chunk placements with the same &lt;code&gt;GetFileRequest&lt;/code&gt; the client uses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage nodes&lt;/strong&gt; (extended): they now also run map tasks on their local chunks, receive shuffle data, and run reduce tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt; (extended): new &lt;code&gt;submitjob&lt;/code&gt; and &lt;code&gt;jobstatus&lt;/code&gt; commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of this uses the same framing as the file system: a length-prefixed protobuf &lt;code&gt;Wrapper&lt;/code&gt;, with new message types like &lt;code&gt;SubmitJobRequest&lt;/code&gt;, &lt;code&gt;MapTaskRequest&lt;/code&gt;, &lt;code&gt;ShuffleData&lt;/code&gt;, &lt;code&gt;ReduceTaskRequest&lt;/code&gt;, and &lt;code&gt;GetJobStatusResponse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Jobs are just executables. A mapper reads lines from stdin and writes &lt;code&gt;key&amp;lt;TAB&amp;gt;value&lt;/code&gt; lines to stdout. A reducer reads sorted &lt;code&gt;key&amp;lt;TAB&amp;gt;value&lt;/code&gt; lines and writes its results. The repo has three example jobs: word count, a log analyzer that counts log lines per domain, and a second-stage report job that reads that output and produces the top 10 domains. The mapper binary has to exist at the same path on every storage node, because each node runs it with &lt;code&gt;exec.Command(req.PluginPath)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: chunks that don't split lines
&lt;/h2&gt;

&lt;p&gt;The original file system split files at fixed byte offsets. That works for storing files but breaks text processing: a chunk boundary can land in the middle of a line, and then no mapper sees that line whole.&lt;/p&gt;

&lt;p&gt;So the client now checks the file type before chunking. It combines &lt;code&gt;http.DetectContentType&lt;/code&gt; on the first 8 KB with the file extension. Text-like types (&lt;code&gt;text/*&lt;/code&gt;, JSON, CSV, YAML, and a few others) go through &lt;code&gt;planLineChunks&lt;/code&gt;, which only cuts between lines. A single line longer than the chunk size gets a chunk of its own. Everything else is still split at fixed byte offsets.&lt;/p&gt;

&lt;p&gt;Each text chunk also records the line number it starts at. That number travels through &lt;code&gt;StoreFileRequest.chunk_start_lines&lt;/code&gt; into &lt;code&gt;ChunkPlacement.start_line&lt;/code&gt;. When a node runs a mapper, &lt;code&gt;formatMapInput&lt;/code&gt; prefixes every line with its line number in the whole file (&lt;code&gt;lineNumber&amp;lt;TAB&amp;gt;line&lt;/code&gt;). So a mapper sees the same line numbers it would see if it read the whole file in one pass.&lt;/p&gt;

&lt;p&gt;I also sped up uploads while I was changing the client: &lt;code&gt;storeChunksConcurrently&lt;/code&gt; now uploads with a pool of 8 workers (&lt;code&gt;storeWorkers&lt;/code&gt;) instead of sending one chunk at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Submitting a job
&lt;/h2&gt;

&lt;p&gt;The client sends &lt;code&gt;SubmitJobRequest&lt;/code&gt; with the mapper path, the input file, the number of reducers, and the reducer path. The manager fetches the placements for the input file, creates a &lt;code&gt;JobState&lt;/code&gt;, assigns an ID, and starts dispatching. A job moves through &lt;code&gt;queued&lt;/code&gt;, &lt;code&gt;running&lt;/code&gt;, &lt;code&gt;reducing&lt;/code&gt; (or &lt;code&gt;merging&lt;/code&gt; for map-only jobs), and ends as &lt;code&gt;complete&lt;/code&gt; or &lt;code&gt;failed&lt;/code&gt;. The client polls with &lt;code&gt;jobstatus&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reduce tasks are dispatched &lt;strong&gt;before&lt;/strong&gt; map tasks, so each reducer is already waiting when mapper output starts arriving. &lt;code&gt;selectReducerNodes&lt;/code&gt; assigns reducers round-robin over the primary node of each chunk.&lt;/p&gt;

&lt;p&gt;For each map task, &lt;code&gt;pickNode&lt;/code&gt; picks the replica of that chunk with the fewest in-flight map tasks (tracked in &lt;code&gt;nodeActiveTasks&lt;/code&gt;). Each map task runs on a node that already has the chunk on local disk, so no input data moves over the network.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map side: spill, sort, merge
&lt;/h2&gt;

&lt;p&gt;On the storage node, &lt;code&gt;executeMapTask&lt;/code&gt; reads the chunk from local disk, verifies its SHA-256 checksum, formats the input, and starts the mapper process. Mapper output can be much larger than memory, so it isn't collected in a slice. &lt;code&gt;spillMapperOutput&lt;/code&gt; reads stdout line by line, buffers up to &lt;code&gt;mapSpillMaxLines&lt;/code&gt; (50,000) lines, sorts them, and writes each batch to a &lt;code&gt;spill_NNNN.run&lt;/code&gt; file. Every line must contain a tab, or the task fails.&lt;/p&gt;

&lt;p&gt;When the mapper exits, the sorted runs are merged with a k-way merge over a min-heap. The heap orders by key, then by the full line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lineHeapItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;advance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EOF&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;heap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&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;p&gt;&lt;code&gt;emit&lt;/code&gt; does two things at once. It writes the line to the node's local map output file, and it partitions the pair to a reducer by hashing the key with FNV-1a:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;emit&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteByte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;senders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;parseKVLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;reducerID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fnv32a&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="kt"&gt;uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;senders&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;senders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;reducerID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&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;p&gt;Each &lt;code&gt;shuffleSender&lt;/code&gt; holds one open TCP connection to its reducer's node. It batches pairs and sends a &lt;code&gt;ShuffleData&lt;/code&gt; message every &lt;code&gt;shuffleBatchMaxKVs&lt;/code&gt; (2,048) pairs. At the end it sends a final message with &lt;code&gt;done: true&lt;/code&gt;. The merge output is sorted, so every batch a reducer receives is already sorted too.&lt;/p&gt;

&lt;p&gt;If a job has zero reducers, there's no shuffle. Each map task uploads its output to the DFS as &lt;code&gt;__mr_tmp_&amp;lt;job&amp;gt;_map_chunk_&amp;lt;i&amp;gt;.out&lt;/code&gt;, and the manager concatenates those files in chunk order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reduce side: waiting for every mapper
&lt;/h2&gt;

&lt;p&gt;This part has a race to handle. A reducer needs to know how many mappers to wait for, and that number comes in &lt;code&gt;ReduceTaskRequest.num_mappers&lt;/code&gt;. But shuffle data from a fast mapper can arrive at the node before the &lt;code&gt;ReduceTaskRequest&lt;/code&gt; does.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;reducerBuffer&lt;/code&gt; handles both orders. If shuffle data arrives first, the buffer is created with &lt;code&gt;numMappers = -1&lt;/code&gt; (unknown). Each non-empty batch is written straight to disk as its own sorted run file, and each &lt;code&gt;done&lt;/code&gt; message increments a counter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;StorageNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;acceptShuffleData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShuffleData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getOrCreateReducerBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReducerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;runPath&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reducerRunPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReducerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nextRunID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nextRunID&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writeShuffleRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runFiles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runPath&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="n"&gt;sd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doneCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numMappers&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doneCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numMappers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;once&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the &lt;code&gt;ReduceTaskRequest&lt;/code&gt; arrives, &lt;code&gt;getOrCreateReducerBuffer&lt;/code&gt; fills in &lt;code&gt;numMappers&lt;/code&gt; and runs the same check, so the &lt;code&gt;ready&lt;/code&gt; channel closes whichever message comes last. &lt;code&gt;sync.Once&lt;/code&gt; makes sure it only closes once. &lt;code&gt;executeReduceTask&lt;/code&gt; blocks on &lt;code&gt;&amp;lt;-buf.ready&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After that, the reducer reuses &lt;code&gt;mergeSortedRuns&lt;/code&gt; from the map side and streams the merged lines directly into the reducer process's stdin. The reducer sees every value for a key together, in sorted order, without the node ever loading the whole partition into memory. The reducer's stdout is uploaded to the DFS as &lt;code&gt;__mr_tmp_&amp;lt;job&amp;gt;_reducer_&amp;lt;id&amp;gt;.out&lt;/code&gt;. Once every reducer reports success, the manager concatenates the partitions in reducer-ID order into &lt;code&gt;&amp;lt;job&amp;gt;_output.out&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling failed and hung map tasks
&lt;/h2&gt;

&lt;p&gt;A map task can fail in two ways, and the manager handles each one differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It reports failure.&lt;/strong&gt; The node sends back &lt;code&gt;MapTaskResponse{success: false}&lt;/code&gt;. The manager moves to the next replica in that chunk's placement and dispatches there right away. Once no replicas are left, the job fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It never answers.&lt;/strong&gt; The node accepts the task and then dies or hangs. &lt;code&gt;watchJobTimeout&lt;/code&gt; checks every 10 seconds for chunks that have been running longer than the timeout and re-dispatches them to the next replica. The timeout adapts to how fast tasks are actually finishing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fallbackTimeout&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MapsTotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MapsDone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MapsTotal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completionTimes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completionTimes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completionTimes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;adaptiveTimeout&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adaptiveMultiplier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;avg&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;adaptiveTimeout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;minAdaptiveTimeout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;adaptiveTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;minAdaptiveTimeout&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;adaptiveTimeout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adaptiveTimeout&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;p&gt;Until 70% of map tasks have finished, the timeout is the fallback of 15 minutes. After that, it's twice the average completion time, but never less than 3 minutes and never more than 15. Early in a job there's no data about how long a task normally takes, so a fixed 3-minute timeout would kill slow but healthy tasks. With enough completions to estimate from, a stuck task gets retried much sooner than the 15-minute fallback. The most recent change to this code made the adaptation more conservative.&lt;/p&gt;

&lt;p&gt;If the original task and its retry both finish, &lt;code&gt;completedChunks&lt;/code&gt; counts the chunk only once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd change next
&lt;/h2&gt;

&lt;p&gt;Some limitations I know about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shuffle data isn't tied to a map attempt.&lt;/strong&gt; &lt;code&gt;ShuffleData&lt;/code&gt; carries &lt;code&gt;job_id&lt;/code&gt; and &lt;code&gt;reducer_id&lt;/code&gt;, but no chunk index or attempt number. If a map task sends part of its shuffle and then fails, or if a timed-out task and its retry both finish, the reducer can receive duplicate pairs, and the extra &lt;code&gt;done&lt;/code&gt; messages can make it start before the real last mapper finishes. Tagging each stream with its chunk index, and counting &lt;code&gt;done&lt;/code&gt; messages per chunk, would fix this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries can go back to the same node.&lt;/strong&gt; Retries walk the placement list starting at index 1, whichever replica &lt;code&gt;pickNode&lt;/code&gt; actually chose. If &lt;code&gt;pickNode&lt;/code&gt; picked &lt;code&gt;Nodes[1]&lt;/code&gt; and that task fails, the first retry goes to the same node again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reducers aren't retried.&lt;/strong&gt; One failed reduce task fails the whole job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The final merge happens in memory.&lt;/strong&gt; The manager downloads every partition into one byte slice and uploads the result as a single chunk with replication factor 1. That limits output size to the manager's memory, and the output has no replicas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing gets cleaned up.&lt;/strong&gt; Spill files, shuffle runs, &lt;code&gt;__mr_tmp_&lt;/code&gt; files in the DFS, and the per-job reducer buffers all stay around after a job finishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job state lives in the manager's memory only.&lt;/strong&gt; If the manager restarts, every job in flight is lost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ideas I learned the most from were external sorting with spill files and a heap merge, and handling shuffle data that shows up before the reduce task does. Scheduling map tasks on the nodes that already hold the chunk was the easy part, because the file system already tracked where every chunk lives.&lt;/p&gt;

</description>
      <category>go</category>
      <category>distributedsystems</category>
      <category>mapreduce</category>
      <category>backend</category>
    </item>
    <item>
      <title>Building an HDFS-Style Distributed File System in Go</title>
      <dc:creator>Shreyas Yadav</dc:creator>
      <pubDate>Tue, 29 Sep 2026 22:23:51 +0000</pubDate>
      <link>https://dev.to/shreyas_yadav_e6fbf9ad3f6/building-an-hdfs-style-distributed-file-system-in-go-48bj</link>
      <guid>https://dev.to/shreyas_yadav_e6fbf9ad3f6/building-an-hdfs-style-distributed-file-system-in-go-48bj</guid>
      <description>&lt;p&gt;I built a small distributed file system in Go, modeled loosely on HDFS. It has three binaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;controller&lt;/strong&gt; that owns metadata: which nodes are alive and which nodes hold which chunks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;storage nodes&lt;/strong&gt; that keep chunk bytes on local disk&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;client&lt;/strong&gt; with &lt;code&gt;store&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;getmeta&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, and &lt;code&gt;nodes&lt;/code&gt; commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything talks over plain TCP using Protocol Buffers. There is no gRPC and no external coordination service. This post walks through how the pieces fit together and the failure cases I had to handle.&lt;/p&gt;

&lt;p&gt;The source code is on GitHub: &lt;a href="https://github.com/Shreyas-Yadav/hdfs-lite" rel="noopener noreferrer"&gt;Shreyas-Yadav/hdfs-lite&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wire protocol: one envelope, length-prefixed
&lt;/h2&gt;

&lt;p&gt;Every message type is a field in a single &lt;code&gt;oneof&lt;/code&gt; called &lt;code&gt;Wrapper&lt;/code&gt;, so each connection carries one kind of thing: a &lt;code&gt;Wrapper&lt;/code&gt;. That covers &lt;code&gt;RegisterNode&lt;/code&gt;, &lt;code&gt;StoreChunkRequest&lt;/code&gt;, &lt;code&gt;Heartbeat&lt;/code&gt;, &lt;code&gt;ReplicateChunkRequest&lt;/code&gt;, &lt;code&gt;CommitFileRequest&lt;/code&gt;, and the rest.&lt;/p&gt;

&lt;p&gt;TCP is a byte stream, so each message needs a boundary. &lt;code&gt;MessageHandler&lt;/code&gt; writes a 4-byte big-endian length followed by the protobuf body. On the read side, &lt;code&gt;ReadMsg&lt;/code&gt; reads 4 bytes, then reads exactly that many bytes with &lt;code&gt;io.ReadFull&lt;/code&gt;, then unmarshals. Callers just call &lt;code&gt;Send(m)&lt;/code&gt;, which type-switches on the message and wraps it. On the receive side they use typed helpers like &lt;code&gt;ReceiveStoreChunkResponse()&lt;/code&gt;, built on a small generic &lt;code&gt;receiveTyped[T]&lt;/code&gt;, which return an error if a different message type shows up.&lt;/p&gt;

&lt;p&gt;Each server accepts connections in a loop and starts one goroutine per connection. That goroutine loops over &lt;code&gt;ReadMsg&lt;/code&gt; and dispatches on &lt;code&gt;msg.Msg.(type)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Joining the cluster and staying alive
&lt;/h2&gt;

&lt;p&gt;On startup, a storage node binds its listening port first and registers with the controller only after that, so it never announces an address it can't serve. The node ID is simply &lt;code&gt;host:port&lt;/code&gt;. After registering, the node keeps the TCP connection to the controller open and sends a &lt;code&gt;Heartbeat&lt;/code&gt; over it every 5 seconds.&lt;/p&gt;

&lt;p&gt;Each heartbeat carries the node's free space (from &lt;code&gt;syscall.Statfs&lt;/code&gt;), a count of requests handled, and a &lt;code&gt;new_chunks&lt;/code&gt; list of chunks stored since the last beat. If a heartbeat fails, &lt;code&gt;reconnectLoop&lt;/code&gt; tries to re-register. It waits 5 seconds first and doubles the wait after each failure. It stops doubling once the wait reaches the 60-second &lt;code&gt;maxBackoff&lt;/code&gt;, so the longest wait in practice is 80 seconds.&lt;/p&gt;

&lt;p&gt;On the controller, &lt;code&gt;monitorNodes&lt;/code&gt; runs every &lt;code&gt;monitorInterval&lt;/code&gt; (5s). Any node that hasn't sent a heartbeat within &lt;code&gt;nodeFailureTimeout&lt;/code&gt; (15s, about three missed beats) is marked dead, and that triggers a replication repair pass. If a dead node later sends another heartbeat, the controller clears the dead flag and starts another repair pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Placing chunks
&lt;/h2&gt;

&lt;p&gt;The client splits a file into fixed-size chunks and sends a &lt;code&gt;StoreFileRequest&lt;/code&gt;. The replication factor defaults to 3. The controller rejects the request if the file already exists or if fewer live nodes are available than the replication factor requires.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;selectNodes&lt;/code&gt; handles placement with an offset round-robin over the live nodes, sorted by ID. The node list for chunk &lt;code&gt;i&lt;/code&gt; starts at index &lt;code&gt;i&lt;/code&gt;, so the primary role rotates across nodes and the write load is spread. The first node in each placement is the primary and the rest are replicas. This scheme ignores free space. Free space only matters later, when the controller picks a node to repair onto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing: pipeline replication
&lt;/h2&gt;

&lt;p&gt;The client doesn't send each chunk three times. It sends each chunk once, to the primary, with the rest of the chain in &lt;code&gt;Replicas&lt;/code&gt; and a SHA-256 checksum of the data. The primary:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;checks that the checksum matches the received bytes&lt;/li&gt;
&lt;li&gt;writes the chunk plus a &lt;code&gt;.checksum&lt;/code&gt; sidecar file to disk&lt;/li&gt;
&lt;li&gt;forwards the chunk to &lt;code&gt;Replicas[0]&lt;/code&gt; and passes along only &lt;code&gt;Replicas[1:]&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each hop waits for the next hop's ack before it acks upstream. So when the client gets &lt;code&gt;ok&lt;/code&gt; from the primary, every node in the chain has already written the chunk. If a forward fails, the node deletes its own copy before reporting failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Replicas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;next&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Replicas&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;forwardChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rbErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleteChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChunkIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;rbErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rollback failed for chunk %d of %s: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChunkIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rbErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoreChunkResponse&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Ok&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pipeline forward to %s failed: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;continue&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;h2&gt;
  
  
  Committing: no half-written files in the namespace
&lt;/h2&gt;

&lt;p&gt;When the controller receives a &lt;code&gt;StoreFileRequest&lt;/code&gt;, it puts the placement plan in &lt;code&gt;pendingFiles&lt;/code&gt;, not &lt;code&gt;files&lt;/code&gt;. The client uploads every chunk and then sends &lt;code&gt;CommitFileRequest&lt;/code&gt; on the same controller connection. Only then does &lt;code&gt;commitFile&lt;/code&gt; move the record into &lt;code&gt;files&lt;/code&gt; and write state to disk.&lt;/p&gt;

&lt;p&gt;If the client dies partway through, its controller connection closes. A deferred block in the connection handler sees a pending filename that never got committed and calls &lt;code&gt;abortPending&lt;/code&gt;. That removes the record and asks every planned node to delete its chunk. On storage nodes, deleting a chunk that doesn't exist counts as success, so cleaning up a partial upload is safe.&lt;/p&gt;

&lt;p&gt;Only committed files are persisted, as JSON in &lt;code&gt;controller_state.json&lt;/code&gt;. Node membership is not persisted. The controller rebuilds it as nodes register and send heartbeats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading: parallel fetch, positional writes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;getFile&lt;/code&gt; gets the placements from the controller, creates the output file, and truncates it to its full size up front. It then fetches chunks in parallel. A buffered channel caps the fetches at 8 goroutines at a time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitGroup&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;firstErr&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;errMu&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;placement&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;placements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChunkPlacement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;wg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt;

        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fetchChunkWithFallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each goroutine writes its chunk with &lt;code&gt;WriteAt&lt;/code&gt; at offset &lt;code&gt;ChunkIndex * ChunkSize&lt;/code&gt;. Chunks can finish in any order, and the file is never assembled in memory. &lt;code&gt;fetchChunkWithFallback&lt;/code&gt; tries each replica in placement order and checks the SHA-256 of what it receives. If the data was corrupted in transit, or any other error occurs, it moves on to the next replica.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-healing reads on the storage node
&lt;/h2&gt;

&lt;p&gt;Checksums are also checked on disk. When a storage node serves a &lt;code&gt;GetChunkRequest&lt;/code&gt;, it compares the chunk against its &lt;code&gt;.checksum&lt;/code&gt; sidecar. If the chunk is missing or the checksum doesn't match, the node doesn't just return an error. It calls &lt;code&gt;repairChunkFromReplicas&lt;/code&gt;, which fetches a good copy from another replica, stores that copy locally, and then serves it.&lt;/p&gt;

&lt;p&gt;Replica A might fetch from B while B's copy is also bad, and B would then try to repair from A. &lt;code&gt;GetChunkRequest.avoid_node_ids&lt;/code&gt; prevents that loop. Each node adds itself to the list before asking the next one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;avoidSet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
&lt;span class="n"&gt;nextAvoidNodeIDs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;avoidNodeIDs&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodeID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;placement&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;placements&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;placement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChunkIndex&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;chunkIndex&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;placement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nodes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;avoidSet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NodeId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fetchReplicaChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunkIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replica&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextAvoidNodeIDs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a result, a client read also repairs a corrupted local copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-replication after a node dies
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;reconcileUnderReplicated&lt;/code&gt; brings chunks back up to their target replica count. It works in three phases so the controller mutex is never held during network calls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Step A (under lock):&lt;/strong&gt; Find every chunk with fewer live replicas than its target and queue the repair work: a healthy source, a replacement node, and the dead node to replace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step B (no lock):&lt;/strong&gt; For each work item, send a &lt;code&gt;ReplicateChunkRequest&lt;/code&gt; to the source storage node. The source verifies its own checksum and pushes the chunk straight to the destination. Chunk bytes never pass through the controller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step C (under lock):&lt;/strong&gt; Swap the replacement into the placement and save state once at the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;pickReplacementNode&lt;/code&gt; picks the live node with the most free space that doesn't already hold the chunk. If no spare node exists, the chunk stays marked under-replicated and a later pass retries it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the 3-minute grace period
&lt;/h3&gt;

&lt;p&gt;This one came from a real failure mode. When the controller restarts, it reloads every file's placements, but it doesn't know about any nodes yet. If it ran a repair pass right away, every chunk would look under-replicated, and the controller would start copying data around the cluster for no reason.&lt;/p&gt;

&lt;p&gt;So the first pass waits for &lt;code&gt;startupGracePeriod&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monitorNodes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;startupGracePeriod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconcileUnderReplicated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"controller startup"&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;p&gt;&lt;code&gt;startupGracePeriod&lt;/code&gt; is 3 minutes. The repair triggers on node registration and on node recovery are also skipped during this window. That gives storage nodes, which retry on their own backoff, time to reconnect before the controller makes any replication decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deletes
&lt;/h2&gt;

&lt;p&gt;The client sends &lt;code&gt;DeleteFileRequest&lt;/code&gt; to the controller. The controller asks every replica to delete its chunk, then drops the metadata. If some replicas can't be reached, the delete still succeeds from the client's point of view, and the response message says how many replica deletes couldn't be confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd change next
&lt;/h2&gt;

&lt;p&gt;Some limitations I know about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The controller is a single point of failure.&lt;/strong&gt; Metadata goes to one local JSON file, and &lt;code&gt;saveState&lt;/code&gt; writes it with &lt;code&gt;os.WriteFile&lt;/code&gt; directly, not a write-to-temp-then-rename. A crash during the write could corrupt the state file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uploads are sequential per chunk.&lt;/strong&gt; Reads run 8 at a time, but &lt;code&gt;storeFile&lt;/code&gt; reads and pushes chunks one after another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;new_chunks&lt;/code&gt; is reported but barely used.&lt;/strong&gt; Storage nodes send it in every heartbeat, but the controller only logs it. The controller never compares its placement map against what nodes actually have on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Initial placement ignores capacity.&lt;/strong&gt; Round-robin spreads load evenly, but a nearly full node gets as many new chunks as an empty one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The liveness window is duplicated.&lt;/strong&gt; Two functions hard-code &lt;code&gt;15*time.Second&lt;/code&gt; instead of using &lt;code&gt;nodeFailureTimeout&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ideas I learned the most from were pipeline replication with rollback, commit-after-upload, and checksum-verified reads that repair the bad copy. None of them took much code, but each one closed a failure case that I only understood after it had happened.&lt;/p&gt;

</description>
      <category>go</category>
      <category>distributedsystems</category>
      <category>backend</category>
      <category>filesystem</category>
    </item>
    <item>
      <title>Implementing Strict Priority and Deficit Round Robin Schedulers in NS-3</title>
      <dc:creator>Shreyas Yadav</dc:creator>
      <pubDate>Tue, 29 Sep 2026 21:10:11 +0000</pubDate>
      <link>https://dev.to/shreyas_yadav_e6fbf9ad3f6/implementing-strict-priority-and-deficit-round-robin-schedulers-in-ns-3-5fij</link>
      <guid>https://dev.to/shreyas_yadav_e6fbf9ad3f6/implementing-strict-priority-and-deficit-round-robin-schedulers-in-ns-3-5fij</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; NS-3 · C++ · nlohmann/json · Wireshark&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a router's outgoing link is slower than the traffic arriving at it, packets wait in a queue, and the router has to decide which packet goes next. That decision is what Quality of Service (QoS) is about. I implemented two classic answers to that question in NS-3: &lt;strong&gt;Strict Priority Queueing (SPQ)&lt;/strong&gt; and &lt;strong&gt;Deficit Round Robin (DRR)&lt;/strong&gt;. Both plug into a small Differentiated Services (DiffServ) framework that I wrote, and both are configured from JSON.&lt;/p&gt;

&lt;p&gt;This post covers the design, one classification bug that is easy to hit in NS-3, the results in Wireshark, and what I would do differently.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/Shreyas-Yadav/QoS-NS3" rel="noopener noreferrer"&gt;github.com/Shreyas-Yadav/QoS-NS3&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup: Creating a Bottleneck
&lt;/h2&gt;

&lt;p&gt;The topology is three nodes in a line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client ──(4 Mbps, 10 ms)──&amp;gt; Router ──(1 Mbps, 10 ms)──&amp;gt; Server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each UDP client sends a 1000-byte packet every 2 ms, which is about 4 Mbps per flow. The router can only forward 1 Mbps, so its egress queue fills up quickly. That queue is where the scheduler runs. I replaced the router's default queue on the server-facing device with my own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;Ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PointToPointNetDevice&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;routerEgress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;m_routerNode&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PointToPointNetDevice&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;routerEgress&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;SetQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spq&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// or drr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I capture PCAP traces on both sides of the router (&lt;code&gt;Pre_*&lt;/code&gt; and &lt;code&gt;Post_*&lt;/code&gt;) so I can compare the traffic before and after scheduling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Design
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ucwbwe1m3dhoftjhv9z.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ucwbwe1m3dhoftjhv9z.jpg" alt="Flowchart of a packet moving through DiffServ classification, the SPQ or DRR scheduler, and router transmission" width="800" height="1136"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;How a packet moves through classification, the SPQ or DRR scheduler, and out of the router.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Classification: Filters and Traffic Classes
&lt;/h3&gt;

&lt;p&gt;Every packet must be placed in a traffic class before a scheduler can pick from it. The classification logic has three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FilterElement&lt;/strong&gt; checks a single field: source or destination IP, subnet mask, source or destination port, or protocol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter&lt;/strong&gt; combines elements with &lt;strong&gt;AND&lt;/strong&gt; logic, so all of them must match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TrafficClass&lt;/strong&gt; holds a list of filters combined with &lt;strong&gt;OR&lt;/strong&gt; logic, plus its own queue, packet limit, and priority or weight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If no class matches a packet, it goes to the class marked &lt;code&gt;Default&lt;/code&gt;. If there is no default class, the packet is dropped.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Scheduler Interface
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;DiffServ&lt;/code&gt; is the base class. It handles enqueue (classify, then push to the matching class) and dequeue. Subclasses only implement one method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="n"&gt;Ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Packet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Schedule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Schedule()&lt;/code&gt; returns the packet that should go next, and the base class removes it from the correct queue. SPQ and DRR differ only in how they implement this one method.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON Configuration
&lt;/h3&gt;

&lt;p&gt;The scheduler type and the traffic classes come from a config file:&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"queues"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MaxPackets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"DestPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9000&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;span class="nl"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MaxPackets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"DestPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10000&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;span class="nl"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"MaxPackets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"DestPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11000&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;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;h2&gt;
  
  
  A Gotcha: The PPP Header
&lt;/h2&gt;

&lt;p&gt;My first version of the port filter matched nothing. At the point where a packet sits in a point-to-point device's queue, NS-3 has already added a &lt;strong&gt;PPP header&lt;/strong&gt; in front of the IPv4 header. You have to remove that header before you can read anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;Ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Packet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkt&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// never modify the real packet&lt;/span&gt;

&lt;span class="n"&gt;PppHeader&lt;/span&gt; &lt;span class="n"&gt;ppp&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="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;RemoveHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ppp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Ipv4Header&lt;/span&gt; &lt;span class="n"&gt;ip&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="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;RemoveHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;false&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="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetProtocol&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;     &lt;span class="c1"&gt;// UDP&lt;/span&gt;
    &lt;span class="n"&gt;UdpHeader&lt;/span&gt; &lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;PeekHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;udp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetDestinationPort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;m_port&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;p&gt;Working on a copy matters too. &lt;code&gt;RemoveHeader&lt;/code&gt; changes the packet, and the real packet still has to leave the router with all its headers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strict Priority Queueing
&lt;/h2&gt;

&lt;p&gt;SPQ is simple: always serve the highest-priority queue that has packets. A lower priority number means a higher priority.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;classes&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="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IsEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetPriority&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;highestPriority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;highestPriority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetPriority&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="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;nullptr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test scenario:&lt;/strong&gt; the low-priority flow (port 10001) starts at 0 s. The high-priority flow (port 10000) starts at 15 s.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foeixeynqh53sx9abfeym.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foeixeynqh53sx9abfeym.jpg" alt="Wireshark I/O graph before the router: the low-priority and high-priority flows share the 4 Mbps link equally after 15 seconds" width="800" height="455"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Before the router (4 Mbps link). Red: low priority (port 10001). Green: high priority (port 10000). The two flows share the link equally.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr89t4tpoc2uc6vao5909.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr89t4tpoc2uc6vao5909.jpg" alt="Wireshark I/O graph after the router: the low-priority flow drops to zero while the high-priority flow uses the full 1 Mbps link" width="800" height="455"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After the router (1 Mbps bottleneck). While the high-priority queue has packets, the low-priority flow gets nothing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After the router, the low-priority flow uses the full link (about 120 packets/s) until 15 s. When the high-priority flow starts, the low-priority flow drops to &lt;strong&gt;zero&lt;/strong&gt;. It gets no bandwidth until the high-priority queue is empty, and then it recovers.&lt;/p&gt;

&lt;p&gt;This is exactly what SPQ promises, and it also shows SPQ's weakness: &lt;strong&gt;starvation&lt;/strong&gt;. A heavy high-priority flow can block everything else indefinitely.&lt;/p&gt;


&lt;h2&gt;
  
  
  Deficit Round Robin
&lt;/h2&gt;

&lt;p&gt;DRR shares bandwidth in proportion to weights instead. Each queue has a credit counter. On each visit, the queue earns credit equal to its weight. If its credit covers the size of the packet at the front, it sends that packet and pays for it. Otherwise, the scheduler moves on to the next queue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;true&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="n"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;IsEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;credit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetWeight&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;GetSize&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="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;credit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;credit&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;Peek&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="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;queues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&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;
  
  
  Peeking Must Not Spend Credit
&lt;/h3&gt;

&lt;p&gt;NS-3 calls &lt;code&gt;Peek()&lt;/code&gt; on a queue separately from &lt;code&gt;Dequeue()&lt;/code&gt;. If &lt;code&gt;Schedule()&lt;/code&gt; updated the real credit counters, a single peek would spend credit for a packet that was never sent.&lt;/p&gt;

&lt;p&gt;To avoid this, &lt;code&gt;Schedule()&lt;/code&gt; works on &lt;strong&gt;temporary&lt;/strong&gt; state (&lt;code&gt;m_tempCreditBalance&lt;/code&gt;, &lt;code&gt;m_scheduledQueueIndex&lt;/code&gt;). &lt;code&gt;Dequeue()&lt;/code&gt; copies that state into the real counters only after a packet actually leaves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;Ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Packet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DRR&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Dequeue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Ptr&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Packet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DiffServ&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Dequeue&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="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;m_currentQueueIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m_scheduledQueueIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;m_creditBalance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m_tempCreditBalance&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="n"&gt;p&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;p&gt;&lt;strong&gt;Test scenario:&lt;/strong&gt; three flows with weights 10, 20, and 30, all starting at 0 s.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8au6hntsezgsphtf36jx.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8au6hntsezgsphtf36jx.jpg" alt="Wireshark I/O graph before the router: three UDP flows arriving at the same rate" width="800" height="455"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Before the router. All three flows arrive at the same rate.&lt;/em&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsbpgvo91nrz6cbrl0de.jpg" 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhsbpgvo91nrz6cbrl0de.jpg" alt="Wireshark I/O graph after the router: three flows at about 20, 40, and 60 packets per second, matching DRR weights of 10, 20, and 30" width="800" height="455"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After the router. Blue: weight 10. Teal: weight 20. Yellow: weight 30.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After the router, the flows settle at about &lt;strong&gt;20, 40, and 60 packets/s&lt;/strong&gt;, which is the 1:2:3 ratio of the weights.&lt;/p&gt;

&lt;p&gt;The graph also shows DRR's other property. When the weight-30 flow's queue empties at about 22 s, its share is &lt;strong&gt;redistributed&lt;/strong&gt; to the remaining flows. They move to about 40 and 80 packets/s, which keeps their 1:2 ratio. When the weight-20 queue empties at about 27 s, the last flow gets the full link. DRR never leaves the link idle while there are packets waiting.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I Would Change
&lt;/h2&gt;

&lt;p&gt;Looking back at the code, these are the things I would fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The quantum is smaller than a packet.&lt;/strong&gt; The weights (10–30) are added as bytes of credit, but packets are about 1000 bytes. The DRR loop therefore goes around dozens of times before any queue can send. The ratio is still correct, but the loop wastes work. In textbook DRR, the quantum is at least one MTU, so a queue can send on every visit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit is not reset when a queue empties.&lt;/strong&gt; In standard DRR, an empty queue loses its leftover credit, so it can't save up credit and send a burst later. My implementation keeps the credit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue limits count packets instead of bytes.&lt;/strong&gt; A queue full of small packets and a queue full of large packets hold very different amounts of data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classification assumes IPv4 over PPP.&lt;/strong&gt; Any other link layer or IPv6 would not match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The config file has no schema validation.&lt;/strong&gt; A typo in a field name silently falls back to a default value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DiffServ&lt;/code&gt; does too much.&lt;/strong&gt; Classification, scheduling, and queue storage all live in one class. Splitting them into a Classifier, a Scheduler, and a Queue Manager would make each part easier to test and replace.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Run It Yourself
&lt;/h2&gt;

&lt;p&gt;Copy the source files into &lt;code&gt;scratch/final-project/&lt;/code&gt; in an NS-3 (3.36 or later) installation, install &lt;code&gt;nlohmann-json3-dev&lt;/code&gt;, and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./ns3 run scratch/final-project/driver.cc &lt;span class="nt"&gt;--&lt;/span&gt; scratch/final-project/spq_config.json
./ns3 run scratch/final-project/driver.cc &lt;span class="nt"&gt;--&lt;/span&gt; scratch/final-project/drr_config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the PCAP files in Wireshark, go to &lt;strong&gt;Statistics → I/O Graph&lt;/strong&gt;, and add one line per flow with a filter such as &lt;code&gt;udp.dstport == 10000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The full code is on &lt;a href="https://github.com/Shreyas-Yadav/QoS-NS3" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>cpp</category>
      <category>ns3</category>
      <category>qualityofservice</category>
    </item>
    <item>
      <title>I Built a Nightly Pipeline That Deploys My App While I Sleep</title>
      <dc:creator>Shreyas Yadav</dc:creator>
      <pubDate>Mon, 09 Mar 2026 04:07:07 +0000</pubDate>
      <link>https://dev.to/shreyas_yadav_e6fbf9ad3f6/i-built-a-nightly-pipeline-that-deploys-my-app-while-i-sleep-3lh4</link>
      <guid>https://dev.to/shreyas_yadav_e6fbf9ad3f6/i-built-a-nightly-pipeline-that-deploys-my-app-while-i-sleep-3lh4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Next.js 14 · Express.js · MySQL 8 · Docker · GitHub Actions · AWS EC2 · AWS ECR · AWS RDS · Route 53 · Nginx · Let's Encrypt&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built a &lt;strong&gt;Job Application Tracker&lt;/strong&gt;, a full-stack SPA with GitHub OAuth login, and set up an automated nightly pipeline that builds Docker images, runs smoke tests on a temporary EC2, pushes verified images to ECR, and deploys to a persistent QA server, all without manual intervention. Here's exactly how I did it.&lt;/p&gt;




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

&lt;ol&gt;
&lt;li&gt;Architecture Overview&lt;/li&gt;
&lt;li&gt;The Application: Job Application Tracker&lt;/li&gt;
&lt;li&gt;Dockerizing the App&lt;/li&gt;
&lt;li&gt;Local Development with Docker Compose&lt;/li&gt;
&lt;li&gt;AWS Infrastructure Setup&lt;/li&gt;
&lt;li&gt;GitHub Actions CI/CD Pipeline&lt;/li&gt;
&lt;li&gt;Domain Name with Route 53 and SSL with Let's Encrypt&lt;/li&gt;
&lt;li&gt;Nginx as a Reverse Proxy&lt;/li&gt;
&lt;li&gt;Security Best Practices&lt;/li&gt;
&lt;li&gt;Lessons Learned&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The project is split into two repositories, a separation of concerns that keeps application code and infrastructure code independent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;job-application-tracker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Application source code (frontend, backend, Dockerfiles, local docker-compose)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;job-application-tracker-infra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Infrastructure: GitHub Actions workflows, Nginx config, smoke tests, prod docker-compose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  High-Level Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer pushes to source repo
         │
         ▼
  GitHub Actions (infra repo)
  Nightly at 2 AM UTC
         │
  ┌──────▼──────┐
  │  1. BUILD   │  Build Docker images with timestamp tag
  │             │  Push to AWS ECR
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  2. SMOKE   │  Launch temporary EC2 (t3.micro)
  │    TEST     │  Run containers, execute curl tests
  │             │  Terminate EC2 (pass or fail)
  └──────┬──────┘
         │ (only if tests pass)
  ┌──────▼──────┐
  │  3. PROMOTE │  Retag timestamp → :latest in ECR
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  4. DEPLOY  │  SSH-less deploy via AWS SSM
  │    TO QA    │  Pull :latest from ECR
  │             │  docker compose up on persistent EC2
  └─────────────┘
         │
  ┌──────▼──────┐
  │  QA EC2     │  Nginx (SSL/HTTPS)
  │  shri.      │  Frontend :3000 → /
  │  software   │  Backend  :5000 → /api/
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  AWS RDS    │  MySQL 8 (persistent, managed)
  └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The Application: Job Application Tracker
&lt;/h2&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%2Fal5g8xak6wldwlk76085.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%2Fal5g8xak6wldwlk76085.png" alt="Job Application Tracker, live at shri.software" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The finished app running at &lt;a href="https://shri.software" rel="noopener noreferrer"&gt;shri.software&lt;/a&gt; with HTTPS enforced&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The app lets users track their job applications (Applied → Interview → Offer / Rejected). Authentication is handled via &lt;strong&gt;GitHub OAuth&lt;/strong&gt; using NextAuth.js, no username/password to manage.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 14.2 with React 18, Tailwind CSS, NextAuth.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Express.js 4.18, JWT middleware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; MySQL 8 (local via Docker, production via AWS RDS)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Database Schema
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Users created on first GitHub login&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;                  &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt;               &lt;span class="nb"&gt;VARCHAR&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="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;                &lt;span class="nb"&gt;VARCHAR&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;avatar_url&lt;/span&gt;          &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt;            &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;provider_account_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&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="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_account_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;          &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;          &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Each row belongs to one user (CASCADE delete keeps DB clean)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;job_applications&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;company&lt;/span&gt;      &lt;span class="nb"&gt;VARCHAR&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="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;role&lt;/span&gt;         &lt;span class="nb"&gt;VARCHAR&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="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;       &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applied'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'interview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'offer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'applied'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;date_applied&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;      &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&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%2Flemw1j5j9jtwyqrdbp97.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%2Flemw1j5j9jtwyqrdbp97.png" alt="App dashboard showing an application entry" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dashboard view, filter by status, edit or delete entries inline&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Backend API
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Auth&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Health check for monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/users/upsert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Called by NextAuth on login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;List user's applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;Create application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;Update application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;Delete application&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Authentication Flow
&lt;/h3&gt;

&lt;p&gt;NextAuth.js handles the GitHub OAuth dance. The key insight: after OAuth completes, we generate a JWT signed with &lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt; that the frontend sends to the backend on every API call.&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="c1"&gt;// frontend/src/app/api/auth/[...nextauth]/route.js (simplified)&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;authOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;GitHubProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clientSecret&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
  &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Store GitHub identity in our DB on first login&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/users/upsert`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&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;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;provider_account_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;providerAccountId&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Mint a JWT for API calls; embed it in the session&lt;/span&gt;
      &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backendToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1h&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;session&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;p&gt;The backend verifies this token on every protected route:&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="c1"&gt;// backend/src/middleware/auth.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&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;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No token&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;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// { userId, email }&lt;/span&gt;
  &lt;span class="nf"&gt;next&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;h2&gt;
  
  
  3. Dockerizing the App
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend Dockerfile
&lt;/h3&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; node:20-alpine&lt;/span&gt;

&lt;span class="c"&gt;# Non-root user for security&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; shri &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; shri &lt;span class="nt"&gt;-G&lt;/span&gt; shri

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--production&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src/ ./src/&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; shri:shri /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; shri&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;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "src/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alpine base&lt;/strong&gt;, minimal attack surface, smaller image (~50 MB vs ~900 MB for &lt;code&gt;node:20&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-root user&lt;/strong&gt;, if the container is compromised, the attacker can't escalate to root on the host&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--production&lt;/code&gt; install&lt;/strong&gt;, excludes dev dependencies from the final image&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Frontend Dockerfile (Multi-Stage Build)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# --- Stage 1: Build ---&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&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;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# --- Stage 2: Runtime ---&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; shri &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; shri &lt;span class="nt"&gt;-G&lt;/span&gt; shri
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy only the artifacts needed to run&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/.next ./.next&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/package.json ./&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; shri:shri /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; shri&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node_modules/.bin/next", "start"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multi-stage builds are critical for Next.js, the build stage pulls in all dev dependencies and compiles TypeScript/JSX. The final stage only contains the compiled output, shrinking the image dramatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Local Development with Docker Compose
&lt;/h2&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%2Fdbenkoep5uq2jumpwn5l.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%2Fdbenkoep5uq2jumpwn5l.png" alt="App running locally at localhost:9000" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The app running locally via Docker Compose at localhost:9000&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The source repo's &lt;code&gt;docker-compose.yml&lt;/code&gt; wires everything together for local development with a single command:&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;# docker-compose.yml&lt;/span&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;mysql:8&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;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_DATABASE}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_USER}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/mysql&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"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysqladmin"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-h"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost"&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;10s&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;backend&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;./backend&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;10000: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;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_DATABASE}&lt;/span&gt;
      &lt;span class="na"&gt;DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_USER}&lt;/span&gt;
      &lt;span class="na"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${NEXTAUTH_SECRET}&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;span class="c1"&gt;# Wait for MySQL, not just the container&lt;/span&gt;

  &lt;span class="na"&gt;frontend&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;./frontend&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;9000:3000"&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;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${NEXTAUTH_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;NEXTAUTH_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9000&lt;/span&gt;
      &lt;span class="na"&gt;GITHUB_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_CLIENT_ID}&lt;/span&gt;
      &lt;span class="na"&gt;GITHUB_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_CLIENT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:10000&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get started locally:&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;# 1. Clone the source repo&lt;/span&gt;
git clone https://github.com/Shreyas-Yadav/job-application-tracker
&lt;span class="nb"&gt;cd &lt;/span&gt;job-application-tracker

&lt;span class="c"&gt;# 2. Copy and fill in the env file&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Edit .env with your GitHub OAuth credentials and secrets&lt;/span&gt;

&lt;span class="c"&gt;# 3. Launch everything&lt;/span&gt;
docker compose up

&lt;span class="c"&gt;# App available at:&lt;/span&gt;
&lt;span class="c"&gt;# Frontend: http://localhost:9000&lt;/span&gt;
&lt;span class="c"&gt;# Backend:  http://localhost:10000&lt;/span&gt;
&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%2F66g8ukuhpdr2n13wq028.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%2F66g8ukuhpdr2n13wq028.png" alt="docker compose up --build output in terminal" width="800" height="411"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All three containers (db, backend, frontend) starting up successfully&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; is important, the backend waits for MySQL to be truly ready (not just the container running) before starting.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. AWS Infrastructure Setup
&lt;/h2&gt;
&lt;h3&gt;
  
  
  5.1 GitHub OAuth App
&lt;/h3&gt;

&lt;p&gt;Go to GitHub → Settings → Developer Settings → OAuth Apps → New OAuth App:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Homepage URL:&lt;/strong&gt; &lt;code&gt;https://shri.software&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization callback URL:&lt;/strong&gt; &lt;code&gt;https://shri.software/api/auth/callback/github&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save the &lt;code&gt;Client ID&lt;/code&gt; and &lt;code&gt;Client Secret&lt;/code&gt;, these go into GitHub Actions secrets.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.2 AWS ECR (Elastic Container Registry)
&lt;/h3&gt;

&lt;p&gt;Create two private repositories to store Docker images:&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%2F28990urbh32rkkkae6nm.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%2F28990urbh32rkkkae6nm.png" alt="AWS ECR, both repositories" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Both ECR repositories created and ready to receive images from the CI pipeline&lt;/em&gt;&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="nt"&gt;--repository-name&lt;/span&gt; job-tracker-backend &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws ecr create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; job-tracker-frontend &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.3 AWS RDS (MySQL 8)
&lt;/h3&gt;

&lt;p&gt;Create a MySQL 8 RDS instance in the AWS Console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Engine: MySQL 8.0&lt;/li&gt;
&lt;li&gt;Instance class: &lt;code&gt;db.t3.micro&lt;/code&gt; (Free Tier eligible)&lt;/li&gt;
&lt;li&gt;Storage: 20 GB gp2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Important:&lt;/strong&gt; Place in the same VPC as your EC2 instances&lt;/li&gt;
&lt;li&gt;Set a master username and password&lt;/li&gt;
&lt;li&gt;Create a database: &lt;code&gt;jobtracker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Note the endpoint, it looks like &lt;code&gt;job-tracker-db.xxx.us-east-1.rds.amazonaws.com&lt;/code&gt;
&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%2F84vz82fy48tik7xtv3ob.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%2F84vz82fy48tik7xtv3ob.png" alt="AWS RDS, job-tracker-db instance" width="800" height="485"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;RDS MySQL 8 instance showing "Available" status with the endpoint used by the backend&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why RDS instead of a containerized DB? Managed backups, automated patching, and persistence across EC2 restarts without dealing with EBS volumes.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.4 QA EC2 Instance
&lt;/h3&gt;

&lt;p&gt;Launch a persistent EC2 instance (Ubuntu 22.04, &lt;code&gt;t3.micro&lt;/code&gt;):&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;# After SSH-ing in, install Docker and the SSM agent&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker.io docker-compose-plugin

&lt;span class="c"&gt;# Enable SSM agent (usually pre-installed on Ubuntu 22.04 AMIs)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;amazon-ssm-agent
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start amazon-ssm-agent

&lt;span class="c"&gt;# Allow ubuntu user to run docker without sudo&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker ubuntu
&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%2F5ej7bvqjekbiv12qqsrv.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%2F5ej7bvqjekbiv12qqsrv.png" alt="AWS EC2, QA instance details" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The persistent QA EC2 instance with LabRole attached, running Docker and Nginx&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Attach the &lt;strong&gt;LabRole&lt;/strong&gt; to this EC2 instance. In AWS Academy, &lt;code&gt;LabRole&lt;/code&gt; is a pre-provisioned IAM role that already has the permissions needed for SSM, ECR, and EC2 operations, you don't create it yourself, it's provided by the lab environment.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.5 IAM Credentials for GitHub Actions (AWS Academy)
&lt;/h3&gt;

&lt;p&gt;AWS Academy accounts don't support OIDC federation or long-lived IAM users. Instead, you get temporary session credentials from the &lt;strong&gt;AWS Details&lt;/strong&gt; panel in the Vocareum lab console. These rotate every session.&lt;/p&gt;

&lt;p&gt;Store them as GitHub Secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_ACCESS_KEY_ID      → from AWS Details panel
AWS_SECRET_ACCESS_KEY  → from AWS Details panel
AWS_SESSION_TOKEN      → from AWS Details panel (required for temporary credentials)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure credentials in your workflow using the static credential method:&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="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-session-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SESSION_TOKEN }}&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;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; AWS Academy session credentials expire when the lab session ends (~4 hours). You'll need to update these three secrets each time you start a new lab session. This is a limitation of the Academy environment, in a real AWS account you would use OIDC to avoid static credentials entirely.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. GitHub Actions CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;The infra repo contains five workflow files that chain together via &lt;code&gt;workflow_call&lt;/code&gt;. Each does one thing well.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 Nightly Orchestrator (&lt;code&gt;nightly.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is the entry point, triggered on a schedule or manually:&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;# .github/workflows/nightly.yml&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;Nightly Build and Deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&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;*'&lt;/span&gt;  &lt;span class="c1"&gt;# 2 AM UTC every day&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;# Allow manual triggers&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.tag.outputs.image_tag }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;tag&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;echo "image_tag=$(date +%Y%m%d%H%M%S)" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;

  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;setup&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;./.github/workflows/build.yml&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;image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.setup.outputs.image_tag }}&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;

  &lt;span class="na"&gt;smoke-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;]&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;./.github/workflows/smoke-test.yml&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;image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.setup.outputs.image_tag }}&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;

  &lt;span class="na"&gt;promote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;smoke-test&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Only runs if smoke test passes&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;./.github/workflows/promote.yml&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;image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.setup.outputs.image_tag }}&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;

  &lt;span class="na"&gt;deploy-qa&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;promote&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;./.github/workflows/deploy-qa.yml&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamp tag (e.g., &lt;code&gt;20250307021530&lt;/code&gt;) makes every build uniquely identifiable. If a smoke test fails, the image stays tagged only with the timestamp, it's never promoted to &lt;code&gt;:latest&lt;/code&gt; and never deployed.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 Build and Push to ECR (&lt;code&gt;build.yml&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/build.yml&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 and Push to ECR&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&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;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;

&lt;span class="na"&gt;jobs&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="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&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;Checkout source code&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;actions/checkout@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;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Shreyas-Yadav/job-application-tracker&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SOURCE_REPO_TOKEN }}&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;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-session-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SESSION_TOKEN }}&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;us-east-1&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;Login to 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 and push backend&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;docker build -t ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/job-tracker-backend:${{ inputs.image_tag }} ./backend&lt;/span&gt;
          &lt;span class="s"&gt;docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/job-tracker-backend:${{ inputs.image_tag }}&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 and push frontend&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;docker build -t ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/job-tracker-frontend:${{ inputs.image_tag }} ./frontend&lt;/span&gt;
          &lt;span class="s"&gt;docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/job-tracker-frontend:${{ inputs.image_tag }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.3 Smoke Test on Temporary EC2 (&lt;code&gt;smoke-test.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is the most interesting part. Instead of testing on the QA instance (risking breaking it), we launch a fresh, temporary EC2 for every test run:&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;# .github/workflows/smoke-test.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;smoke-test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&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;Launch temporary EC2&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;launch&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;INSTANCE_ID=$(aws ec2 run-instances \&lt;/span&gt;
            &lt;span class="s"&gt;--image-id ${{ secrets.TEMP_EC2_AMI }} \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-type t3.micro \&lt;/span&gt;
            &lt;span class="s"&gt;--subnet-id ${{ secrets.TEMP_EC2_SUBNET_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--security-group-ids ${{ secrets.TEMP_EC2_SG_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--iam-instance-profile Name=LabInstanceProfile \&lt;/span&gt;
            &lt;span class="s"&gt;--query 'Instances[0].InstanceId' \&lt;/span&gt;
            &lt;span class="s"&gt;--output text)&lt;/span&gt;
          &lt;span class="s"&gt;echo "instance_id=$INSTANCE_ID" &amp;gt;&amp;gt; $GITHUB_OUTPUT&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;Wait for SSM agent&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;# Wait up to 4 minutes for the instance to boot and SSM to connect&lt;/span&gt;
          &lt;span class="s"&gt;for i in {1..24}; do&lt;/span&gt;
            &lt;span class="s"&gt;STATUS=$(aws ssm describe-instance-information \&lt;/span&gt;
              &lt;span class="s"&gt;--filters "Key=InstanceIds,Values=${{ steps.launch.outputs.instance_id }}" \&lt;/span&gt;
              &lt;span class="s"&gt;--query 'InstanceInformationList[0].PingStatus' \&lt;/span&gt;
              &lt;span class="s"&gt;--output text 2&amp;gt;/dev/null || echo "None")&lt;/span&gt;
            &lt;span class="s"&gt;[ "$STATUS" = "Online" ] &amp;amp;&amp;amp; break&lt;/span&gt;
            &lt;span class="s"&gt;sleep 10&lt;/span&gt;
          &lt;span class="s"&gt;done&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;Run smoke tests via SSM&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;COMMAND_ID=$(aws ssm send-command \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ steps.launch.outputs.instance_id }} \&lt;/span&gt;
            &lt;span class="s"&gt;--document-name "AWS-RunShellScript" \&lt;/span&gt;
            &lt;span class="s"&gt;--parameters commands='[&lt;/span&gt;
              &lt;span class="s"&gt;"aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com",&lt;/span&gt;
              &lt;span class="s"&gt;"IMAGE_TAG=${{ inputs.image_tag }} docker compose -f /tmp/docker-compose.smoke.yml up -d",&lt;/span&gt;
              &lt;span class="s"&gt;"bash /tmp/smoke-test.sh"&lt;/span&gt;
            &lt;span class="s"&gt;]' \&lt;/span&gt;
            &lt;span class="s"&gt;--query 'Command.CommandId' --output text)&lt;/span&gt;

          &lt;span class="s"&gt;# Poll until complete&lt;/span&gt;
          &lt;span class="s"&gt;aws ssm wait command-executed \&lt;/span&gt;
            &lt;span class="s"&gt;--command-id $COMMAND_ID \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-id ${{ steps.launch.outputs.instance_id }}&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;Terminate temporary EC2&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;  &lt;span class="c1"&gt;# Clean up even if tests fail&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 ec2 terminate-instances \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ steps.launch.outputs.instance_id }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;smoke-test.sh&lt;/code&gt; script runs three checks:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5000"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Wait up to 2 minutes for backend to be ready&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..12&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKEND&lt;/span&gt;&lt;span class="s2"&gt;/health"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break
  echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for backend... (&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;/12)"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;10
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Test 1: Backend health check&lt;/span&gt;
curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKEND&lt;/span&gt;&lt;span class="s2"&gt;/health"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"status":"ok"'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: /health"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: Backend /health returns ok"&lt;/span&gt;

&lt;span class="c"&gt;# Test 2: Auth middleware is working (unauthenticated request → 401)&lt;/span&gt;
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKEND&lt;/span&gt;&lt;span class="s2"&gt;/api/applications"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"401"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: /api/applications should return 401, got &lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: /api/applications correctly requires authentication"&lt;/span&gt;

&lt;span class="c"&gt;# Test 3: Frontend is serving pages&lt;/span&gt;
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FRONTEND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: Frontend returned &lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: Frontend is serving pages"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"All smoke tests passed!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.4 Promote Image (&lt;code&gt;promote.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Once smoke tests pass, we retag the timestamp image as &lt;code&gt;:latest&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="c1"&gt;# .github/workflows/promote.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;promote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;job-tracker-backend&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;job-tracker-frontend&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&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;Retag image as latest&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;REGISTRY="${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com"&lt;/span&gt;
          &lt;span class="s"&gt;# Fetch the manifest of the tested image&lt;/span&gt;
          &lt;span class="s"&gt;MANIFEST=$(aws ecr batch-get-image \&lt;/span&gt;
            &lt;span class="s"&gt;--repository-name ${{ matrix.repo }} \&lt;/span&gt;
            &lt;span class="s"&gt;--image-ids imageTag=${{ inputs.image_tag }} \&lt;/span&gt;
            &lt;span class="s"&gt;--query 'images[0].imageManifest' --output text)&lt;/span&gt;

          &lt;span class="s"&gt;# Push the same manifest with the :latest tag&lt;/span&gt;
          &lt;span class="s"&gt;aws ecr put-image \&lt;/span&gt;
            &lt;span class="s"&gt;--repository-name ${{ matrix.repo }} \&lt;/span&gt;
            &lt;span class="s"&gt;--image-tag latest \&lt;/span&gt;
            &lt;span class="s"&gt;--image-manifest "$MANIFEST"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach (retagging the manifest) is instant, no re-pulling or re-pushing layers. The &lt;code&gt;:latest&lt;/code&gt; image is byte-for-byte identical to the tested timestamp image.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.5 Deploy to QA EC2 (&lt;code&gt;deploy-qa.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The final step deploys to the persistent QA server using AWS SSM, no SSH keys needed:&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;# .github/workflows/deploy-qa.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&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;Check QA EC2 is running&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;check&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;STATE=$(aws ec2 describe-instances \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ secrets.QA_EC2_INSTANCE_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--query 'Reservations[0].Instances[0].State.Name' \&lt;/span&gt;
            &lt;span class="s"&gt;--output text)&lt;/span&gt;
          &lt;span class="s"&gt;echo "state=$STATE" &amp;gt;&amp;gt; $GITHUB_OUTPUT&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;Sync config files&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.check.outputs.state == 'running'&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;# Base64-encode configs and decode them on the EC2 to avoid quoting issues&lt;/span&gt;
          &lt;span class="s"&gt;COMPOSE_B64=$(base64 -w0 docker-compose.prod.yml)&lt;/span&gt;
          &lt;span class="s"&gt;NGINX_B64=$(base64 -w0 nginx/nginx.conf)&lt;/span&gt;

          &lt;span class="s"&gt;aws ssm send-command \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ secrets.QA_EC2_INSTANCE_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--document-name "AWS-RunShellScript" \&lt;/span&gt;
            &lt;span class="s"&gt;--parameters commands="[&lt;/span&gt;
              &lt;span class="s"&gt;\"echo $COMPOSE_B64 | base64 -d &amp;gt; /home/ubuntu/app/docker-compose.prod.yml\",&lt;/span&gt;
              &lt;span class="s"&gt;\"echo $NGINX_B64 | base64 -d &amp;gt; /etc/nginx/sites-enabled/default\",&lt;/span&gt;
              &lt;span class="s"&gt;\"nginx -t &amp;amp;&amp;amp; systemctl reload nginx\"&lt;/span&gt;
            &lt;span class="s"&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;Deploy containers&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.check.outputs.state == 'running'&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 ssm send-command \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ secrets.QA_EC2_INSTANCE_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--document-name "AWS-RunShellScript" \&lt;/span&gt;
            &lt;span class="s"&gt;--parameters commands="[&lt;/span&gt;
              &lt;span class="s"&gt;\"aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com\",&lt;/span&gt;
              &lt;span class="s"&gt;\"cd /home/ubuntu/app &amp;amp;&amp;amp; docker compose -f docker-compose.prod.yml pull\",&lt;/span&gt;
              &lt;span class="s"&gt;\"DB_HOST=${{ secrets.DB_HOST }} DB_NAME=${{ secrets.DB_NAME }} DB_USER=${{ secrets.DB_USER }} DB_PASSWORD=${{ secrets.DB_PASSWORD }} NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }} docker compose -f docker-compose.prod.yml up -d\",&lt;/span&gt;
              &lt;span class="s"&gt;\"sleep 10 &amp;amp;&amp;amp; curl -sf http://localhost:5000/health\"&lt;/span&gt;
            &lt;span class="s"&gt;]"&lt;/span&gt;
&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%2Fn1b2dve7g5zqtn2j5yah.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%2Fn1b2dve7g5zqtn2j5yah.png" alt="GitHub Actions, Nightly Build pipeline all green" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All 5 jobs in the nightly pipeline completing successfully in 5m 26s&lt;/em&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%2F6kt0c0otongxxwxpnufv.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%2F6kt0c0otongxxwxpnufv.png" alt="GitHub Actions, deploy-qa job steps" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The deploy-qa job expanded showing each SSM step: sync config files, deploy via docker compose, health check&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  7. Domain Name with Route 53 and SSL with Let's Encrypt
&lt;/h2&gt;
&lt;h3&gt;
  
  
  7.1 Get a Domain and Migrate to Route 53
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Purchase a domain on &lt;a href="https://www.name.com" rel="noopener noreferrer"&gt;Name.com&lt;/a&gt; (or any registrar)&lt;/li&gt;
&lt;li&gt;Create a &lt;strong&gt;Hosted Zone&lt;/strong&gt; in AWS Route 53 for your domain&lt;/li&gt;
&lt;li&gt;Note the 4 NS (nameserver) records Route 53 provides&lt;/li&gt;
&lt;li&gt;In Name.com's DNS settings, replace the default nameservers with Route 53's NS records&lt;/li&gt;
&lt;li&gt;Wait 24-48 hours for propagation&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  7.2 Create DNS Records
&lt;/h3&gt;

&lt;p&gt;In Route 53, create an &lt;strong&gt;A record&lt;/strong&gt; pointing to your QA EC2's public IP:&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%2Fa126gjqoetu10iknoqnp.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%2Fa126gjqoetu10iknoqnp.png" alt="Route 53, shri.software hosted zone" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Route 53 hosted zone for shri.software showing the A record, NS, and SOA records&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Type: A
Name: shri.software
Value: &amp;lt;EC2 Public IP&amp;gt;
TTL: 300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you stop/start EC2 instances, the public IP changes. Consider using an Elastic IP for the QA instance to keep the IP stable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  7.3 Install Certbot and Get an SSL Certificate
&lt;/h3&gt;

&lt;p&gt;SSH into your QA EC2 and run:&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;# Install Certbot with the Nginx plugin&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; certbot python3-certbot-nginx

&lt;span class="c"&gt;# Obtain a certificate (Certbot automatically configures Nginx)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; shri.software

&lt;span class="c"&gt;# Verify auto-renewal is configured&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status certbot.timer
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify domain ownership by placing a file at &lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Download the certificate to &lt;code&gt;/etc/letsencrypt/live/shri.software/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Modify your Nginx config to use the certificate&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  8. Nginx as a Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;Nginx sits in front of both services, routing traffic and terminating SSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/nginx/sites-enabled/default&lt;/span&gt;

&lt;span class="c1"&gt;# Redirect HTTP to HTTPS&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;shri.software&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# HTTPS server&lt;/span&gt;
&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;shri.software&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt;     &lt;span class="n"&gt;/etc/letsencrypt/live/shri.software/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/shri.software/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# NextAuth routes must go to the frontend (Next.js handles them)&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/auth/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# All other /api/ routes go to Express backend&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Everything else goes to Next.js frontend&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="s"&gt;https&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;p&gt;The order of &lt;code&gt;location&lt;/code&gt; blocks matters: &lt;code&gt;/api/auth/&lt;/code&gt; must be listed before &lt;code&gt;/api/&lt;/code&gt; because Nginx uses longest prefix matching.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Security Best Practices
&lt;/h2&gt;

&lt;p&gt;Here's what I did deliberately for security:&lt;/p&gt;

&lt;h3&gt;
  
  
  Containers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-root users&lt;/strong&gt; in every container (&lt;code&gt;adduser -S shri&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpine base images&lt;/strong&gt;, smaller surface area, fewer CVEs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--production&lt;/code&gt; npm install&lt;/strong&gt;, dev tools never ship to production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage builds&lt;/strong&gt;, build tools (compilers, test runners) never end up in production images&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub OAuth&lt;/strong&gt;, no password storage, no credential database to protect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived JWTs&lt;/strong&gt; (1 hour expiry) between frontend and backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-scoped queries&lt;/strong&gt;, every DB query filters by &lt;code&gt;user_id&lt;/code&gt; from the verified JWT; users cannot access each other's data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Infrastructure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No SSH keys&lt;/strong&gt;, AWS SSM for all remote execution; port 22 is closed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LabRole on EC2&lt;/strong&gt;, AWS Academy's pre-provisioned role grants SSM and ECR access without custom policy authoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS in private subnet&lt;/strong&gt;, database is not publicly accessible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets in GitHub Secrets&lt;/strong&gt;, never hardcoded in workflow files or checked into git&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS only&lt;/strong&gt;, Nginx enforces HTTP → HTTPS redirect at the server level&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Secrets Management
&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%2F5agkpsh2eohc4ybuqrwz.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%2F5agkpsh2eohc4ybuqrwz.png" alt="GitHub Secrets, all 16 secrets configured" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All secrets stored in GitHub, values are never visible after being saved&lt;/em&gt;&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;# GitHub Secrets used in this project:&lt;/span&gt;
AWS_ACCOUNT_ID          &lt;span class="c"&gt;# AWS account number&lt;/span&gt;
AWS_ACCESS_KEY_ID       &lt;span class="c"&gt;# From AWS Academy lab details panel&lt;/span&gt;
AWS_SECRET_ACCESS_KEY   &lt;span class="c"&gt;# From AWS Academy lab details panel&lt;/span&gt;
AWS_SESSION_TOKEN       &lt;span class="c"&gt;# From AWS Academy lab details panel (rotates each session)&lt;/span&gt;
TEMP_EC2_AMI            &lt;span class="c"&gt;# AMI ID for smoke test instances&lt;/span&gt;
TEMP_EC2_SUBNET_ID      &lt;span class="c"&gt;# VPC subnet for temporary instances&lt;/span&gt;
TEMP_EC2_SG_ID          &lt;span class="c"&gt;# Security group ID&lt;/span&gt;
QA_EC2_INSTANCE_ID      &lt;span class="c"&gt;# Persistent QA EC2 instance ID&lt;/span&gt;
DB_HOST                 &lt;span class="c"&gt;# RDS endpoint&lt;/span&gt;
DB_NAME / DB_USER / DB_PASSWORD
NEXTAUTH_SECRET         &lt;span class="c"&gt;# Shared secret for JWT signing&lt;/span&gt;
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET  &lt;span class="c"&gt;# OAuth app credentials&lt;/span&gt;
SOURCE_REPO_TOKEN       &lt;span class="c"&gt;# PAT to check out source repo from infra repo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The full pipeline, from a git push to a verified deployment on HTTPS, runs without any manual steps. The key architectural wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two repos&lt;/strong&gt; for clean separation of application vs. infrastructure concerns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamp-tagged images&lt;/strong&gt; with promotion to &lt;code&gt;:latest&lt;/code&gt; only after passing tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral test infrastructure&lt;/strong&gt; that is created and destroyed per pipeline run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSM-based deployment&lt;/strong&gt; with no SSH keys to manage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route 53 + Let's Encrypt&lt;/strong&gt; for production-grade DNS and SSL at zero cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live app is running at &lt;code&gt;https://shri.software&lt;/code&gt;. The source code is split across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source repo: &lt;code&gt;github.com/Shreyas-Yadav/job-application-tracker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Infra repo: &lt;code&gt;github.com/Shreyas-Yadav/job-application-tracker-infra&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>aws</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Building High-Availability Web Apps on AWS: Auto Scaling, ALB, and Private Subnets</title>
      <dc:creator>Shreyas Yadav</dc:creator>
      <pubDate>Tue, 14 Oct 2025 22:26:38 +0000</pubDate>
      <link>https://dev.to/shreyas_yadav_e6fbf9ad3f6/building-high-availability-web-apps-on-aws-auto-scaling-alb-and-private-subnets-48a9</link>
      <guid>https://dev.to/shreyas_yadav_e6fbf9ad3f6/building-high-availability-web-apps-on-aws-auto-scaling-alb-and-private-subnets-48a9</guid>
      <description>&lt;p&gt;This guide, we’ll see how to deploy a production ready weather application on AWS with enterprise grade architecture. We’l' use &lt;strong&gt;Auto Scaling Groups&lt;/strong&gt; and &lt;strong&gt;Application Load Balancers&lt;/strong&gt; for high availability, &lt;strong&gt;public-private subnets&lt;/strong&gt; for security isolation, and &lt;strong&gt;Amazon ECR&lt;/strong&gt; for containerized deployments. To make it production complete, we'll configure a custom domain with &lt;strong&gt;Route 53&lt;/strong&gt; and enable HTTPS using &lt;strong&gt;AWS Certificate Manager&lt;/strong&gt;. The result? A fully scalable, secure web application accessible via your own domain with SSL encryption exactly how professional applications should be deployed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/Shreyas-Yadav/weather-aws" rel="noopener noreferrer"&gt;https://github.com/Shreyas-Yadav/weather-aws&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone the above repo and continue. &lt;/p&gt;

&lt;p&gt;Our weather application uses a multi-tier architecture on AWS that separates the frontend and backend for better security and scalability. &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%2Fu19yqto7lwty49fhm96w.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%2Fu19yqto7lwty49fhm96w.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Creating the VPC
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a VPC?
&lt;/h3&gt;

&lt;p&gt;A VPC (Virtual Private Cloud) is your isolated network in AWS. Think of it as your own private data center in the cloud where all your servers and resources will live securely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up the VPC
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate to VPC Creation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to AWS Console → Search "VPC" → Click "Your VPCs" → Click &lt;strong&gt;"Create VPC"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose "VPC and more"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'll see two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPC only&lt;/strong&gt; - Manual setup (tedious)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC and more&lt;/strong&gt; - Automatic setup(recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Select &lt;strong&gt;"VPC and more"&lt;/strong&gt; - AWS will automatically create all networking components for you.&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%2F6qflqvjwu1rk8x3crzsb.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%2F6qflqvjwu1rk8x3crzsb.png" alt=" " width="800" height="815"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name tag auto-generation:&lt;/strong&gt; &lt;code&gt;weather-app&lt;/code&gt;&lt;br&gt;
This prefix will be added to all resources, making them easy to identify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IPv4 CIDR block:&lt;/strong&gt; &lt;code&gt;10.0.0.0/16&lt;/code&gt;&lt;br&gt;
This is your VPC's IP address range (65,536 available IPs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IPv6:&lt;/strong&gt; Select "No IPv6 CIDR block"&lt;br&gt;
&lt;strong&gt;Tenancy:&lt;/strong&gt; Keep as "Default"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Availability Zones and Subnets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number of Availability Zones:&lt;/strong&gt; Select &lt;strong&gt;2&lt;/strong&gt;&lt;br&gt;
This is crucial for high availability - if one data center fails, your app continues running in the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number of public subnets:&lt;/strong&gt; &lt;strong&gt;2&lt;/strong&gt;&lt;br&gt;
These will host your frontend servers (internet-accessible)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number of private subnets:&lt;/strong&gt; &lt;strong&gt;2&lt;/strong&gt;&lt;br&gt;
These will host your backend servers (hidden from internet)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subnet CIDR blocks:&lt;/strong&gt; Keep the defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Public subnet 1a:  10.0.0.0/20
Public subnet 1b:  10.0.16.0/20
Private subnet 1a: 10.0.128.0/20
Private subnet 1b: 10.0.144.0/20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NAT Gateways&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;"1 per AZ"&lt;/strong&gt; (creates 2 total)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a NAT Gateway?&lt;/strong&gt;&lt;br&gt;
It allows your private backend servers to access the internet (for API calls, updates) while staying hidden from incoming internet traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 2 NAT Gateways?&lt;/strong&gt;&lt;br&gt;
High availability each private subnet gets its own NAT Gateway for redundancy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional Settings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC endpoints:&lt;/strong&gt; None&lt;br&gt;
&lt;strong&gt;DNS options:&lt;/strong&gt; Enable both&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable DNS hostnames&lt;/li&gt;
&lt;li&gt;Enable DNS resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Create VPC&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create VPC"&lt;/strong&gt; and wait 2-3 minutes.&lt;/p&gt;

&lt;p&gt;AWS will create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 VPC&lt;/li&gt;
&lt;li&gt;4 Subnets (2 public, 2 private)
&lt;/li&gt;
&lt;li&gt;1 Internet Gateway&lt;/li&gt;
&lt;li&gt;2 NAT Gateways&lt;/li&gt;
&lt;li&gt;Route tables&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 2: Creating Security Groups
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What are Security Groups?
&lt;/h3&gt;

&lt;p&gt;Security Groups are virtual firewalls that control inbound and outbound traffic to your AWS resources.&lt;/p&gt;

&lt;p&gt;We'll create 4 security groups to control traffic between components.&lt;/p&gt;
&lt;h3&gt;
  
  
  Security Group 1: Frontend ALB Security Group
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Security Groups → Create security group&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%2Fl6m8einthgud0t5ayoon.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%2Fl6m8einthgud0t5ayoon.png" alt=" " width="800" height="437"&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;Name: weather-frontend-alb-sg
Description: Security group for Frontend Application Load Balancer
VPC: weather-app-vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inbound Rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP (80) from 0.0.0.0/0&lt;/li&gt;
&lt;li&gt;HTTPS (443) from 0.0.0.0/0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outbound Rules:&lt;/strong&gt; All traffic (default)&lt;/p&gt;




&lt;h3&gt;
  
  
  Security Group 2: Frontend EC2 Security Group
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: weather-frontend-sg
Description: Security group for Frontend EC2 instances
VPC: weather-app-vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inbound Rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP (80) from Custom → &lt;code&gt;weather-frontend-alb-sg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SSH (22) from My IP (optional for debugging)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outbound Rules:&lt;/strong&gt; All traffic (default)&lt;/p&gt;




&lt;h3&gt;
  
  
  Security Group 3: Backend ALB Security Group
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: weather-backend-alb-sg
Description: Security group for Backend Application Load Balancer
VPC: weather-app-vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inbound Rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP (80) from Custom → &lt;code&gt;weather-frontend-sg&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outbound Rules:&lt;/strong&gt; All traffic (default)&lt;/p&gt;




&lt;h3&gt;
  
  
  Security Group 4: Backend EC2 Security Group
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: weather-backend-sg
Description: Security group for Backend EC2 instances
VPC: weather-app-vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inbound Rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom TCP (3000) from Custom → &lt;code&gt;weather-backend-alb-sg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SSH (22) from My IP (optional for debugging)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outbound Rules:&lt;/strong&gt; All traffic (default)&lt;/p&gt;

&lt;h3&gt;
  
  
  Traffic Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet → Frontend ALB (80,443) → Frontend EC2 (80) → Backend ALB (80) → Backend EC2 (3000)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: IAM Role for EC2 Instances.
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Note for AWS Academy/Lab Users
&lt;/h3&gt;

&lt;p&gt;If you're using &lt;strong&gt;AWS Academy Labs&lt;/strong&gt; or &lt;strong&gt;AWS Learner Lab&lt;/strong&gt;, you cannot create custom IAM roles. Instead, use the pre-configured role provided:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role name:&lt;/strong&gt; &lt;code&gt;LabRole&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This role already has the necessary permissions for ECR, Parameter Store, and other AWS services.&lt;/p&gt;

&lt;p&gt;When creating EC2 instances or Launch Templates, select &lt;strong&gt;LabRole&lt;/strong&gt; as the IAM instance profile.&lt;/p&gt;


&lt;h3&gt;
  
  
  For Regular AWS Accounts
&lt;/h3&gt;

&lt;p&gt;If you're using a personal/company AWS account (not AWS Labs), follow these steps to create a custom IAM role:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; IAM → Roles → Create role&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Select Trusted Entity&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;Trusted entity type: AWS service
Use case: EC2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Permissions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search and select these 2 managed policies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AmazonEC2ContainerRegistryReadOnly&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AmazonSSMManagedInstanceCore&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name and Create&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;Role name: weather-ec2-role
Description: IAM role for Weather App EC2 instances
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Create role&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Parameter Store Policy&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on &lt;strong&gt;weather-ec2-role&lt;/strong&gt; → &lt;strong&gt;Permissions&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add permissions&lt;/strong&gt; → &lt;strong&gt;Create inline policy&lt;/strong&gt; → &lt;strong&gt;JSON&lt;/strong&gt; tab
&lt;/li&gt;
&lt;/ol&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="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;"ssm:GetParameter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:GetParameters"&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:ssm:*:*:parameter/weather-app/*"&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;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;ol&gt;
&lt;li&gt;Policy name: &lt;code&gt;ParameterStoreReadPolicy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click **Create &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4: Creating ECR Repositories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Amazon ECR?
&lt;/h3&gt;

&lt;p&gt;Amazon Elastic Container Registry (ECR) is a Docker container registry where we'll store our frontend and backend Docker images. EC2 instances will pull images from here during deployment.&lt;/p&gt;




&lt;h3&gt;
  
  
  Create ECR Repositories
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; Amazon ECR → Repositories → Create repository&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%2Fby9e5dymlkrz11uqxudn.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%2Fby9e5dymlkrz11uqxudn.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Repository 1: Frontend
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Repository name: weather-frontend
Image tag mutability: Mutable
Encryption: AES-256 (default)
Scan on push: Enable (optional)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Create repository&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%2F0cxu70pwk84ernnvkyff.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%2F0cxu70pwk84ernnvkyff.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Repository 2: Backend
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Repository name: weather-backend
Image tag mutability: Mutable
Encryption: AES-256 (default)
Scan on push: Enable (optional)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Create repository&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Note Your Repository URIs
&lt;/h3&gt;

&lt;p&gt;After creation, note down both repository URIs (format: &lt;code&gt;account-id.dkr.ecr.region.amazonaws.com/repository-name&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;Frontend URI: 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-frontend
Backend URI: 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need these URIs to push Docker images and configure EC2 instances.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Build and Push Docker Images to ECR
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Ensure you have installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CLI (latest version)&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Build Docker Images
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Build images compatible with EC2 architecture (Linux AMD64/ARM64).&lt;/p&gt;

&lt;p&gt;Navigate to your project directory and build both images:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build Backend:&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;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="nt"&gt;-t&lt;/span&gt; weather-backend &lt;span class="nt"&gt;--load&lt;/span&gt; ./backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Build Frontend:&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;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="nt"&gt;-t&lt;/span&gt; weather-frontend &lt;span class="nt"&gt;--load&lt;/span&gt; ./frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;--platform&lt;/code&gt; flag?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
EC2 instances run Linux. This ensures your Docker images are compatible with EC2's operating system and CPU architecture.&lt;/p&gt;


&lt;h3&gt;
  
  
  Push Images to ECR
&lt;/h3&gt;

&lt;p&gt;AWS provides ready-to-use push commands for each repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To view push commands:&lt;/strong&gt; ECR → Repositories → Select repository → Click &lt;strong&gt;"View push commands"&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%2Fbyxaiu3w23s8t7ydvtl3.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%2Fbyxaiu3w23s8t7ydvtl3.png" alt=" " width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Authenticate Docker to ECR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;us-east-1&lt;/code&gt; with your region and &lt;code&gt;339712997278&lt;/code&gt; with your AWS account ID:&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 get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; 339712997278.dkr.ecr.us-east-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Tag Frontend Image&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;docker tag weather-frontend:latest 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-frontend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Push Frontend Image&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;docker push 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-frontend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Tag Backend Image&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;docker tag weather-backend:latest 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-backend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Push Backend Image&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;docker push 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-backend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Verify Images in ECR
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Amazon ECR → Repositories&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check both repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;weather-frontend&lt;/code&gt; - Should show &lt;code&gt;latest&lt;/code&gt; tag&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weather-backend&lt;/code&gt; - Should show &lt;code&gt;latest&lt;/code&gt; tag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Creating Target Groups
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are Target Groups?
&lt;/h3&gt;

&lt;p&gt;Target Groups are used by Application Load Balancers to route traffic to registered EC2 instances. They also perform health checks to ensure traffic only goes to healthy instances.&lt;/p&gt;

&lt;p&gt;We'll create 2 target groups - one for frontend and one for backend.&lt;/p&gt;




&lt;h3&gt;
  
  
  Target Group 1: Backend Target Group
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Target Groups → Create target group&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%2F2uq5pjco3m347mpg5iup.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%2F2uq5pjco3m347mpg5iup.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Basic Configuration&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;Target type: Instances
Target group name: weather-backend-tg
Protocol: HTTP
Port: 3000
VPC: weather-app-vpc
Protocol version: HTTP1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Health Checks&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;Health check protocol: HTTP
Health check path: /health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advanced health check settings:&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;Healthy threshold: 2
Unhealthy threshold: 3
Timeout: 5 seconds
Interval: 30 seconds
Success codes: 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Register Targets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Skip this step - instances will be added automatically by Auto Scaling Group.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create target group&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Target Group 2: Frontend Target Group
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Basic Configuration&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;Target type: Instances
Target group name: weather-frontend-tg
Protocol: HTTP
Port: 80
VPC: weather-app-vpc
Protocol version: HTTP1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Health Checks&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;Health check protocol: HTTP
Health check path: /health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advanced health check settings:&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;Healthy threshold: 2
Unhealthy threshold: 3
Timeout: 5 seconds
Interval: 30 seconds
Success codes: 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Register Targets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Skip this step - instances will be added automatically by Auto Scaling Group.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create target group&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Creating Application Load Balancers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an Application Load Balancer?
&lt;/h3&gt;

&lt;p&gt;An Application Load Balancer (ALB) distributes incoming traffic across multiple EC2 instances. We'll create two ALBs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend ALB&lt;/strong&gt; (Internet-facing) - Receives traffic from users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend ALB&lt;/strong&gt; (Internal) - Receives traffic from frontend instances&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ALB 1: Backend Application Load Balancer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Load Balancers → Create load balancer → Application Load Balancer&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Configuration&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;Load balancer name: weather-backend-alb
Scheme: Internal
IP address type: IPv4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Network Mapping&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;VPC: weather-app-vpc
Availability Zones: Select both
  ☑ us-east-1a → Select private subnet
  ☑ us-east-1b → Select private subnet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security Groups&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;Remove: default
Add: weather-backend-alb-sg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Listeners and Routing&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;Protocol: HTTP
Port: 80
Default action: Forward to weather-backend-tg
&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%2Ftpeqxgb0ttw2p32748xj.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%2Ftpeqxgb0ttw2p32748xj.png" alt=" " width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create load balancer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait 2-3 minutes for the ALB to become active.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Copy the Backend ALB DNS name - you'll need it later!&lt;/p&gt;




&lt;h3&gt;
  
  
  ALB 2: Frontend Application Load Balancer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Basic Configuration&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;Load balancer name: weather-frontend-alb
Scheme: Internet-facing
IP address type: IPv4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Network Mapping&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;VPC: weather-app-vpc
Availability Zones: Select both
  ☑ us-east-1a → Select public subnet
  ☑ us-east-1b → Select public subnet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security Groups&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;Remove: default
Add: weather-frontend-alb-sg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Listeners and Routing&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;Protocol: HTTP
Port: 80
Default action: Forward to weather-frontend-tg
&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%2F9fegqng71s8lwf2qzoqd.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%2F9fegqng71s8lwf2qzoqd.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create load balancer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait 2-3 minutes for the ALB to become active.&lt;/p&gt;




&lt;h3&gt;
  
  
  Verify Load Balancers
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;EC2 → Load Balancers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check both ALBs show &lt;strong&gt;State: Active&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note down both DNS names:&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;Frontend ALB DNS: weather-frontend-alb-xxxxx.us-east-1.elb.amazonaws.com
Backend ALB DNS: weather-backend-alb-xxxxx.us-east-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 8: Configure AWS Systems Manager Parameter Store
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Parameter Store?
&lt;/h3&gt;

&lt;p&gt;Parameter Store allows you to store configuration data and secrets centrally. EC2 instances will fetch these values at runtime, eliminating hardcoded configuration.&lt;/p&gt;




&lt;h3&gt;
  
  
  Create Parameters
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; AWS Systems Manager → Parameter Store → Create parameter. &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%2Fq1dhqvcn467g712hkzfj.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%2Fq1dhqvcn467g712hkzfj.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create the following parameters:&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Parameter 1: AWS Region&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;Name: /weather-app/aws-region
Type: String
Value: us-east-1 (or your region)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 2: Backend ALB Host (used by frontend to call an api)&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;Name: /weather-app/backend-host
Type: String
Value: &amp;lt;Your Backend ALB DNS without http://&amp;gt;
Example: internal-weather-backend-alb-xxxxx.us-east-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 3: Backend ALB Port (used by frontend to call an api)&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;Name: /weather-app/backend-port
Type: String
Value: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 4: Frontend ECR Registry&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;Name: /weather-app/ecr-registry-frontend
Type: String
Value: &amp;lt;Your account ID&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/weather-frontend
Example: 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 5: Backend ECR Registry&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;Name: /weather-app/ecr-registry-backend
Type: String
Value: &amp;lt;Your account ID&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/weather-backend
Example: 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 6: OpenWeather API Key&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;Name: /weather-app/openweather-api-key
Type: String (or SecureString for encryption)
Value: &amp;lt;Your OpenWeatherMap API key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get your free API key from: &lt;a href="https://openweathermap.org/api" rel="noopener noreferrer"&gt;https://openweathermap.org/api&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Verify Parameters
&lt;/h3&gt;

&lt;p&gt;All 6 parameters should now be visible in Parameter Store with the prefix &lt;code&gt;/weather-app/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Create Launch Templates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a Launch Template?
&lt;/h3&gt;

&lt;p&gt;A Launch Template defines the configuration for EC2 instances that will be launched by Auto Scaling Groups. We'll create two templates - one for backend and one for frontend.&lt;/p&gt;




&lt;h3&gt;
  
  
  Launch Template 1: Backend Launch Template
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Launch Templates → Create launch template&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch Template Name and Description&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;Launch template name: weather-backend-lt
Template version description: Backend instances for weather app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Application and OS Images (AMI)&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;Quick Start: Ubuntu
Ubuntu Server 24.04 LTS (HVM), SSD Volume Type
Architecture: 64-bit (x86)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Instance Type&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;Instance type: t3.micro (or t2.micro for free tier)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Pair&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;Key pair name: Select your existing key pair
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Network Settings&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;Subnet: Don't include in launch template
Firewall (security groups): weather-backend-sg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Storage&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;Keep default: 8 GiB, gp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advanced Details&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IAM instance profile:&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;LabRole (for AWS Academy users)
OR weather-ec2-role (for regular AWS accounts)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Metadata accessible:&lt;/strong&gt; Enabled&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User data:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copy the backend user data script from the repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Shreyas-Yadav/weather-aws/blob/main/backend-deployment.sh" rel="noopener noreferrer"&gt;Backend User Data Script →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installs Docker and AWS CLI&lt;/li&gt;
&lt;li&gt;Fetches configuration from Parameter Store&lt;/li&gt;
&lt;li&gt;Pulls Docker image from ECR&lt;/li&gt;
&lt;li&gt;Starts the backend container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Create launch template&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Launch Template 2: Frontend Launch Template
&lt;/h3&gt;

&lt;p&gt;Follow the same steps with these differences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launch template name: weather-frontend-lt
Template version description: Frontend instances for weather app
Security group: weather-frontend-sg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;User data:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copy the frontend user data script from the repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Shreyas-Yadav/weather-aws/blob/main/frontend-deployment.sh" rel="noopener noreferrer"&gt;Frontend User Data Script →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installs Docker and AWS CLI&lt;/li&gt;
&lt;li&gt;Fetches backend ALB DNS from Parameter Store&lt;/li&gt;
&lt;li&gt;Pulls Docker image from ECR&lt;/li&gt;
&lt;li&gt;Starts the frontend container with backend configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Create launch template&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Create Auto Scaling Groups
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an Auto Scaling Group?
&lt;/h3&gt;

&lt;p&gt;An Auto Scaling Group (ASG) automatically manages EC2 instances - launching new instances when traffic increases and terminating them when traffic decreases. We'll create two ASGs - one for backend and one for frontend. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; Create ASG first for backend and then create for frontend.&lt;/p&gt;




&lt;h3&gt;
  
  
  Auto Scaling Group 1: Backend ASG
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Auto Scaling Groups → Create Auto Scaling group&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Choose Launch Template&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;Auto Scaling group name: weather-backend-asg
Launch template: weather-backend-lt
Version: Default (1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Choose Instance Launch Options&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;VPC: weather-app-vpc

Availability Zones and subnets:
  ☑ weather-app-subnet-private1-us-east-1a
  ☑ weather-app-subnet-private2-us-east-1b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Configure Advanced Options&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;Load balancing:
  ☑ Attach to an existing load balancer

Choose from your load balancer target groups:
  ☑ weather-backend-tg

Health checks:
  ☑ Turn on Elastic Load Balancing health checks
  Health check grace period: 300 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Configure Group Size and Scaling&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;Group size:
  Desired capacity: 2
  Minimum capacity: 2
  Maximum capacity: 4

Scaling:
  ☑ Target tracking scaling policy

  Scaling policy name: backend-cpu-scaling
  Metric type: Average CPU utilization
  Target value: 70
  Instance warmup: 300 seconds
&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%2F6jwckwpwn6qguxasn90b.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%2F6jwckwpwn6qguxasn90b.png" alt=" " width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt; → &lt;strong&gt;Next&lt;/strong&gt; → &lt;strong&gt;Create Auto Scaling group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait 5-10 minutes for instances to launch and become healthy.&lt;/p&gt;




&lt;h3&gt;
  
  
  Auto Scaling Group 2: Frontend ASG
&lt;/h3&gt;

&lt;p&gt;Follow the same steps with these differences:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Choose Launch Template&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;Auto Scaling group name: weather-frontend-asg
Launch template: weather-frontend-lt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Network&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;Availability Zones and subnets:
  ☑ weather-app-subnet-public1-us-east-1a
  ☑ weather-app-subnet-public2-us-east-1b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Load Balancing&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;Target group: weather-frontend-tg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Group Size and Scaling&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;Desired capacity: 2
Minimum capacity: 2
Maximum capacity: 4

Scaling policy name: frontend-cpu-scaling
Metric type: Average CPU utilization
Target value: 70
&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%2Frrwrmvrenj946vl2gkn3.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%2Frrwrmvrenj946vl2gkn3.png" alt=" " width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create Auto Scaling group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait 5-10 minutes for instances to launch and become healthy.&lt;/p&gt;




&lt;h3&gt;
  
  
  Verify Auto Scaling Groups
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Check Instances:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;EC2 → Instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You should see 4 running instances:

&lt;ul&gt;
&lt;li&gt;2 backend instances (in private subnets)&lt;/li&gt;
&lt;li&gt;2 frontend instances (in public subnets)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Check Target Health:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;EC2 → Target Groups&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;weather-backend-tg&lt;/strong&gt; → Targets tab

&lt;ul&gt;
&lt;li&gt;Both targets should show &lt;strong&gt;Healthy&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;weather-frontend-tg&lt;/strong&gt; → Targets tab

&lt;ul&gt;
&lt;li&gt;Both targets should show &lt;strong&gt;Healthy&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;—--&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11: Testing Your Application
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Test Application via Frontend ALB
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Get Frontend ALB DNS:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;EC2 → Load Balancers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;weather-frontend-alb&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;DNS name&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example: &lt;code&gt;weather-frontend-alb-841040545.us-east-1.elb.amazonaws.com&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test in Browser:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open your browser and navigate 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://weather-frontend-alb-XXXXXXX.us-east-1.elb.amazonaws.com
&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%2Fylnpqyw7yqvgkco8navu.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%2Fylnpqyw7yqvgkco8navu.png" alt=" " width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weather Application interface&lt;/li&gt;
&lt;li&gt;Search box to enter city name&lt;/li&gt;
&lt;li&gt;Real-time weather data when you search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test the Complete Flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter a city name (e.g., "Mumbai", "London", "New York")&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Search&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Weather information should display:

&lt;ul&gt;
&lt;li&gt;Current temperature&lt;/li&gt;
&lt;li&gt;Weather condition (Haze, Clear, etc.)&lt;/li&gt;
&lt;li&gt;Feels like temperature&lt;/li&gt;
&lt;li&gt;Humidity&lt;/li&gt;
&lt;li&gt;Wind speed&lt;/li&gt;
&lt;li&gt;Pressure&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Common Issues:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the application doesn't work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Target Health:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EC2 → Target Groups → Check both target groups show "Healthy"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Instance Logs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect to instances via Session Manager&lt;/li&gt;
&lt;li&gt;View logs: &lt;code&gt;sudo docker logs weather-frontend&lt;/code&gt; or &lt;code&gt;sudo docker logs weather-backend&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Security Groups:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify all security group rules are correct&lt;/li&gt;
&lt;li&gt;Frontend ALB should allow 80 from internet&lt;/li&gt;
&lt;li&gt;Backend ALB should allow 80 from frontend SG&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;—--&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 12: Add Custom Domain and Enable HTTPS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;You need a registered domain name from any registrar (Namecheap, GoDaddy, etc.)&lt;/p&gt;




&lt;h3&gt;
  
  
  Part A: Create Hosted Zone in Route 53
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; Route 53 → Hosted zones → Create hosted zone&lt;/p&gt;

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

&lt;p&gt;Domain name: shri.software&lt;br&gt;
Type: Public hosted zone&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create hosted zone&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update Nameservers at Your Domain Registrar:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the 4 NS (nameserver) records from Route 53&lt;/li&gt;
&lt;li&gt;Go to your domain registrar (Namecheap, GoDaddy, etc.)&lt;/li&gt;
&lt;li&gt;Replace existing nameservers with the 4 AWS nameservers&lt;/li&gt;
&lt;li&gt;Save changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Wait 15-30 minutes for DNS propagation&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part B: Request SSL/TLS Certificate
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; AWS Certificate Manager (ACM) → Request certificate&lt;br&gt;
&lt;strong&gt;Step 1: Request Certificate&lt;/strong&gt;&lt;br&gt;
Certificate type: Request a public certificate&lt;br&gt;
Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Domain Names&lt;/strong&gt;&lt;br&gt;
Fully qualified domain name: shri.software&lt;br&gt;
Add another name: *.shri.software (optional, for subdomains)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Validation Method&lt;/strong&gt;&lt;br&gt;
Validation method: DNS validation - recommended&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Key Algorithm&lt;/strong&gt;&lt;br&gt;
RSA 2048 (default)&lt;br&gt;
Click &lt;strong&gt;Request&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part C: Validate Certificate
&lt;/h3&gt;

&lt;p&gt;After requesting the certificate, ACM will show validation details.&lt;br&gt;
&lt;strong&gt;Click "Create records in Route”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This automatically adds the required CNAME validation records to your hosted zone.&lt;br&gt;
Wait 5-10 minutes for validation to complete.&lt;br&gt;
&lt;strong&gt;Certificate Status:&lt;/strong&gt; Will change from "Pending validation" to "Issued"&lt;/p&gt;




&lt;h3&gt;
  
  
  Part D: Create A Record for Your Domain
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; Route 53 → Hosted zones → shri.software&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create A Record:&lt;/strong&gt;&lt;br&gt;
Record name: (leave empty for root domain)&lt;br&gt;
Record type: A - Alias&lt;br&gt;
Route traffic to: Alias to Application Load Balancer&lt;br&gt;
Region: us-east-1&lt;br&gt;
Load balancer: weather-frontend-alb&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create records&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part E: Add HTTPS Listener to Frontend ALB
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Load Balancers → weather-frontend-alb&lt;br&gt;
&lt;strong&gt;Add HTTPS Listener:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Listeners&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add listener&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Protocol: HTTPS&lt;br&gt;
Port: 443&lt;br&gt;
Default action: Forward to weather-frontend-tg&lt;br&gt;
Default SSL/TLS certificate: From ACM → Select your certificate (shri.software)&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Add&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part F: Redirect HTTP to HTTPS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Edit HTTP:80 Listener:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;strong&gt;HTTP:80&lt;/strong&gt; listener&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Actions&lt;/strong&gt; → &lt;strong&gt;Edit listener&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Remove existing forward action&lt;/li&gt;
&lt;li&gt;Add action → &lt;strong&gt;Redirect to...&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Protocol: HTTPS&lt;br&gt;
Port: 443&lt;br&gt;
Status code: 301 - Permanently moved&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Save changes&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Verify Your Setup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Test in Browser:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open: &lt;a href="https://your_domain_name" rel="noopener noreferrer"&gt;https://your_domain_name&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see:&lt;br&gt;
✅ Padlock icon (secure connection)&lt;br&gt;
✅ Weather application loads&lt;br&gt;
✅ HTTP automatically redirects 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%2F6uheosccn406oxmnp1r2.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%2F6uheosccn406oxmnp1r2.png" alt=" " width="800" height="781"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Congratulations!&lt;/strong&gt; Your application is live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 13: Cleanup and Delete Resources
&lt;/h2&gt;

&lt;p&gt;It is essential to delete all resources created in this guide to avoid incurring unnecessary charges on your AWS account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup Order
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto Scaling Groups (ASGs):&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend-asg&lt;/code&gt; and &lt;code&gt;weather-backend-asg&lt;/code&gt;. (This action automatically terminates all running EC2 instances.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application Load Balancers (ALBs):&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend-alb&lt;/code&gt; and &lt;code&gt;weather-backend-alb&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Target Groups:&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend-tg&lt;/code&gt; and &lt;code&gt;weather-backend-tg&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Launch Templates:&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend-lt&lt;/code&gt; and &lt;code&gt;weather-backend-lt&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ECR Repositories:&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend&lt;/code&gt; and &lt;code&gt;weather-backend&lt;/code&gt;. (This deletes the stored Docker images.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Route 53 &amp;amp; AWS Certificate Manager (ACM):&lt;/strong&gt;&lt;br&gt;
Delete the &lt;strong&gt;A Record&lt;/strong&gt; pointing to the ALB.&lt;br&gt;
Delete the &lt;strong&gt;ACM Certificate&lt;/strong&gt;.&lt;br&gt;
Delete the &lt;strong&gt;Hosted Zone&lt;/strong&gt; in Route 53.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IAM Role &amp;amp; Security Groups:&lt;/strong&gt;&lt;br&gt;
Delete the &lt;strong&gt;IAM Role&lt;/strong&gt; (&lt;code&gt;weather-ec2-role&lt;/code&gt;).&lt;br&gt;
Delete the &lt;strong&gt;4 Security Groups&lt;/strong&gt; (&lt;code&gt;weather-frontend-alb-sg&lt;/code&gt;, &lt;code&gt;weather-frontend-sg&lt;/code&gt;, etc.).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Systems Manager Parameters:&lt;/strong&gt;&lt;br&gt;
Delete all &lt;strong&gt;Parameters&lt;/strong&gt; under the &lt;code&gt;/weather-app/&lt;/code&gt; path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;VPC:&lt;/strong&gt;&lt;br&gt;
Delete the &lt;strong&gt;VPC&lt;/strong&gt; (&lt;code&gt;weather-app-vpc&lt;/code&gt;). (This final step cleans up Subnets, Internet Gateway, and NAT Gateways.) &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion and Thank You..!
&lt;/h2&gt;

&lt;p&gt;Congratulations! You have successfully deployed a highly available, secure, and production-ready weather application on AWS using enterprise-grade architecture.&lt;/p&gt;

&lt;p&gt;By completing this guide, you’ve mastered key AWS services including VPC, Auto Scaling Groups, Application Load Balancers, ECR, Route 53, and ACM. This foundational knowledge is crucial for deploying any modern, scalable web application.&lt;/p&gt;

&lt;p&gt;Thank you for following along, and I hope this guide helps you on your cloud journey!&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
