<?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: Chidubem Okoli</title>
    <description>The latest articles on DEV Community by Chidubem Okoli (@duubemmm).</description>
    <link>https://dev.to/duubemmm</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%2F2185225%2Ff6ce66b0-eb2d-4bf0-8eab-5b60889078bd.png</url>
      <title>DEV Community: Chidubem Okoli</title>
      <link>https://dev.to/duubemmm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/duubemmm"/>
    <language>en</language>
    <item>
      <title>I replaced theoretical study with high-intensity practice — and the result? I passed the AWS Solutions Architect Associate exam.</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Wed, 07 Oct 2026 22:18:16 +0000</pubDate>
      <link>https://dev.to/duubemmm/i-replaced-theoretical-study-with-high-intensity-practice-and-the-result-i-passed-the-aws-169j</link>
      <guid>https://dev.to/duubemmm/i-replaced-theoretical-study-with-high-intensity-practice-and-the-result-i-passed-the-aws-169j</guid>
      <description>&lt;p&gt;As I progressed through my AWS learning journey, I went from being familiar with EC2 to working with services like Lambda, IAM, VPC, RDS, DynamoDB, Aurora, and several others.&lt;/p&gt;

&lt;p&gt;At some point, I felt I had built enough knowledge to take the AWS Certified Solutions Architect – Associate exam.&lt;/p&gt;

&lt;p&gt;Until I decided to solve Tutorials Dojo practice questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That was my rude awakening.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I quickly realized that &lt;strong&gt;knowing AWS services was not the same as knowing how to architect solutions on AWS.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I came across Tutorials Dojo frequently while researching how other people prepared for the exam, especially through the &lt;a href="https://www.reddit.com/r/AWSCertifications/" rel="noopener noreferrer"&gt;AWS Certifications Reddit community&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NB:&lt;/strong&gt; This Reddit community was extremely helpful throughout my preparation. If you're preparing for an AWS certification, I'd recommend spending some time going through the posts, discussions, study tips, and experiences shared there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The exam was testing something different
&lt;/h3&gt;

&lt;p&gt;The scenario-based questions weren't simply asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What does this AWS service do?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They were asking me to think through situations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which architecture best satisfies these requirements?&lt;/li&gt;
&lt;li&gt;What is the most cost-effective solution?&lt;/li&gt;
&lt;li&gt;Which solution provides the highest availability?&lt;/li&gt;
&lt;li&gt;What trade-offs should I consider in this scenario?&lt;/li&gt;
&lt;li&gt;Which service or configuration minimizes operational overhead?&lt;/li&gt;
&lt;li&gt;Which option satisfies the requirements with the least amount of change?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was when I realized I had to change my study approach.&lt;/p&gt;

&lt;p&gt;I was studying AWS services.&lt;/p&gt;

&lt;p&gt;The exam was testing whether I could &lt;strong&gt;use those services to make the right architectural decisions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That changed how I approached my preparation.&lt;/p&gt;

&lt;h3&gt;
  
  
  I stopped studying only for knowledge and started practicing for decision-making
&lt;/h3&gt;

&lt;p&gt;I went through Stephane Maarek's AWS Solutions Architect Associate course on Udemy to build and strengthen my understanding of the exam objectives and AWS services.&lt;/p&gt;

&lt;p&gt;But I didn't stop there.&lt;/p&gt;

&lt;p&gt;I used Tutorials Dojo extensively to get accustomed to the &lt;strong&gt;scenario-based nature of the questions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I primarily used the review mode, and I developed a simple rule for myself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every wrong answer had to become a learning opportunity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I got a question wrong, I didn't just look at the correct answer and move on.&lt;/p&gt;

&lt;p&gt;I read the explanation carefully to understand &lt;strong&gt;why I got it wrong&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I wanted to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why was the correct option correct?&lt;/li&gt;
&lt;li&gt;Why were the other options wrong?&lt;/li&gt;
&lt;li&gt;What requirement in the question eliminated the other choices?&lt;/li&gt;
&lt;li&gt;Was I misunderstanding an AWS service?&lt;/li&gt;
&lt;li&gt;Was I missing a key architectural concept?&lt;/li&gt;
&lt;li&gt;Was I choosing a technically valid solution when the question was asking for the &lt;em&gt;most cost-effective&lt;/em&gt;, &lt;em&gt;most highly available&lt;/em&gt;, or &lt;em&gt;least operationally complex&lt;/em&gt; solution?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whenever the explanation wasn't enough for me, I used ChatGPT to break the concept down further and help me understand the reasoning behind the answer.&lt;/p&gt;

&lt;p&gt;This became more important because I was trying to get better at recognizing the architectural pattern behind the question.&lt;/p&gt;

&lt;p&gt;I also used AWS Skill Builder to supplement the other resources I already had. I didn't want to take any chances with my preparation, so I used it to reinforce areas where I felt I needed more understanding.&lt;/p&gt;

&lt;p&gt;My preparation eventually became less about memorizing AWS services and more about understanding &lt;strong&gt;trade-offs, requirements, architecture, cost, availability, performance, and operational complexity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That shift from &lt;strong&gt;theoretical study to high-intensity, scenario-based practice&lt;/strong&gt; changed my preparation completely.&lt;/p&gt;

&lt;p&gt;And eventually, it paid off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I passed the AWS Certified Solutions Architect – Associate certification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is such a significant milestone for me in my Cloud and DevOps career.  &lt;/p&gt;

&lt;p&gt;Onto the next. &lt;/p&gt;

&lt;p&gt;Also if you have any questions, don't forget to reach out to me on &lt;a href="https://www.linkedin.com/in/chidubemm-okoli" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&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%2Fodyk95da83qwa39ginpu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fodyk95da83qwa39ginpu.jpeg" alt="AWS Solutions Architect Associate Badge" width="600" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awscertification</category>
      <category>cloudcomputing</category>
      <category>solutionsarchitect</category>
    </item>
    <item>
      <title>Choosing Amazon Bedrock Managed Knowledge Bases: An Architecture Decision in My RAG Application</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Wed, 23 Sep 2026 11:32:10 +0000</pubDate>
      <link>https://dev.to/duubemmm/choosing-amazon-bedrock-managed-knowledge-bases-an-architecture-decision-in-my-rag-application-5afj</link>
      <guid>https://dev.to/duubemmm/choosing-amazon-bedrock-managed-knowledge-bases-an-architecture-decision-in-my-rag-application-5afj</guid>
      <description>&lt;p&gt;When I started designing the RAG architecture for the internal AI assistant project, one of the first decisions I had to make was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where should the application's knowledge live, and which component should be responsible for turning that knowledge into something the model can retrieve?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are several ways to build a RAG system on AWS.&lt;/p&gt;

&lt;p&gt;I could assemble and manage the retrieval components myself, use a vector database directly, or use Amazon Bedrock Knowledge Bases to manage much of the RAG data pipeline.&lt;/p&gt;

&lt;p&gt;During the project, I explored the architecture from both perspectives before settling on an Amazon Bedrock Managed Knowledge Base.&lt;/p&gt;

&lt;p&gt;This article explains that decision and the technical tradeoffs behind it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Initial Problem
&lt;/h2&gt;

&lt;p&gt;The application needed to answer questions using internal company documents.&lt;/p&gt;

&lt;p&gt;The source documents included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Brand Guidelines
Script Bank
Caption Bank
Client Proposal Template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My initial mental model was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Documents
    ↓
Amazon S3
    ↓
Vector Storage
    ↓
Similarity Search
    ↓
Foundation Model
    ↓
Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But implementing that architecture means taking responsibility for considerably more than vector storage.&lt;/p&gt;

&lt;p&gt;The complete pipeline involves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document ingestion
       ↓
Document parsing
       ↓
Chunking
       ↓
Embedding generation
       ↓
Vector indexing
       ↓
Metadata
       ↓
Similarity search
       ↓
Retrieval
       ↓
Context construction
       ↓
Model invocation
       ↓
Source attribution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At that point, the most important architectural question became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Which parts of the RAG pipeline should my application own, and which parts should I delegate to a managed AWS service?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question led me toward Amazon Bedrock Managed Knowledge Bases.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Alternative: Amazon Bedrock Managed Knowledge Bases
&lt;/h2&gt;

&lt;p&gt;Amazon Bedrock Knowledge Bases provides a managed abstraction for building RAG applications.&lt;/p&gt;

&lt;p&gt;Instead of my application owning every component of the retrieval pipeline, the architecture becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    Company Documents
                           │
                           ▼
                      Amazon S3
                           │
                           ▼
               ┌───────────────────────┐
               │ Amazon Bedrock        │
               │ Managed Knowledge     │
               │ Base                  │
               │                       │
               │ Parsing               │
               │ Chunking              │
               │ Embeddings            │
               │ Indexing              │
               │ Retrieval             │
               └───────────┬───────────┘
                           │
                    Relevant Context
                           │
                           ▼
                  Application Layer
                           │
                           ▼
                  Foundation Model
                           │
                           ▼
                       Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates a significantly different responsibility boundary.&lt;/p&gt;

&lt;p&gt;My application remains responsible for the business logic, orchestration, model invocation, and user experience.&lt;/p&gt;

&lt;p&gt;The Knowledge Base handles much of the document ingestion and retrieval pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Exactly Does the Knowledge Base Manage?
&lt;/h2&gt;

&lt;p&gt;It is important not to think of a managed Knowledge Base as simply "a vector database."&lt;/p&gt;

&lt;p&gt;That misses most of the abstraction it provides.&lt;/p&gt;

&lt;p&gt;During ingestion, Amazon Bedrock processes the source content, splits it into chunks, generates embeddings, and indexes the resulting vectors for retrieval while maintaining their relationship to the source documents.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source Document
      │
      ▼
   Parsing
      │
      ▼
   Chunking
      │
      ▼
  Embeddings
      │
      ▼
 Vector Index
      │
      ▼
   Retrieval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the Knowledge Base owns a substantial portion of the RAG data pipeline that would otherwise have to be implemented and operated by the application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Amazon S3?
&lt;/h2&gt;

&lt;p&gt;I wanted the original company documents to remain in an object-storage layer rather than making the vector index the primary source of truth.&lt;/p&gt;

&lt;p&gt;Amazon S3 became that source layer.&lt;/p&gt;

&lt;p&gt;The document structure was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai-creative-content/

├── brand-guidelines/
├── past-scripts/
├── captions/
└── proposals/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the architecture a clear separation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S3
│
└── Original company documents

Knowledge Base
│
└── Searchable representation of those documents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The S3 data source can be configured with inclusion prefixes, allowing the Knowledge Base to process specific paths within the bucket. It also supports synchronization when documents are added, modified, or deleted.&lt;/p&gt;

&lt;p&gt;That separation became particularly useful because the original documents remain easy to manage independently of the retrieval layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating the S3 Data Source
&lt;/h2&gt;

&lt;p&gt;At the AWS API level, the Knowledge Base data source can be configured with an S3 bucket and optional inclusion prefixes.&lt;/p&gt;

&lt;p&gt;For example:&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;"s3Configuration"&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;"bucketArn"&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:s3:::ai-creative-content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"inclusionPrefixes"&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;"brand-guidelines/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"past-scripts/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"captions/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"proposals/"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"S3"&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;p&gt;The important part is that the Knowledge Base is not simply scanning an arbitrary bucket.&lt;/p&gt;

&lt;p&gt;The data source configuration tells Bedrock which S3 location contains the documents that belong to the Knowledge Base.&lt;/p&gt;

&lt;p&gt;The S3 bucket and Knowledge Base also need to satisfy the regional requirements, and the Knowledge Base service role needs permission to access the configured data source.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ingestion: Turning Documents into Searchable Knowledge
&lt;/h2&gt;

&lt;p&gt;Uploading a document to S3 is only the beginning.&lt;/p&gt;

&lt;p&gt;The document must be ingested into the Knowledge Base before it can participate in retrieval.&lt;/p&gt;

&lt;p&gt;The high-level flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PDF
 │
 ▼
Amazon S3
 │
 ▼
Knowledge Base Data Source
 │
 ▼
Ingestion Job
 │
 ├── Parse
 ├── Chunk
 ├── Generate Embeddings
 └── Index
 │
 ▼
Queryable Knowledge Base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ingestion transforms the source content into representations that can be searched semantically.&lt;/p&gt;

&lt;p&gt;This distinction is important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S3 upload
   ≠
Knowledge Base ingestion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A document existing in S3 does not automatically mean it is available for retrieval.&lt;/p&gt;




&lt;h2&gt;
  
  
  Starting an Ingestion Job
&lt;/h2&gt;

&lt;p&gt;Once the S3 data source is configured, an ingestion job can be started through the AWS Console or CLI.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws bedrock-agent start-ingestion-job &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--knowledge-base-id&lt;/span&gt; &amp;lt;knowledge-base-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-source-id&lt;/span&gt; &amp;lt;data-source-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command starts an ingestion job for the specified Knowledge Base and data source.&lt;/p&gt;

&lt;p&gt;The response includes an ingestion job ID, which can then be used to monitor the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws bedrock-agent get-ingestion-job &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--knowledge-base-id&lt;/span&gt; &amp;lt;knowledge-base-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-source-id&lt;/span&gt; &amp;lt;data-source-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ingestion-job-id&lt;/span&gt; &amp;lt;ingestion-job-id&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The operational flow is therefore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;StartIngestionJob
       │
       ▼
Ingestion Job
       │
       ▼
Monitor Status
       │
       ▼
   COMPLETE
       │
       ▼
Knowledge Base Ready
for Retrieval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This became an important operational distinction during the project.&lt;/p&gt;

&lt;p&gt;When troubleshooting RAG, I could no longer assume that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"The file exists in S3"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;meant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"The Knowledge Base can retrieve it."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The document has to successfully pass through the ingestion pipeline first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Incremental Synchronization
&lt;/h2&gt;

&lt;p&gt;Another useful property of the S3 data source is incremental synchronization.&lt;/p&gt;

&lt;p&gt;Suppose the Knowledge Base already contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Brand Guidelines
Script Bank
Caption Bank
Proposal Template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the caption bank is updated.&lt;/p&gt;

&lt;p&gt;The architecture does not require me to manually rebuild the entire retrieval system.&lt;/p&gt;

&lt;p&gt;After synchronization, the affected content can be reprocessed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                Existing Knowledge Base
                         │
                  Document Change
                         │
                         ▼
                    Amazon S3
                         │
                         ▼
                       Sync
                         │
                         ▼
              Detect Changed Content
                         │
                         ▼
                 Re-process Content
                         │
                ┌────────┼────────┐
                ▼        ▼        ▼
             Parsing  Chunking  Embedding
                         │
                         ▼
                       Index
                         │
                         ▼
                Updated Knowledge Base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the operational advantages of using a managed data source rather than implementing a custom document synchronization pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chunking
&lt;/h2&gt;

&lt;p&gt;Chunking is one of the most important stages of the ingestion process.&lt;/p&gt;

&lt;p&gt;A document can contain hundreds or thousands of lines. Treating the entire document as one retrieval unit makes precise retrieval difficult.&lt;/p&gt;

&lt;p&gt;Instead, the document is divided into smaller chunks.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Brand Guidelines
       │
       ├── Chunk 1: Company overview
       ├── Chunk 2: Brand positioning
       ├── Chunk 3: Brand voice
       ├── Chunk 4: Social media guidelines
       ├── Chunk 5: Visual identity
       └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amazon Bedrock supports multiple chunking strategies, including fixed-size, hierarchical, semantic, and no-chunking configurations depending on the Knowledge Base setup.&lt;/p&gt;

&lt;p&gt;The resulting chunks are then embedded and indexed for retrieval.&lt;/p&gt;

&lt;p&gt;This is an important architectural detail because retrieval quality is influenced not only by the vector store, but also by how the original documents were divided before indexing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Embeddings
&lt;/h2&gt;

&lt;p&gt;After chunking, each chunk needs a numerical representation that allows semantic similarity to be calculated.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document Chunk
      │
      ▼
Embedding Model
      │
      ▼
Vector Representation
      │
      ▼
Vector Index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At query time, the user's question is also represented in a form that can be compared against the indexed content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Question
      │
      ▼
Query Representation
      │
      ▼
Similarity Search
      │
      ▼
Relevant Chunks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Knowledge Base uses the embedding configuration selected during its creation to generate the vector representations used for retrieval.&lt;/p&gt;

&lt;p&gt;This is another responsibility I did not have to implement as a separate application pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Retrieval
&lt;/h2&gt;

&lt;p&gt;Once ingestion succeeds, the Knowledge Base can be queried.&lt;/p&gt;

&lt;p&gt;Suppose a staff member asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What tone should our social media captions use?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The retrieval flow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Question
      │
      ▼
Knowledge Base
      │
      ▼
Semantic Retrieval
      │
      ├── Brand Guidelines chunk
      ├── Caption Bank chunk
      └── Other relevant chunk
      │
      ▼
Retrieved Context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amazon Bedrock provides the &lt;code&gt;Retrieve&lt;/code&gt; operation for retrieving source chunks relevant to a query.&lt;/p&gt;

&lt;p&gt;This was important to my architecture because I wanted retrieval to remain an explicit stage of the application flow rather than hiding it behind a single black-box operation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Retrieval Count Matters
&lt;/h2&gt;

&lt;p&gt;The number of chunks retrieved is another configuration decision.&lt;/p&gt;

&lt;p&gt;For managed Knowledge Bases, &lt;code&gt;numberOfResults&lt;/code&gt; controls the maximum number of source chunks returned. The actual number can be lower if fewer relevant results are available.&lt;/p&gt;

&lt;p&gt;For example:&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;"retrievalConfiguration"&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;"managedSearchConfiguration"&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;"numberOfResults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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;p&gt;The important word is &lt;strong&gt;maximum&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;numberOfResults = 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Always return exactly five chunks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Return up to five relevant chunks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More retrieved content is not automatically better.&lt;/p&gt;

&lt;p&gt;If a question can be answered using two highly relevant chunks, adding several loosely related chunks may introduce unnecessary context into the model prompt.&lt;/p&gt;

&lt;p&gt;So retrieval count becomes a tuning parameter influenced by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document structure
Chunking strategy
Question complexity
Retrieval quality
Model context requirements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Retrieve vs. Generation
&lt;/h2&gt;

&lt;p&gt;Another important architectural distinction is separating retrieval from generation.&lt;/p&gt;

&lt;p&gt;The Knowledge Base can retrieve the relevant source material, while the application remains responsible for passing that context to the foundation model.&lt;/p&gt;

&lt;p&gt;For my architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Question
      │
      ▼
Retrieve
      │
      ▼
Relevant Chunks
      │
      ▼
Application Orchestrator
      │
      ▼
Claude Sonnet 4.5
      │
      ▼
Generated Response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation gives the application control over how retrieved information is incorporated into the model request.&lt;/p&gt;

&lt;p&gt;It also makes the system easier to debug.&lt;/p&gt;

&lt;p&gt;Instead of treating the final answer as one opaque operation, I can inspect two separate stages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retrieval
    ↓
Did I retrieve the right information?

Generation
    ↓
Did the model produce the right answer from that information?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are fundamentally different failure modes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Separation Mattered to My Application
&lt;/h2&gt;

&lt;p&gt;The application was not intended to be an autonomous agent.&lt;/p&gt;

&lt;p&gt;It was designed as an internal creative assistant with human review.&lt;/p&gt;

&lt;p&gt;So I wanted the architecture to remain explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Staff
 │
 ▼
Application
 │
 ▼
Retrieve Company Knowledge
 │
 ▼
Construct Context
 │
 ▼
Claude
 │
 ▼
Draft Response
 │
 ▼
Human Review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a clear boundary between:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;That boundary is useful both architecturally and operationally.&lt;/p&gt;

&lt;p&gt;If an answer is incorrect, I can investigate whether:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the wrong information was retrieved, or&lt;/li&gt;
&lt;li&gt;the correct information was retrieved but the model generated an incorrect response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is much more actionable than simply knowing that "the AI got it wrong."&lt;/p&gt;




&lt;h2&gt;
  
  
  Source Attribution
&lt;/h2&gt;

&lt;p&gt;Retrieval should also remain observable.&lt;/p&gt;

&lt;p&gt;A useful RAG system should allow the application to understand where retrieved information came from.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Retrieve&lt;/code&gt; response includes the retrieved source content and source-location information.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generated Response
       │
       ├── Source: Brand Guidelines
       ├── Source: Caption Bank
       └── Source: Script Bank
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For an internal assistant, this provides an additional layer of transparency.&lt;/p&gt;

&lt;p&gt;The employee should not have to blindly trust a generated response. The application should be able to associate the response with the company information used to produce it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Gained by Choosing the Managed Approach
&lt;/h2&gt;

&lt;p&gt;The biggest advantage was a clearer responsibility boundary.&lt;/p&gt;

&lt;p&gt;My application owns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authentication
API
Application Logic
Prompt Construction
Model Invocation
User Interface
Human Review
Access Control
Observability
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The managed Knowledge Base handles much of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data Source Integration
Document Ingestion
Parsing
Chunking
Embedding Generation
Vector Indexing
Retrieval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means fewer retrieval-specific components become part of my application code and operational surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Gave Up
&lt;/h2&gt;

&lt;p&gt;Choosing a managed abstraction also means giving up some low-level control.&lt;/p&gt;

&lt;p&gt;That tradeoff is important.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lower-level / Self-managed&lt;/th&gt;
&lt;th&gt;Managed Knowledge Base&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;More direct control over vector infrastructure&lt;/td&gt;
&lt;td&gt;Higher-level AWS abstraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More control over retrieval implementation&lt;/td&gt;
&lt;td&gt;Bedrock-managed retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More responsibility for ingestion&lt;/td&gt;
&lt;td&gt;Managed ingestion workflow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More infrastructure to configure&lt;/td&gt;
&lt;td&gt;Less application-owned infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More flexibility at the infrastructure layer&lt;/td&gt;
&lt;td&gt;Less low-level control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More operational responsibility&lt;/td&gt;
&lt;td&gt;Lower operational overhead&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So I would not describe the managed Knowledge Base as universally better.&lt;/p&gt;

&lt;p&gt;The appropriate architecture depends on the application's requirements.&lt;/p&gt;

&lt;p&gt;If an application required highly customized vector infrastructure or retrieval behavior, a lower-level architecture could make sense.&lt;/p&gt;

&lt;p&gt;For this project, I valued the managed ingestion and retrieval pipeline more than having complete ownership of the underlying vector infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Final Architecture
&lt;/h2&gt;

&lt;p&gt;After working through those tradeoffs, the AI architecture became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                       Company Documents
                              │
                              ▼
                         Amazon S3
                              │
                              ▼
                    S3 Data Source
                              │
                              ▼
                   Ingestion / Sync
                              │
                ┌─────────────┴─────────────┐
                │                           │
             Parsing                    Chunking
                │                           │
                └─────────────┬─────────────┘
                              │
                              ▼
                         Embeddings
                              │
                              ▼
                        Vector Index
                              │
                              ▼
                         Retrieval
                              │
                              ▼
                     Relevant Context
                              │
                              ▼
                  Application Orchestrator
                              │
                              ▼
                     Claude Sonnet 4.5
                              │
                              ▼
                       Draft Response
                              │
                              ▼
                        Human Review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broader application architecture was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         Staff User
                             │
                             ▼
                          Cognito
                             │
                             ▼
                       API Gateway
                             │
                             ▼
                    Chat Orchestrator
                             │
                  ┌──────────┴──────────┐
                  │                     │
                  ▼                     ▼
           Bedrock Knowledge      Claude Sonnet 4.5
                 Base                    │
                  │                      │
                  └──── Retrieved ───────┘
                         Context
                             │
                             ▼
                      Draft Response
                             │
                             ▼
                       Human Review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the knowledge pipeline was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Company Documents
       │
       ▼
      S3
       │
       ▼
Knowledge Base Data Source
       │
       ▼
    Ingestion
       │
       ├── Parsing
       ├── Chunking
       ├── Embeddings
       └── Indexing
       │
       ▼
    Retrieval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Architecture Decision in One Sentence
&lt;/h2&gt;

&lt;p&gt;If I had to summarize the decision:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I chose Amazon Bedrock Managed Knowledge Bases because I wanted AWS to handle much of the operational complexity of the RAG ingestion and retrieval pipeline while my application focused on authentication, orchestration, generation, and the user experience.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a more useful way to think about managed services than simply saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"AWS has a service that does this for me."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important architectural question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where should responsibility live?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What I Learned From the Decision
&lt;/h2&gt;

&lt;p&gt;The biggest lesson was that cloud architecture is not simply about choosing the most powerful service or building everything yourself.&lt;/p&gt;

&lt;p&gt;It is about defining boundaries.&lt;/p&gt;

&lt;p&gt;My initial mental model was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S3
 ↓
Vector Database
 ↓
LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After building the system, my mental model became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 SOURCE
                   │
                   ▼
                  S3
                   │
                   ▼
              INGESTION
                   │
          ┌────────┼────────┐
          ▼        ▼        ▼
       Parsing  Chunking  Embedding
                   │
                   ▼
                Index
                   │
                   ▼
               Retrieval
                   │
                   ▼
              Application
               Context
                   │
                   ▼
              Claude
                   │
                   ▼
              Response
                   │
                   ▼
             Human Review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shift was one of the most valuable parts of the architecture decision.&lt;/p&gt;

&lt;p&gt;I stopped thinking about RAG as simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Put documents in a vector database and ask an LLM questions."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, I started thinking about it as a pipeline with explicit responsibilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Storage
   ↓
Ingestion
   ↓
Representation
   ↓
Retrieval
   ↓
Context
   ↓
Generation
   ↓
Review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That mental model made it much easier to reason about where managed AWS infrastructure could provide value and where application-level control was still necessary.&lt;/p&gt;




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

&lt;p&gt;Amazon Bedrock Managed Knowledge Bases was not simply a shortcut for me.&lt;/p&gt;

&lt;p&gt;It was an architectural choice.&lt;/p&gt;

&lt;p&gt;I could have owned more of the ingestion and vector-search infrastructure myself. Instead, I chose to delegate that responsibility to a managed AWS service and keep my application focused on the parts that were specific to the product.&lt;/p&gt;

&lt;p&gt;The resulting responsibility boundaries were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Amazon S3
    → Source documents

Bedrock Knowledge Base
    → Ingestion + retrieval

Application
    → Orchestration + context construction

Claude
    → Generation

Human
    → Final review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this project, that division of responsibility gave me the balance I was looking for: enough control over application behavior without unnecessarily taking ownership of infrastructure that AWS could manage for me.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>Building an Internal AI Assistant on AWS: A Production-Ready RAG Architecture with Amazon Bedrock</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Mon, 21 Sep 2026 12:02:09 +0000</pubDate>
      <link>https://dev.to/duubemmm/building-an-internal-ai-assistant-on-aws-with-amazon-bedrock-and-managed-rag-26bp</link>
      <guid>https://dev.to/duubemmm/building-an-internal-ai-assistant-on-aws-with-amazon-bedrock-and-managed-rag-26bp</guid>
      <description>&lt;p&gt;AI is becoming an increasingly important part of how organizations handle day to day work. But introducing AI into an organization is not just about connecting an application to an LLM and allowing people to start asking questions.&lt;/p&gt;

&lt;p&gt;For an internal AI assistant to be useful, it needs access to the organization's own information. It also needs authentication, access control, protection around AI input and output, logging, and a clear separation between the AI system and the people responsible for reviewing its responses.&lt;/p&gt;

&lt;p&gt;In this project, I built an internal AI assistant on AWS that allows authenticated staff to interact with an AI assistant using the organization's internal documents as its knowledge source.&lt;/p&gt;

&lt;p&gt;The system uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Amplify for hosting the frontend&lt;/li&gt;
&lt;li&gt;Amazon Cognito for staff authentication&lt;/li&gt;
&lt;li&gt;Amazon API Gateway for the API layer&lt;/li&gt;
&lt;li&gt;AWS Lambda for application logic&lt;/li&gt;
&lt;li&gt;Amazon Bedrock Knowledge Bases for retrieval augmented generation&lt;/li&gt;
&lt;li&gt;Amazon Bedrock for the foundation model&lt;/li&gt;
&lt;li&gt;Amazon Bedrock Guardrails for controlling model input and output&lt;/li&gt;
&lt;li&gt;Amazon S3 for storing internal documents&lt;/li&gt;
&lt;li&gt;Amazon CloudWatch for logging and monitoring&lt;/li&gt;
&lt;li&gt;AWS IAM for permissions and access control&lt;/li&gt;
&lt;li&gt;Terraform for infrastructure provisioning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was not simply to get an AI model to generate a response.&lt;/p&gt;

&lt;p&gt;I wanted to build the complete application around the model.&lt;/p&gt;

&lt;p&gt;A staff member logs in, sends a request through the application, API Gateway validates the request, Lambda coordinates the AI workflow, relevant information is retrieved from the organization's documents through a managed Bedrock Knowledge Base, the model generates a response using that context, and the response is returned to the authenticated staff member.&lt;/p&gt;

&lt;p&gt;The system was also designed around a human review process. The assistant generates drafts and responses for staff to review. It does not automatically publish content or take actions on behalf of the organization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key takeaways
&lt;/h3&gt;

&lt;p&gt;Building this project gave me practical experience working with multiple AWS services as one system rather than learning each service in isolation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Building and hosting a static frontend on AWS with AWS Amplify&lt;/li&gt;
&lt;li&gt;Implementing authentication with Amazon Cognito&lt;/li&gt;
&lt;li&gt;Protecting an API with JWT authentication&lt;/li&gt;
&lt;li&gt;Building a serverless backend with Lambda&lt;/li&gt;
&lt;li&gt;Connecting Lambda to Amazon Bedrock&lt;/li&gt;
&lt;li&gt;Building a RAG workflow using Bedrock Knowledge Bases&lt;/li&gt;
&lt;li&gt;Using S3 as a document source&lt;/li&gt;
&lt;li&gt;Applying Bedrock Guardrails to AI requests and responses&lt;/li&gt;
&lt;li&gt;Debugging IAM permission issues&lt;/li&gt;
&lt;li&gt;Working with Bedrock model access and inference profiles&lt;/li&gt;
&lt;li&gt;Monitoring Lambda execution with CloudWatch&lt;/li&gt;
&lt;li&gt;Managing the infrastructure with Terraform&lt;/li&gt;
&lt;li&gt;Designing an AI workflow where humans remain responsible for the final output&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;There are many areas where employees repeatedly need to search through documents, look at previous work, check company guidelines, draft content, or find information that already exists somewhere inside the organization.&lt;/p&gt;

&lt;p&gt;The problem is that having the information does not necessarily mean that it is easy to use.&lt;/p&gt;

&lt;p&gt;Important documents can be spread across different files and folders. Previous scripts, captions, proposals, brand guidelines and other internal resources can become difficult to search through manually.&lt;/p&gt;

&lt;p&gt;This was the problem I wanted to address with this project.&lt;/p&gt;

&lt;p&gt;The idea was to build an internal AI assistant that could give staff a simpler way to interact with the organization's existing knowledge.&lt;/p&gt;

&lt;p&gt;Instead of manually searching through multiple documents before starting a task, a staff member could ask the assistant a question and have the system retrieve relevant information from the organization's internal knowledge base.&lt;br&gt;
I also wanted the responses to be grounded in the organization's own information rather than relying entirely on the model's general knowledge.&lt;/p&gt;

&lt;p&gt;That led me to use a Retrieval Augmented Generation architecture with Amazon Bedrock Knowledge Bases.&lt;/p&gt;

&lt;p&gt;At a high level, internal documents are stored in Amazon S3 and connected to a Bedrock Knowledge Base. When a staff member submits a request, the application retrieves relevant information from the knowledge base and uses that information as context for generating the response.&lt;/p&gt;

&lt;p&gt;But building the complete system required more than the RAG component.&lt;/p&gt;

&lt;p&gt;I also needed to solve authentication, API security, permissions, input and output controls, application logic, logging and infrastructure provisioning.&lt;/p&gt;

&lt;p&gt;That is what made this project interesting from an engineering perspective.&lt;/p&gt;
&lt;h2&gt;
  
  
  I was not just connecting an application to an AI model. I was building an internal application around it.
&lt;/h2&gt;
&lt;h2&gt;
  
  
  The Use Case
&lt;/h2&gt;

&lt;p&gt;The assistant was designed for internal staff who need to work with the organization's existing content and information.&lt;/p&gt;

&lt;p&gt;The knowledge base included areas such as&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Brand guidelines&lt;/li&gt;
&lt;li&gt;Previous scripts&lt;/li&gt;
&lt;li&gt;Captions&lt;/li&gt;
&lt;li&gt;Client proposals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The documents were organized in Amazon S3 using separate prefixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brand-guidelines/
past-scripts/
captions/
proposals/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a staff member could ask the assistant to create a draft based on the organization's existing style and previous work.&lt;/p&gt;

&lt;p&gt;Instead of relying only on the model's general knowledge, the application could retrieve relevant internal content and use it as context.&lt;/p&gt;

&lt;p&gt;This gives the system two important components.&lt;/p&gt;

&lt;p&gt;The model provides the language generation capability.&lt;/p&gt;

&lt;p&gt;The organization's documents provide the context.&lt;/p&gt;

&lt;p&gt;The application brings the two together.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem I Needed to Solve
&lt;/h2&gt;

&lt;p&gt;Before building the system, I had to answer a few practical questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. How would staff authenticate?
&lt;/h3&gt;

&lt;p&gt;The assistant was intended for internal use, so I needed an authentication mechanism that would prevent unauthenticated users from accessing the application API.&lt;/p&gt;

&lt;p&gt;I used Amazon Cognito User Pools for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. How would the frontend communicate with the backend securely?
&lt;/h3&gt;

&lt;p&gt;The browser needed to communicate with an API, but I did not want the frontend directly interacting with Bedrock or having broad AWS permissions.&lt;/p&gt;

&lt;p&gt;I used Amazon API Gateway as the API layer and configured a JWT authorizer using tokens issued by Cognito.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. How would the assistant use internal knowledge?
&lt;/h3&gt;

&lt;p&gt;A foundation model would not automatically know the organization's private documents.&lt;/p&gt;

&lt;p&gt;I needed a retrieval layer that could search the organization's content and provide relevant information to the model.&lt;/p&gt;

&lt;p&gt;This is where Amazon Bedrock Knowledge Bases came into the architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. How would the documents be managed?
&lt;/h3&gt;

&lt;p&gt;Amazon S3 became the document repository and the source for the Knowledge Base.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. How would I control what the AI could process and return?
&lt;/h3&gt;

&lt;p&gt;An internal AI assistant still needs controls around what users can submit and what the system can return.&lt;/p&gt;

&lt;p&gt;I used Amazon Bedrock Guardrails for this.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. How would I troubleshoot the system?
&lt;/h3&gt;

&lt;p&gt;With several managed services involved, I needed visibility into what was happening during each request.&lt;/p&gt;

&lt;p&gt;Amazon CloudWatch provided the logging and monitoring needed to troubleshoot the backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. How would I manage the infrastructure?
&lt;/h3&gt;

&lt;p&gt;I wanted the infrastructure to be reproducible rather than relying entirely on manual configuration through the AWS console.&lt;/p&gt;

&lt;p&gt;I used Terraform to provision and manage the AWS resources.&lt;/p&gt;




&lt;h2&gt;
  
  
  Solution Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The final architecture connects the staff member's browser to several AWS managed services.&lt;/p&gt;

&lt;p&gt;The main request path is:&lt;/p&gt;

&lt;p&gt;Staff → Amplify → Cognito → API Gateway → Lambda → Guardrails → Knowledge Base → Bedrock Model → Guardrails → Response&lt;/p&gt;

&lt;p&gt;The supporting data path is:&lt;/p&gt;

&lt;p&gt;S3 → Bedrock Knowledge Base → Document processing and indexing&lt;/p&gt;

&lt;p&gt;CloudWatch provides observability across the backend, while IAM controls access between the AWS services.&lt;/p&gt;

&lt;p&gt;Here's the complete architecture.&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%2Fwucbur03zx1lulcbkdwn.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%2Fwucbur03zx1lulcbkdwn.jpg" alt="Architectural diagram" width="799" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Request Flow
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The staff member opens the frontend hosted on AWS Amplify.&lt;/li&gt;
&lt;li&gt;The staff member authenticates through Amazon Cognito.&lt;/li&gt;
&lt;li&gt;Cognito issues the authentication tokens required by the application.&lt;/li&gt;
&lt;li&gt;The frontend sends the authenticated request to Amazon API Gateway.&lt;/li&gt;
&lt;li&gt;API Gateway validates the JWT before allowing the request to reach Lambda.&lt;/li&gt;
&lt;li&gt;AWS Lambda receives the request and acts as the orchestration layer.&lt;/li&gt;
&lt;li&gt;The request passes through the configured Bedrock Guardrails.&lt;/li&gt;
&lt;li&gt;Lambda retrieves relevant information from the Bedrock Knowledge Base.&lt;/li&gt;
&lt;li&gt;The retrieved information is provided as context for the model generation step.&lt;/li&gt;
&lt;li&gt;Amazon Bedrock generates the response.&lt;/li&gt;
&lt;li&gt;The generated response passes through the output controls.&lt;/li&gt;
&lt;li&gt;Lambda formats the response and returns it through API Gateway.&lt;/li&gt;
&lt;li&gt;The frontend displays the response to the authenticated staff member.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important part of this architecture is the separation of responsibilities. The browser handles the user interface, Cognito handles authentication, API Gateway protects the API boundary, Lambda coordinates the workflow, Bedrock handles retrieval and generation, S3 stores the source documents, and CloudWatch provides visibility into the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step Implementation
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1. Building the Frontend
&lt;/h2&gt;

&lt;p&gt;I started with a simple frontend.&lt;/p&gt;

&lt;p&gt;I did not need a large frontend framework for this application because the main purpose of the interface was to provide staff with a simple way to authenticate, enter a prompt and view the generated response.&lt;/p&gt;

&lt;p&gt;The frontend was built using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTML&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application contained the basic interface required for the assistant.&lt;/p&gt;

&lt;p&gt;The important part here was that the frontend was intentionally kept simple.&lt;/p&gt;

&lt;p&gt;Most of the complexity of the application lives in the backend and AWS services.&lt;/p&gt;

&lt;p&gt;The browser is responsible for providing the user interface and communicating with the API.&lt;/p&gt;

&lt;p&gt;It should not be responsible for directly accessing the organization's S3 documents, invoking Bedrock models or managing broad AWS permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosting the frontend
&lt;/h3&gt;

&lt;p&gt;I hosted the static frontend using AWS Amplify.&lt;/p&gt;

&lt;p&gt;The deployment model was straightforward.&lt;/p&gt;

&lt;p&gt;The frontend files were deployed to Amplify and served to staff through the application's web interface.&lt;/p&gt;

&lt;p&gt;This also gave me a clean separation between the presentation layer and the backend.&lt;/p&gt;

&lt;p&gt;The browser communicates with the API.&lt;/p&gt;

&lt;p&gt;The API communicates with the backend services.&lt;/p&gt;

&lt;p&gt;The backend communicates with Bedrock and the organization's internal knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: Frontend of the internal ai assistant deployed with amplify&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftjx7uvyyo0bqjdc9cjt1.jpeg" alt="Frontend of the internal ai assistant deployed with amplify" width="800" height="419"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  2. Adding Staff Authentication with Amazon Cognito
&lt;/h2&gt;

&lt;p&gt;The next problem was authentication.&lt;/p&gt;

&lt;p&gt;Because this was an internal application, I needed to make sure that only authenticated staff could access the assistant.&lt;/p&gt;

&lt;p&gt;I used Amazon Cognito User Pools for this.&lt;/p&gt;

&lt;p&gt;The Cognito User Pool became the identity layer for the application.&lt;/p&gt;

&lt;p&gt;I configured the application so that users could authenticate through Cognito's managed login experience.&lt;/p&gt;

&lt;p&gt;The basic authentication flow was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Staff member
      ↓
Amazon Cognito Managed Login
      ↓
Successful authentication
      ↓
Authentication tokens
      ↓
Frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend then uses the authentication information when communicating with the backend API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Cognito was placed before the API
&lt;/h3&gt;

&lt;p&gt;I wanted authentication to happen before the request reached the application logic.&lt;/p&gt;

&lt;p&gt;This meant the Lambda function did not need to implement the basic responsibility of determining whether a request was authenticated.&lt;/p&gt;

&lt;p&gt;Instead, API Gateway could validate the JWT issued by Cognito.&lt;/p&gt;

&lt;p&gt;The request path therefore became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser
   ↓
Cognito authentication
   ↓
JWT
   ↓
API Gateway
   ↓
JWT validation
   ↓
Lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave me a cleaner separation of responsibilities.&lt;/p&gt;

&lt;p&gt;Cognito handled identity.&lt;/p&gt;

&lt;p&gt;API Gateway handled API-level authentication.&lt;/p&gt;

&lt;p&gt;Lambda handled application logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: Cognito User Pool overview&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8laxhxfn68ojlpxueiyc.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8laxhxfn68ojlpxueiyc.jpeg" alt="Cognito User Pool overview" width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Creating the API with Amazon API Gateway
&lt;/h2&gt;

&lt;p&gt;Once authentication was in place, I needed an API for the frontend to communicate with the backend.&lt;/p&gt;

&lt;p&gt;I used Amazon API Gateway with an HTTP API.&lt;/p&gt;

&lt;p&gt;The application exposed a chat endpoint for the assistant.&lt;/p&gt;

&lt;p&gt;The important part of this configuration was the JWT authorizer.&lt;/p&gt;

&lt;p&gt;The API Gateway route was configured to validate tokens issued by the Cognito User Pool.&lt;/p&gt;

&lt;p&gt;That meant a request without a valid token should not reach the Lambda function.&lt;/p&gt;

&lt;p&gt;I tested this during implementation.&lt;/p&gt;

&lt;p&gt;When I attempted to access the API without valid authentication, the API returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;401 Unauthorized
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was an important test because it confirmed that the JWT authorizer was enforcing authentication at the API layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: API Gateway route and JWT authorizer&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnnmjsa0ofl54u6e2tcf7.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnnmjsa0ofl54u6e2tcf7.jpeg" alt="API Gateway route and JWT authorizer" width="799" height="356"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Building the Lambda Chat Orchestrator
&lt;/h2&gt;

&lt;p&gt;After API Gateway validates the request, the request is passed to AWS Lambda.&lt;/p&gt;

&lt;p&gt;I used a Python Lambda function as the chat orchestrator.&lt;/p&gt;

&lt;p&gt;The Lambda function became the central piece connecting the application to the Bedrock services.&lt;/p&gt;

&lt;p&gt;Instead of allowing the frontend to directly communicate with Bedrock, the Lambda function controlled the workflow.&lt;/p&gt;

&lt;p&gt;Conceptually, the Lambda function is responsible for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive request
      ↓
Validate/process input
      ↓
Apply input guardrail
      ↓
Retrieve relevant knowledge
      ↓
Generate response
      ↓
Apply output guardrail
      ↓
Return response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was one of the most important architectural decisions in the project.&lt;/p&gt;

&lt;p&gt;The frontend does not need to know how the Bedrock Knowledge Base works.&lt;/p&gt;

&lt;p&gt;It does not need to know the model configuration.&lt;/p&gt;

&lt;p&gt;It does not need direct permissions to invoke the model.&lt;/p&gt;

&lt;p&gt;It simply sends a request to the API.&lt;/p&gt;

&lt;p&gt;Lambda handles the orchestration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: Lambda configuration&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwhxlo8d4uyt8e3d3a9i.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwhxlo8d4uyt8e3d3a9i.jpeg" alt="Lambda configuration" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Storing Internal Documents in Amazon S3
&lt;/h2&gt;

&lt;p&gt;The assistant needed access to the organization's existing information.&lt;/p&gt;

&lt;p&gt;I used Amazon S3 as the document repository.&lt;/p&gt;

&lt;p&gt;I organized the documents into separate prefixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brand-guidelines/
past-scripts/
captions/
proposals/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made the document source easier to organize and gave the knowledge base a structured location from which to retrieve information.&lt;/p&gt;

&lt;p&gt;The important point here is that S3 is not the component generating the answer.&lt;/p&gt;

&lt;p&gt;S3 stores the source documents.&lt;/p&gt;

&lt;p&gt;The Bedrock Knowledge Base uses those documents as its data source.&lt;/p&gt;

&lt;p&gt;That distinction becomes important when looking at the RAG workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: S3 bucket and prefixes&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bf2txerjp20q9i309kw.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9bf2txerjp20q9i309kw.jpeg" alt="S3 bucket and prefixes" width="799" height="352"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Creating the Amazon Bedrock Knowledge Base
&lt;/h2&gt;

&lt;p&gt;This was the part of the architecture that allowed the assistant to work with the organization's internal knowledge.&lt;/p&gt;

&lt;p&gt;I created a managed Amazon Bedrock Knowledge Base and connected it to the S3 document source.&lt;/p&gt;

&lt;p&gt;The documents then go through the knowledge base ingestion process.&lt;/p&gt;

&lt;p&gt;At a high level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;S3 documents
     ↓
Knowledge Base data source
     ↓
Ingestion
     ↓
Document processing
     ↓
Chunking
     ↓
Embeddings
     ↓
Vector representation
     ↓
Retrieval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a staff member asks a question, the system can retrieve relevant chunks from the indexed content.&lt;/p&gt;

&lt;p&gt;The model can then use those retrieved chunks when generating its response.&lt;/p&gt;

&lt;h3&gt;
  
  
  One of the problems I encountered
&lt;/h3&gt;

&lt;p&gt;The first ingestion test did not work as expected.&lt;/p&gt;

&lt;p&gt;The issue was not with the PDF itself.&lt;/p&gt;

&lt;p&gt;The problem was the relationship between the S3 object location and the configured data source.&lt;/p&gt;

&lt;p&gt;The knowledge base was configured to look at a particular S3 prefix, while the test document was not located where the data source expected it.&lt;/p&gt;

&lt;p&gt;After correcting the S3 location and running ingestion again, the ingestion job completed successfully.&lt;/p&gt;

&lt;p&gt;The result showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Status: COMPLETE
Scanned: 1
Indexed: 1
Failed: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was a useful debugging lesson because it showed that a RAG system can fail before the model is ever involved.&lt;/p&gt;

&lt;p&gt;If the knowledge base does not ingest the documents correctly, there is nothing useful for the model to retrieve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: Knowledge Base configuration&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqddxfnvra53xm5bf0va.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcqddxfnvra53xm5bf0va.jpeg" alt="Knowledge Base configuration" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Adding Amazon Bedrock Guardrails
&lt;/h2&gt;

&lt;p&gt;The next part of the implementation was controlling the AI interaction.&lt;/p&gt;

&lt;p&gt;I used Amazon Bedrock Guardrails to add controls around the assistant's input and output.&lt;/p&gt;

&lt;p&gt;The Lambda orchestration flow therefore became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User prompt
    ↓
Input Guardrail
    ↓
Knowledge retrieval
    ↓
Model generation
    ↓
Output Guardrail
    ↓
Final response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guardrail configuration was an important part of the project because it demonstrated that the AI model was not being treated as an unrestricted component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Guardrail debugging
&lt;/h3&gt;

&lt;p&gt;I also ran into an issue with Guardrail versioning.&lt;/p&gt;

&lt;p&gt;I had changed the guardrail configuration and expected the active version to reflect the new configuration.&lt;/p&gt;

&lt;p&gt;However, the existing guardrail version was immutable.&lt;/p&gt;

&lt;p&gt;A configuration change did not automatically mutate the already-created version.&lt;/p&gt;

&lt;p&gt;During testing, an address was still being detected even after I had removed that configuration from the desired Terraform definition.&lt;/p&gt;

&lt;p&gt;The test returned:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;and identified the address category.&lt;/p&gt;

&lt;p&gt;This made it clear that the Lambda was still referencing the older guardrail version.&lt;/p&gt;

&lt;p&gt;I resolved this by replacing the guardrail version through Terraform and then verifying that the Lambda was using the new version.&lt;/p&gt;

&lt;p&gt;The Terraform operation was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply &lt;span class="nt"&gt;-replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws_bedrock_guardrail_version.creative_assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was one of the more useful lessons from the project.&lt;/p&gt;

&lt;p&gt;With versioned AWS resources, changing the Terraform configuration does not always mean that an existing immutable version will change in place.&lt;/p&gt;

&lt;p&gt;You need to understand which resource is actually being referenced at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: Guardrail overview&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq0iib7pg3ujl974h16ba.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq0iib7pg3ujl974h16ba.jpeg" alt="Guardrail overview" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Connecting the Lambda Function to the Bedrock Model
&lt;/h2&gt;

&lt;p&gt;After the knowledge retrieval layer was working, I connected the Lambda orchestrator to the Bedrock foundation model.&lt;/p&gt;

&lt;p&gt;The Lambda function needed permissions to invoke the model.&lt;/p&gt;

&lt;p&gt;This introduced another issue during testing.&lt;/p&gt;

&lt;p&gt;The application initially reached the knowledge retrieval stage successfully, but model generation failed because the Lambda execution role did not have the required &lt;code&gt;bedrock:InvokeModel&lt;/code&gt; permission for the model invocation path.&lt;/p&gt;

&lt;p&gt;This was visible in the CloudWatch logs.&lt;/p&gt;

&lt;p&gt;The useful part of the debugging process was that the logs showed that the earlier parts of the workflow were already working.&lt;/p&gt;

&lt;p&gt;The input guardrail had passed.&lt;/p&gt;

&lt;p&gt;The knowledge base had returned a result.&lt;/p&gt;

&lt;p&gt;The failure happened when the application attempted to invoke the model.&lt;/p&gt;

&lt;p&gt;That narrowed the problem down to the model invocation and IAM configuration rather than the entire application.&lt;/p&gt;

&lt;p&gt;I updated the IAM permissions and continued testing.&lt;/p&gt;

&lt;p&gt;I also encountered an issue with model throughput.&lt;/p&gt;

&lt;p&gt;The base Claude Sonnet 4.5 foundation model identifier did not support the on-demand invocation pattern I was using.&lt;/p&gt;

&lt;p&gt;The solution was to use the appropriate US inference profile for Claude Sonnet 4.5.&lt;/p&gt;

&lt;p&gt;The model configuration was then updated in Terraform and the Lambda environment.&lt;/p&gt;

&lt;p&gt;This was another useful AWS lesson.&lt;/p&gt;

&lt;p&gt;Having access to a model in Amazon Bedrock does not automatically mean that every model identifier can be invoked using every invocation method.&lt;/p&gt;

&lt;p&gt;The model ID, inference profile and IAM permissions all need to line up with the way the application is invoking the model.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. IAM Permissions
&lt;/h2&gt;

&lt;p&gt;IAM was used throughout the architecture to control which AWS resources each component could access.&lt;/p&gt;

&lt;p&gt;The frontend does not need permission to invoke Bedrock.&lt;/p&gt;

&lt;p&gt;The frontend does not need permission to read the internal S3 documents.&lt;/p&gt;

&lt;p&gt;The Lambda function is the component that needs permissions to interact with the services required for the AI workflow.&lt;/p&gt;

&lt;p&gt;This is where the principle of least privilege becomes important.&lt;/p&gt;

&lt;p&gt;Instead of giving the application broad AWS permissions, I created permissions around the actual operations required by the system.&lt;/p&gt;

&lt;p&gt;The Lambda execution role needed access to the relevant Bedrock operations and supporting resources.&lt;/p&gt;

&lt;p&gt;The Knowledge Base also needed the permissions required to access its configured data source.&lt;/p&gt;

&lt;p&gt;This separation helped keep the trust boundaries clear.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend
   ↓
API Gateway
   ↓
Lambda
   ↓
Bedrock services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser never receives broad AWS permissions that would allow it to directly interact with the internal infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Adding CloudWatch for Observability
&lt;/h2&gt;

&lt;p&gt;Once several AWS services are connected together, debugging becomes much easier when there is a clear place to inspect what happened.&lt;/p&gt;

&lt;p&gt;I used Amazon CloudWatch for Lambda logging and monitoring.&lt;/p&gt;

&lt;p&gt;CloudWatch became particularly useful during the problems I encountered while testing the system.&lt;/p&gt;

&lt;p&gt;For example, one request initially returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500
Unable to generate the draft.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the frontend alone, that error did not tell me much.&lt;/p&gt;

&lt;p&gt;The CloudWatch logs provided the actual execution path.&lt;/p&gt;

&lt;p&gt;I could see that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input Guardrail
       ↓
Passed

Knowledge Base
       ↓
Retrieved result

Model invocation
       ↓
Failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That completely changed the debugging process.&lt;/p&gt;

&lt;p&gt;Instead of guessing whether Cognito, API Gateway, Lambda, the Knowledge Base or the model was responsible, I could follow the execution path and identify the stage where the request failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot: CloudWatch logs&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F16r7oprimeqttyfymh1p.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F16r7oprimeqttyfymh1p.jpeg" alt="CloudWatch logs" width="800" height="372"&gt;&lt;/a&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%2Fc6nonsymmm9l7da0pl85.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6nonsymmm9l7da0pl85.jpeg" alt="CloudWatch logs" width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Managing the Infrastructure with Terraform
&lt;/h2&gt;

&lt;p&gt;I used Terraform to provision and manage the AWS infrastructure for the project.&lt;/p&gt;

&lt;p&gt;By this point, the application involved several services, including Cognito, API Gateway, Lambda, S3, Bedrock Knowledge Bases, Bedrock Guardrails, IAM and CloudWatch. Creating and configuring all of these manually through the AWS console would make the environment harder to reproduce and maintain.&lt;/p&gt;

&lt;p&gt;I therefore defined the infrastructure as code.&lt;/p&gt;

&lt;p&gt;The Terraform configuration was split into separate files based on the resources they managed, including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform/
├── api-gateway.tf
├── cloudwatch.tf
├── cognito.tf
├── guardrail.tf
├── iam.tf
├── knowledge-base.tf
├── lambda.tf
├── s3.tf
├── variables.tf
└── outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also became useful during debugging.&lt;/p&gt;

&lt;p&gt;For example, when I discovered that the Claude Sonnet 4.5 model invocation required a different inference profile and corresponding IAM configuration, I could make the changes in Terraform and use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to see exactly what Terraform intended to change before applying it.&lt;/p&gt;

&lt;p&gt;I also ran into an issue with the Bedrock Guardrail version. The configuration had changed, but the existing guardrail version was immutable, so changing the Terraform configuration alone did not update the version being used by Lambda.&lt;/p&gt;

&lt;p&gt;I resolved this by explicitly replacing the guardrail version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform apply &lt;span class="nt"&gt;-replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aws_bedrock_guardrail_version.creative_assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was one of the practical benefits of using Terraform in the project. Infrastructure changes, including changes required while debugging, could be tracked and reproduced rather than relying entirely on manual console changes.&lt;/p&gt;

&lt;p&gt;Terraform was therefore not part of the runtime request path. It was the infrastructure layer used to provision and manage the AWS resources that made the application possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing the Complete Workflow
&lt;/h2&gt;

&lt;p&gt;Once the individual components were configured, I tested the application as a complete system.&lt;/p&gt;

&lt;p&gt;The final workflow was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Staff member
      ↓
AWS Amplify frontend
      ↓
Amazon Cognito authentication
      ↓
JWT
      ↓
Amazon API Gateway
      ↓
JWT validation
      ↓
AWS Lambda
      ↓
Amazon Bedrock Guardrail
      ↓
Amazon Bedrock Knowledge Base
      ↓
Relevant internal context
      ↓
Claude Sonnet 4.5
      ↓
Output Guardrail
      ↓
Lambda response
      ↓
API Gateway
      ↓
Frontend
      ↓
Staff member
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CloudWatch provided visibility into the backend execution while Terraform provided the infrastructure management layer around the application.&lt;/p&gt;

&lt;p&gt;The important thing about the architecture is that each service has a specific responsibility.&lt;/p&gt;

&lt;p&gt;The frontend provides the interface.&lt;/p&gt;

&lt;p&gt;Cognito handles identity.&lt;/p&gt;

&lt;p&gt;API Gateway protects the API boundary.&lt;/p&gt;

&lt;p&gt;Lambda orchestrates the workflow.&lt;/p&gt;

&lt;p&gt;S3 stores the source documents.&lt;/p&gt;

&lt;p&gt;Bedrock Knowledge Bases handles retrieval.&lt;/p&gt;

&lt;p&gt;Bedrock provides the model.&lt;/p&gt;

&lt;p&gt;Guardrails provide AI safety controls.&lt;/p&gt;

&lt;p&gt;IAM controls permissions.&lt;/p&gt;

&lt;p&gt;CloudWatch provides observability.&lt;/p&gt;

&lt;p&gt;Terraform manages the infrastructure.&lt;/p&gt;

&lt;p&gt;This gave me a reproducible way to manage the environment instead of relying entirely on changes made manually through the AWS console.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned Building It
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this project was that building an AI application is not simply a matter of choosing a model.&lt;/p&gt;

&lt;p&gt;The model was only one component of the system.&lt;/p&gt;

&lt;p&gt;Most of the engineering work was around everything surrounding it.&lt;/p&gt;

&lt;p&gt;Authentication had to work.&lt;/p&gt;

&lt;p&gt;The API had to reject unauthenticated requests.&lt;/p&gt;

&lt;p&gt;The documents had to be ingested correctly.&lt;/p&gt;

&lt;p&gt;The knowledge base had to return useful context.&lt;/p&gt;

&lt;p&gt;The Lambda execution role needed the correct permissions.&lt;/p&gt;

&lt;p&gt;The model invocation method had to match the selected model and inference profile.&lt;/p&gt;

&lt;p&gt;Guardrail versions needed to be managed correctly.&lt;/p&gt;

&lt;p&gt;And when something failed, CloudWatch needed to provide enough information to understand where the failure occurred.&lt;/p&gt;

&lt;p&gt;That was what made the project useful to me from a cloud engineering perspective.&lt;/p&gt;

&lt;p&gt;I was able to work with several AWS services as one system instead of treating each service as an isolated topic.&lt;/p&gt;

&lt;p&gt;The final result was an internal AI assistant where authenticated staff can interact with an AI system grounded in the organization's own content, while keeping the actual content generation process under human review.&lt;/p&gt;

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

&lt;p&gt;This project started with a relatively simple idea.&lt;/p&gt;

&lt;p&gt;Give staff an easier way to work with information that already exists inside the organization.&lt;/p&gt;

&lt;p&gt;The implementation ended up involving authentication, API security, serverless computing, object storage, managed RAG, foundation models, guardrails, IAM, monitoring and infrastructure as code.&lt;/p&gt;

&lt;p&gt;That was the part I found most valuable.&lt;/p&gt;

&lt;p&gt;The AI model is only one piece of the application.&lt;/p&gt;

&lt;p&gt;The real engineering challenge is building the system around it in a way that is secure, observable, reproducible and useful to the people who are going to use it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>rag</category>
    </item>
    <item>
      <title>Building a Custom VPC on AWS with Terraform: Understanding the Architecture</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Wed, 26 Aug 2026 21:03:23 +0000</pubDate>
      <link>https://dev.to/duubemmm/building-a-custom-vpc-on-aws-with-terraform-understanding-the-architecture-46dm</link>
      <guid>https://dev.to/duubemmm/building-a-custom-vpc-on-aws-with-terraform-understanding-the-architecture-46dm</guid>
      <description>&lt;p&gt;A VPC (Virtual Private Cloud) is the foundation for networking in AWS. It provides an isolated virtual network where AWS resources such as EC2 instances, databases, and application servers can communicate with each other and with external services in a controlled way.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/Duubemmm/terraform-aws-portfolio/tree/main/vpc" rel="noopener noreferrer"&gt;this project&lt;/a&gt;, I provisioned a custom VPC using Terraform and configured public and private subnets, routing, internet connectivity, security controls, an EC2 instance and an S3 Gateway VPC Endpoint.&lt;/p&gt;

&lt;p&gt;More importantly, this project helped me understand how these networking components fit together rather than treating them as isolated AWS resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The infrastructure consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A custom VPC using &lt;code&gt;10.0.0.0/16&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A public subnet using &lt;code&gt;10.0.1.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A private subnet using &lt;code&gt;10.0.2.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Two Availability Zones&lt;/li&gt;
&lt;li&gt;An Internet Gateway&lt;/li&gt;
&lt;li&gt;Separate public and private route tables&lt;/li&gt;
&lt;li&gt;A security group allowing SSH only from my IP&lt;/li&gt;
&lt;li&gt;An EC2 instance in the public subnet&lt;/li&gt;
&lt;li&gt;An S3 Gateway VPC Endpoint connected to the private route table&lt;/li&gt;
&lt;li&gt;DNS support and DNS hostnames enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture can be represented as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         INTERNET
                             │
                             │
                    ┌────────▼────────┐
                    │ Internet Gateway│
                    └────────┬────────┘
                             │
              ┌──────────────┴──────────────┐
              │            VPC              │
              │         10.0.0.0/16         │
              │                             │
              │  ┌───────────────────────┐  │
              │  │    Public Subnet      │  │
              │  │     10.0.1.0/24       │  │
              │  │      eu-west-2c        │  │
              │  │                       │  │
              │  │        EC2             │  │
              │  │         │              │  │
              │  │   Security Group      │  │
              │  └───────────────────────┘  │
              │                             │
              │  ┌───────────────────────┐  │
              │  │   Private Subnet      │  │
              │  │     10.0.2.0/24       │  │
              │  │      eu-west-2b        │  │
              │  │                       │  │
              │  │   Private Route Table │  │
              │  │          │            │  │
              │  │          ▼            │  │
              │  │    S3 Gateway        │  │
              │  │      Endpoint        │  │
              │  └───────────────────────┘  │
              │                             │
              └─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Creating the VPC
&lt;/h2&gt;

&lt;p&gt;The VPC is the top-level network boundary for the infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_support&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_hostnames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"main-vpc"&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 VPC uses the CIDR block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10.0.0.0/16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides the address space from which the subnets are created.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/16&lt;/code&gt; gives the VPC a large address range, while smaller &lt;code&gt;/24&lt;/code&gt; networks can be carved out for individual subnets.&lt;/p&gt;

&lt;p&gt;I also enabled DNS support and DNS hostnames so resources inside the VPC can use AWS-provided DNS functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Dividing the VPC into Subnets
&lt;/h2&gt;

&lt;p&gt;A VPC is a large network, so it is common to divide it into smaller networks called subnets.&lt;/p&gt;

&lt;p&gt;I created two:&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:  10.0.1.0/24
Private subnet: 10.0.2.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The public subnet is located in &lt;code&gt;eu-west-2c&lt;/code&gt;, while the private subnet is located in &lt;code&gt;eu-west-2b&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_subnet_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2c"&lt;/span&gt;
  &lt;span class="nx"&gt;map_public_ip_on_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"public-subnet"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2b"&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"private-subnet"&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;A subnet belongs to one Availability Zone.&lt;/p&gt;

&lt;p&gt;Using different Availability Zones also introduces the basic concept of fault isolation: resources can be distributed across physically separate Availability Zones instead of depending entirely on one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes a subnet public?
&lt;/h3&gt;

&lt;p&gt;The name &lt;code&gt;public-subnet&lt;/code&gt; does not make a subnet public.&lt;/p&gt;

&lt;p&gt;A subnet becomes public when its associated route table provides a route to an Internet Gateway.&lt;/p&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Internet Gateway
&lt;/h2&gt;

&lt;p&gt;The Internet Gateway provides connectivity between the VPC and the internet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"gw"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"main-igw"&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;However, simply attaching an Internet Gateway to the VPC does not make every subnet public.&lt;/p&gt;

&lt;p&gt;The subnet must have a route that directs internet-bound traffic to the Internet Gateway.&lt;/p&gt;

&lt;p&gt;That brings us to route tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Route Tables
&lt;/h2&gt;

&lt;p&gt;A route table determines where network traffic should be sent.&lt;/p&gt;

&lt;p&gt;For the public subnet, I created a route table with a default route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"public-rt"&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 important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.0.0.0/0 → Internet Gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0.0.0.0/0&lt;/code&gt; represents all IPv4 destinations.&lt;/p&gt;

&lt;p&gt;Therefore, this route essentially says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If traffic is destined for somewhere outside the VPC, send it toward the Internet Gateway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The public subnet is then associated with this route table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The private subnet has a separate route table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"private-rt"&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;There is no direct route from this route table to the Internet Gateway.&lt;/p&gt;

&lt;p&gt;Therefore, resources in the private subnet do not have a direct route to the public internet.&lt;/p&gt;

&lt;p&gt;This is what makes the subnet private from a routing perspective.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Public vs Private Communication
&lt;/h2&gt;

&lt;p&gt;One of the most useful things I learned from this project is that "private" does not mean "cannot communicate with anything."&lt;/p&gt;

&lt;p&gt;Both subnets belong to the same VPC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VPC: 10.0.0.0/16
Public subnet:  10.0.1.0/24
Private subnet: 10.0.2.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VPC has a local route that allows communication within the VPC.&lt;/p&gt;

&lt;p&gt;Therefore, a resource in the private subnet can communicate with a resource in the public subnet using their private IP addresses, assuming security controls allow the traffic.&lt;/p&gt;

&lt;p&gt;The difference is that the private subnet does not have a direct route to the public internet.&lt;/p&gt;

&lt;p&gt;A simple way to think about the components is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route table:&lt;/strong&gt; Where should traffic go?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internet Gateway:&lt;/strong&gt; Provides a path between the VPC and the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Group:&lt;/strong&gt; Is the traffic allowed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet:&lt;/strong&gt; Which section of the VPC does the resource belong to?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Security Groups
&lt;/h2&gt;

&lt;p&gt;I created a security group specifically for SSH access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"allow_ssh"&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="s2"&gt;"allow_ssh"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow SSH inbound, all outbound"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inbound rule allows TCP traffic on port 22 only from my IP address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_security_group_ingress_rule"&lt;/span&gt; &lt;span class="s2"&gt;"ssh_ipv4"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_ipv4&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_ip&lt;/span&gt;
  &lt;span class="nx"&gt;from_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
  &lt;span class="nx"&gt;ip_protocol&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My IP is represented as a &lt;code&gt;/32&lt;/code&gt; CIDR, meaning a single IPv4 address.&lt;/p&gt;

&lt;p&gt;This is preferable to opening SSH to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which would allow SSH attempts from any IPv4 address.&lt;/p&gt;

&lt;p&gt;For outbound traffic, I allowed all IPv4 traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_security_group_egress_rule"&lt;/span&gt; &lt;span class="s2"&gt;"allow_all_ipv4"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_ipv4&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
  &lt;span class="nx"&gt;ip_protocol&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security groups are stateful, meaning return traffic for an allowed connection is automatically permitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Launching the EC2 Instance
&lt;/h2&gt;

&lt;p&gt;The EC2 instance is placed inside the public subnet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ami_id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t2.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_key_pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;generated_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key_name&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"main-instance"&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 instance is launched, AWS creates a primary Elastic Network Interface (ENI) for it.&lt;/p&gt;

&lt;p&gt;The ENI is essentially the virtual network adapter that connects the EC2 instance to the VPC.&lt;/p&gt;

&lt;p&gt;It is associated with things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The subnet&lt;/li&gt;
&lt;li&gt;A private IP address&lt;/li&gt;
&lt;li&gt;Security groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The EC2's network interface provides its connection to the VPC and carries its private network identity.&lt;/p&gt;

&lt;p&gt;Because the instance is in the public subnet and the subnet is configured to assign public IPv4 addresses to launched instances, the instance can also receive a public IPv4 address.&lt;/p&gt;

&lt;p&gt;This allows me to connect to it from outside the VPC using SSH, provided the route and security group rules allow the connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. S3 Gateway VPC Endpoint
&lt;/h2&gt;

&lt;p&gt;The private subnet doesn't have a direct route to the internet.&lt;/p&gt;

&lt;p&gt;But I still wanted resources in the private subnet to be able to access Amazon S3 without requiring a NAT Gateway.&lt;/p&gt;

&lt;p&gt;For this, I created an S3 Gateway VPC Endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;service_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.amazonaws.eu-west-2.s3"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_endpoint_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Gateway"&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&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="s2"&gt;"s3-gateway-endpoint"&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 endpoint is associated with the private route table.&lt;/p&gt;

&lt;p&gt;This provides a private path from the VPC to S3 without requiring the private subnet to have general internet connectivity.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Private subnet
      │
      ▼
Private route table
      │
      ▼
S3 Gateway Endpoint
      │
      ▼
Amazon S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is different from giving the private subnet internet access through a NAT Gateway.&lt;/p&gt;

&lt;p&gt;The subnet remains private while still being able to reach the AWS service it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. SSH Key Management
&lt;/h2&gt;

&lt;p&gt;For SSH authentication, I used Terraform's TLS provider to generate an RSA private key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"tls_private_key"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RSA"&lt;/span&gt;
  &lt;span class="nx"&gt;rsa_bits&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding public key is registered with AWS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_key_pair"&lt;/span&gt; &lt;span class="s2"&gt;"generated_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"registry-vpc-key"&lt;/span&gt;
  &lt;span class="nx"&gt;public_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tls_private_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_key_openssh&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform then writes the private key to a local file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"local_file"&lt;/span&gt; &lt;span class="s2"&gt;"private_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tls_private_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_key_pem&lt;/span&gt;
  &lt;span class="nx"&gt;filename&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/registry-vpc-key.pem"&lt;/span&gt;
  &lt;span class="nx"&gt;file_permission&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0400"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;0400&lt;/code&gt; permission ensures that the private key file is readable only by the file owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Following an SSH Connection Through the Architecture
&lt;/h2&gt;

&lt;p&gt;Putting everything together, an SSH connection from my computer to the EC2 looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;My Computer
     │
     │ TCP 22
     ▼
  Internet
     │
     ▼
Internet Gateway
     │
     ▼
Public Subnet
     │
     ▼
EC2 Network Interface
     │
     ▼
Security Group
     │
     │ Source IP allowed?
     │ TCP 22 allowed?
     ▼
    EC2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several components have different responsibilities in this flow.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;route table&lt;/strong&gt; provides the path.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Internet Gateway&lt;/strong&gt; provides connectivity between the VPC and the internet.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;network interface&lt;/strong&gt; connects the EC2 to the VPC.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;security group&lt;/strong&gt; determines whether the incoming connection is permitted.&lt;/p&gt;

&lt;p&gt;Understanding these responsibilities makes the architecture much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. What I Built
&lt;/h2&gt;

&lt;p&gt;The final infrastructure can be summarized as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Region: eu-west-2
│
└── VPC: 10.0.0.0/16
    │
    ├── Availability Zone: eu-west-2c
    │   └── Public Subnet: 10.0.1.0/24
    │       ├── Public Route Table
    │       │   └── 0.0.0.0/0 → Internet Gateway
    │       │
    │       └── EC2
    │           └── SSH allowed from my IP
    │
    └── Availability Zone: eu-west-2b
        └── Private Subnet: 10.0.2.0/24
            ├── Private Route Table
            │
            └── S3 Gateway VPC Endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project demonstrates the fundamental building blocks of AWS networking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC → Subnets → Route Tables → Internet Gateway → Security Groups → Network Interfaces → EC2 → VPC Endpoints&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most important lesson for me was understanding that these components are not independent pieces of configuration. They work together to determine &lt;strong&gt;where resources live, where their traffic can go, and which traffic is allowed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This foundation makes it much easier to understand more advanced AWS architectures such as load-balanced applications, private application tiers, NAT Gateways, RDS deployments, ECS, and multi-AZ infrastructure.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>vpc</category>
      <category>devops</category>
    </item>
    <item>
      <title>Deploying a React App to AWS S3 + CloudFront</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Thu, 13 Aug 2026 23:38:59 +0000</pubDate>
      <link>https://dev.to/duubemmm/deploying-a-react-app-to-aws-s3-cloudfront-55hn</link>
      <guid>https://dev.to/duubemmm/deploying-a-react-app-to-aws-s3-cloudfront-55hn</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CountryRank&lt;/strong&gt; is a React (Vite) app for exploring and comparing countries. I deployed it to AWS using a production-grade static hosting setup: a private S3 bucket serving the build output, sitting behind CloudFront for HTTPS, caching, and correct handling of client-side routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React + Vite&lt;/strong&gt;: the app itself, built to static assets with &lt;code&gt;npm run build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS S3&lt;/strong&gt;: private bucket storing the build output, no public access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CloudFront&lt;/strong&gt;: CDN in front of S3, HTTPS, custom error handling for SPA routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS IAM (bucket policy)&lt;/strong&gt;: scoped access control via a service principal and condition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin Access Control (OAC)&lt;/strong&gt;: the mechanism that lets CloudFront read from the private bucket&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&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%2F3erwxtzmqgi829i8kf36.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3erwxtzmqgi829i8kf36.png" alt="AWS architecture diagram showing a React app served through CloudFront, with HTTPS, caching, WAF, and custom error handling. CloudFront accesses a private S3 bucket through Origin Access Control (OAC), with S3 Block Public Access enabled." width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built this project in two deliberate stages. I set up plain S3 static hosting first to understand the baseline, then added CloudFront and OAC after seeing the limitations of the S3-only setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: S3 bucket setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the bucket.&lt;/strong&gt; The name has to be globally unique, lowercase and hyphens only. Dots break SSL matching later if CloudFront gets added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn off Block Public Access&lt;/strong&gt; for this initial public-hosting stage. AWS made me type a confirmation phrase here rather than just unchecking a box, a useful signal that this is a consequential decision and not a routine toggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable static website hosting&lt;/strong&gt; on the bucket:

&lt;ul&gt;
&lt;li&gt;Index document: &lt;code&gt;index.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Error document: &lt;code&gt;index.html&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attach a bucket policy&lt;/strong&gt; granting public read access:
&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;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&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:s3:::dubem-country-rank-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;p&gt;Two details worth internalizing here. &lt;code&gt;Resource&lt;/code&gt; needs &lt;code&gt;/*&lt;/code&gt; because &lt;code&gt;s3:GetObject&lt;/code&gt; acts on individual objects, not the bucket itself; the bucket ARN without &lt;code&gt;/*&lt;/code&gt; refers to the container (its configuration, its listing) rather than what's inside it. And S3 validates the bucket name in the policy against the actual bucket it's attached to, so a typo gets rejected immediately on save.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload the build.&lt;/strong&gt; This is where S3's flat structure matters:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Wrong: nests everything under a dist/ prefix, so index.html isn't found at root&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;sync &lt;/span&gt;dist s3://dubem-country-rank-app/

&lt;span class="c"&gt;# Right: the trailing slash means "copy the contents of dist", not "dist as a unit"&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;sync &lt;/span&gt;dist/ s3://dubem-country-rank-app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S3 has no real folders. Everything is a flat list of objects with string keys, and the console just visually groups keys that share a prefix. &lt;code&gt;sync&lt;/code&gt; (as opposed to &lt;code&gt;cp --recursive&lt;/code&gt;) only uploads what's changed, which matters once I'm redeploying repeatedly.&lt;/p&gt;

&lt;p&gt;At this point the site was live on the S3 website endpoint. No server was running anywhere, just object storage answering HTTP requests. But it had real gaps: no HTTPS, no CDN, no custom domain and broken client-side routing. A path like &lt;code&gt;/countries/nigeria&lt;/code&gt; isn't a real object in the bucket, so direct navigation or a refresh returns a 404 unless the error document papers over it. Even then, the response still carries a 404 or 403 status code, which is bad for SEO and monitoring. That's what CloudFront fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: CloudFront setup
&lt;/h2&gt;

&lt;p&gt;Putting CloudFront in front isn't just adding a CDN. It's a structural shift in the trust model. Three things change together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The bucket goes back to &lt;strong&gt;fully private&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;CloudFront gets a dedicated identity via &lt;strong&gt;OAC&lt;/strong&gt; to read from it.&lt;/li&gt;
&lt;li&gt;The bucket policy's principal changes from &lt;code&gt;"*"&lt;/code&gt; to a scoped AWS service principal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Creating the distribution through the guided console flow looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distribution type: &lt;strong&gt;Single website or app&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Origin: selected via &lt;strong&gt;Browse S3&lt;/strong&gt;, which resolves to the bucket's &lt;strong&gt;REST/object endpoint&lt;/strong&gt; (&lt;code&gt;bucket-name.s3.region.amazonaws.com&lt;/code&gt;) rather than the website endpoint&lt;/li&gt;
&lt;li&gt;No custom Route 53 domain for now; CloudFront gives a free &lt;code&gt;*.cloudfront.net&lt;/code&gt; domain by default&lt;/li&gt;
&lt;li&gt;WAF's managed protections left on default (free tier, no cost) rather than customized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why the REST endpoint and not the website endpoint?&lt;/strong&gt; OAC works by having CloudFront sign its requests to S3, and only the REST API understands signed requests. The website-hosting endpoint is public-only by design and can't validate a signature. Since the bucket is going private anyway, the website endpoint becomes irrelevant at this stage regardless.&lt;/p&gt;

&lt;p&gt;One consequence worth flagging: the REST endpoint has &lt;strong&gt;no concept of index or error documents&lt;/strong&gt;. Everything configured in Step 1's hosting settings doesn't carry over. CloudFront needs its own equivalent settings, covered next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Setting up OAC
&lt;/h2&gt;

&lt;p&gt;Enabling &lt;strong&gt;"Allow private S3 bucket access to CloudFront"&lt;/strong&gt; during distribution creation is the actual OAC toggle. It does two things: it creates an Origin Access Control resource, and it auto-generates a bucket policy update scoped to that resource.&lt;/p&gt;

&lt;p&gt;The generated policy looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowCloudFrontServicePrincipal"&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;"Principal"&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;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudfront.amazonaws.com"&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;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&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:s3:::dubem-country-rank-app/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&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;"ArnLike"&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;"AWS:SourceArn"&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:cloudfront::942004241182:distribution/E2XUCTY83EAT31"&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;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;p&gt;The &lt;code&gt;Condition&lt;/code&gt; block matters more than it looks. &lt;code&gt;Service: cloudfront.amazonaws.com&lt;/code&gt; alone would let any CloudFront distribution in any AWS account read the bucket. Scoping it to a specific &lt;code&gt;SourceArn&lt;/code&gt; restricts it to this one distribution. Without that, I would just be trading one public-access problem for a slightly narrower one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; enabling OAC updates the bucket policy, but it doesn't touch Block Public Access or remove prior policy statements automatically. That's what bit me next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors I hit and how I fixed them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AccessDenied on the CloudFront root URL
&lt;/h3&gt;

&lt;p&gt;After the distribution deployed, hitting the CloudFront URL returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Code&amp;gt;&lt;/span&gt;AccessDenied&lt;span class="nt"&gt;&amp;lt;/Code&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Message&amp;gt;&lt;/span&gt;Access Denied&lt;span class="nt"&gt;&amp;lt;/Message&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Error&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reasoned through it like this: when requesting &lt;code&gt;/&lt;/code&gt;, what object key was CloudFront actually asking S3 for? Nothing in the setup so far told CloudFront that &lt;code&gt;/&lt;/code&gt; should resolve to &lt;code&gt;index.html&lt;/code&gt;. That's a distribution-level setting called &lt;strong&gt;Default Root Object&lt;/strong&gt;, and it isn't inherited from anything configured back in Step 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; in the distribution, go to General, click Edit and set Default Root Object to &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Client-side routes still broken after that
&lt;/h3&gt;

&lt;p&gt;Fixing the root didn't fix direct navigation to &lt;code&gt;/countries/nigeria&lt;/code&gt;. Same underlying problem as the S3-only stage, still unresolved, because Default Root Object only handles the exact-root case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Custom Error Responses on the distribution:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;HTTP error code&lt;/th&gt;
&lt;th&gt;Response page path&lt;/th&gt;
&lt;th&gt;HTTP response code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/index.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/index.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both codes are needed because a private bucket accessed via OAC tends to return 403 for a missing key rather than a clean 404. It doesn't distinguish "doesn't exist" from "not allowed to see it," it just denies. This is the fix plain S3 hosting couldn't offer: CloudFront can rewrite the status code itself, not just swap in different content, so a deep-linked SPA route now returns a legitimate 200 while React Router takes over client-side.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Old public bucket policy left stacked on top of the new one
&lt;/h3&gt;

&lt;p&gt;After everything was working, I went back and checked the bucket policy directly instead of assuming the wizard had cleaned up after itself. I found the original &lt;code&gt;Principal: "*"&lt;/code&gt; statement from Step 1 still sitting there, alongside the new CloudFront-scoped one. Block Public Access was also still off.&lt;/p&gt;

&lt;p&gt;That meant the bucket was reachable two ways: through CloudFront (secured, cached, HTTPS) and directly via the old public endpoint (none of that), quietly defeating the point of the whole migration.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Removed the old &lt;code&gt;Principal: "*"&lt;/code&gt; statement, keeping only the CloudFront-scoped one.&lt;/li&gt;
&lt;li&gt;Re-enabled Block Public Access.&lt;/li&gt;
&lt;li&gt;Verified by testing both URLs: the CloudFront URL still worked, and the direct S3 endpoint failed. That's the actual proof the bucket is only reachable through the intended path.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;S3 access control is layered: Block Public Access is the master override, the bucket policy is the grant, and the resource ARN scope decides bucket versus object. All three have to align, and missing any one breaks things in a specific, diagnosable way.&lt;/li&gt;
&lt;li&gt;S3 "folders" are prefixes, not real directories. This affects upload commands directly, not just how the console displays things.&lt;/li&gt;
&lt;li&gt;Website-hosting endpoints and REST endpoints are genuinely different features with different capabilities. OAC only works with the REST one.&lt;/li&gt;
&lt;li&gt;CloudFront doesn't inherit S3's static-hosting settings. Default Root Object and Custom Error Responses have to be configured independently, and skipping them produces specific, traceable errors rather than vague failures.&lt;/li&gt;
&lt;li&gt;Auto-generated policies from console wizards can leave old, conflicting statements behind. It's worth checking the actual policy JSON manually rather than assuming the tool fully cleaned up after itself.&lt;/li&gt;
&lt;li&gt;Debugging AccessDenied is faster when reasoning through which layer is missing (policy, public access block, or routing config) rather than guessing and re-toggling settings at random.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloudfront</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Set Up SSH Keys on Windows: A Step-by-Step Guide</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Tue, 02 Sep 2025 08:08:57 +0000</pubDate>
      <link>https://dev.to/duubemmm/set-up-ssh-keys-on-windows-a-step-by-step-guide-43d5</link>
      <guid>https://dev.to/duubemmm/set-up-ssh-keys-on-windows-a-step-by-step-guide-43d5</guid>
      <description>&lt;p&gt;Secure Shell (SSH) is a security protocol that creates a highly secure connection between your computer and GitHub. It uses a pair of cryptographic keys—a public key you share and a private key you keep safe—to verify your identity.&lt;/p&gt;

&lt;p&gt;The main benefit of using SSH keys is convenience and security. Once set up, you can connect to GitHub to push and pull code without having to enter your username and personal access token every time.&lt;/p&gt;

&lt;p&gt;Here's a step-by-step guide on how to set up your SSH keys in a Windows environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Check for Existing SSH Keys&lt;/strong&gt;&lt;br&gt;
First, let's see if you already have an SSH key pair. It's good practice to avoid creating new ones if you don't have to.&lt;/p&gt;

&lt;p&gt;a) Open &lt;strong&gt;PowerShell&lt;/strong&gt; (press the Windows key, type PowerShell, and press Enter).&lt;/p&gt;

&lt;p&gt;b) The default directory is /.ssh. Run the following command to list files in the .ssh directory, which is where keys are typically stored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls -a ~/.ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;c) Look for a pair of files named something like id_rsa and id_rsa.pub, or id_ed25519 and id_ed25519.pub. If you see a .pub file, you already have a key and can skip to &lt;strong&gt;Step 3&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Alternatively, you can get the key by typing into the powershell terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Generate a New SSH Key Pair&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don't have an existing key, you'll need to create one. We'll generate a modern and secure Ed25519 key.&lt;/p&gt;

&lt;p&gt;a) In your PowerShell terminal, run the following command. Make sure to replace "&lt;a href="mailto:your_email@example.com"&gt;your_email@example.com&lt;/a&gt;" with the email address associated with your GitHub account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-keygen -t ed25519 -C "your_email@example.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) The command will prompt you to choose a location to save the key. The default location (~/.ssh/id_ed25519) is perfect, so just press Enter.&lt;/p&gt;

&lt;p&gt;c) Next, you'll be asked to enter a passphrase. 🔐 &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A passphrase adds an extra layer of security; if someone gains access to your computer, they still can't use your key without knowing the passphrase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It's highly recommended to use one, but you can leave it blank by pressing Enter twice.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Enter passphrase (empty for no passphrase): [Type a passphrase]
&amp;gt; Enter same passphrase again: [Type passphrase again]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've now created two files in your ~/.ssh directory: id_ed25519 (your private key) and id_ed25519.pub (your public key). Never share your private key!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Add Your SSH Key to the ssh-agent&lt;/strong&gt;&lt;br&gt;
The ssh-agent is a background program that securely stores your private key and manages its passphrase so you don't have to re-enter it constantly.&lt;/p&gt;

&lt;p&gt;a) First, ensure the ssh-agent service is running. This command will start it if it isn't active.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start-SshAgent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) Now, add your private SSH key to the agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-add ~/.ssh/id_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;c) If you created a passphrase in the previous step, you will be prompted to enter it now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Add Your Public SSH Key to GitHub&lt;/strong&gt;&lt;br&gt;
Now you need to tell GitHub about your public key so it can grant you access.&lt;br&gt;
a) Now you need to copy your public SSH key. To do this, we’re going to use a command called cat to read the file to the console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat ~/.ssh/id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) Open your browser and navigate to GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Click your profile picture in the top-right corner, then click Settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the left sidebar, click SSH and GPG keys.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;c) Click the New SSH key button.&lt;/p&gt;

&lt;p&gt;d) In the Title field, give your key a descriptive name, like "My Windows Laptop" or "Work PC".&lt;/p&gt;

&lt;p&gt;e) Click inside the Key field and paste your key by pressing Ctrl + V.&lt;/p&gt;

&lt;p&gt;f) Finally, click Add SSH key. You may be asked to enter your GitHub password to confirm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Test Your SSH Connection&lt;/strong&gt; &lt;br&gt;
Let's make sure everything is working correctly.&lt;/p&gt;

&lt;p&gt;a) Back in your PowerShell terminal, run this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -T git@github.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) You may see a warning about the authenticity of the host. This is normal for the first connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The authenticity of host 'github.com (IP ADDRESS)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type yes and press Enter.&lt;/p&gt;

&lt;p&gt;c) If everything is set up correctly, you'll see a welcome message with your username! 🎉&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hi your-username! You've successfully authenticated, but GitHub does not provide shell access.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're all set! You can now clone, pull, and push to your GitHub repositories using SSH without needing to enter your credentials every time.&lt;/p&gt;

</description>
      <category>github</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Fixed Firebase’s “Unauthorized Domain” Error While Building Resumpire</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Wed, 09 Jul 2025 14:52:58 +0000</pubDate>
      <link>https://dev.to/duubemmm/how-i-fixed-firebases-unauthorized-domain-error-while-building-resumpire-2567</link>
      <guid>https://dev.to/duubemmm/how-i-fixed-firebases-unauthorized-domain-error-while-building-resumpire-2567</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64ke1z161s0dp8hqt7lv.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.amazonaws.com%2Fuploads%2Farticles%2F64ke1z161s0dp8hqt7lv.jpg" alt="Firebase Error" width="800" height="385"&gt;&lt;/a&gt;&lt;br&gt;
I was deep into building &lt;a href="//resumpire.vercel.app"&gt;resumpire.vercel.app&lt;/a&gt; (a resume builder app) when I hit a wall. Firebase kept rejecting my authentication requests with an “Unauthorized Domain” error.&lt;/p&gt;

&lt;p&gt;At first, I was running through all the usual suspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Maybe I broke something in my codebase?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Could there be a dependency version conflict?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is there a typo in my config?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After scouring my codebase trying to find the bug, I finally spotted the issue.&lt;br&gt;
Turns out, the issue was much simpler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Culprit: Firebase’s Authorized Domain List&lt;/strong&gt;&lt;br&gt;
Firebase Authentication only allows requests from domains you explicitly authorize. That means:&lt;/p&gt;

&lt;p&gt;If you’re testing locally, you must add &lt;a href="http://localhost" rel="noopener noreferrer"&gt;http://localhost&lt;/a&gt; (or your custom dev URL) to Firebase’s Authorized Domains list.&lt;/p&gt;

&lt;p&gt;If you’re deploying, your live domain (e.g., resumpire.vercel.app) needs to be added too.&lt;/p&gt;

&lt;p&gt;My deployed domain resumpire.vercel.app wasn't in the Firebase Authorized Domains list. I'd only added localhost during development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I Fixed It&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Opened Firebase Console → Authentication → Settings&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scrolled to “Authorized Domains”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Added my production domain (resumpire.vercel.app)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reloaded the app → Auth worked instantly&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson learned: Always check the docs (and the obvious settings) first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Matters&lt;/strong&gt;&lt;br&gt;
This wasn’t just about fixing an error—it reinforced a bigger lesson:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Small oversights cause big blockers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reading docs &amp;gt; guessing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every error is an avenue to learn and grow which makes you a better dev.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How I Built an AI-Powered Text Processor Using Chrome AI API.</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Thu, 27 Mar 2025 08:40:36 +0000</pubDate>
      <link>https://dev.to/duubemmm/how-i-built-an-ai-powered-text-processor-using-chrome-ai-api-1j0f</link>
      <guid>https://dev.to/duubemmm/how-i-built-an-ai-powered-text-processor-using-chrome-ai-api-1j0f</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;When I first discovered the Chrome AI API, I recognized its potential immediately. The ability to integrate AI-powered text processing directly into a web app—without requiring a backend—opens up new possibilities for seamless user experiences. Using React and TailwindCSS, I built a practical AI Text Processor capable of summarizing, translating, and detecting languages entirely in the browser.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll walk you through:&lt;br&gt;
✔ Why the Chrome AI API is a game-changer&lt;br&gt;
✔ Step-by-step setup (including troubleshooting tips)&lt;br&gt;
✔ How I built the UI with React &amp;amp; TailwindCSS&lt;br&gt;
✔ Lessons learned and where to go next&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup &amp;amp; Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before diving in, you’ll need:&lt;/p&gt;

&lt;p&gt;1) Comply with Google’s AI Policies&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure your use case follows Google’s Generative AI Prohibited Use Policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2) Install Chrome Canary&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download Chrome Canary (v129.0.6639.0 or newer).
Why Canary? The API is experimental and only available here for now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3) Check Storage Requirements&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum 22GB free storage. If storage drops below 10GB, Chrome automatically deletes the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4) Enable the Summarization API&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open chrome://flags/#summarization-api-for-gemini-nano&lt;/li&gt;
&lt;li&gt;Select Enabled&lt;/li&gt;
&lt;li&gt;Relaunch Chrome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;5) Force-Download the AI Model&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open DevTools (F12) and run:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;await ai.summarizer.create(); // Triggers model download&lt;br&gt;
await ai.summarizer.capabilities(); // Check status&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Repeat the second command until it returns "readily" (~3-5 mins).&lt;/p&gt;

&lt;p&gt;Stuck? If you see:&lt;/p&gt;

&lt;p&gt;"The model was available but there was no execution config available for the feature."&lt;/p&gt;

&lt;p&gt;Wait 24 hours and retry (Google rolls out access gradually).&lt;/p&gt;

&lt;p&gt;Troubleshooting Tip: For full documentation, check Chrome’s AI API Guide &lt;a href="https://docs.google.com/document/d/1bzpeKk4k26KfjtR-_d9OuXLMpJdRMiLZAOVNMuFIejk/edit?tab=t.0" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Chrome AI API?
&lt;/h2&gt;

&lt;p&gt;This API brings serverless AI to the browser with:&lt;br&gt;
✅ No backend needed – Runs entirely client-side.&lt;br&gt;
✅ Blazing fast – Responses in milliseconds.&lt;br&gt;
✅ Privacy-focused – No data sent to third-party servers.&lt;br&gt;
✅ Easy integration – Just call ai. methods like translate() or summarize().&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Features
&lt;/h2&gt;

&lt;p&gt;The AI Text Processor includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Text Input - A responsive text area for content entry&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Language Detection - Automatic identification of input text language&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Text Summarization - Concise distillation of key information&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multilingual Translation - Conversion between supported languages&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Accessible Interface - Screen reader compatible with keyboard navigation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Error Handling - Graceful failure modes and user feedback&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;React – UI components &amp;amp; state management.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TailwindCSS – Rapid styling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chrome AI API – ai.summarizer, ai.translator, ai.detector.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building the App
&lt;/h2&gt;

&lt;p&gt;1) Initialize the Project&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm create vite@latest ai-text-processor --template react&lt;br&gt;
cd ai-text-processor&lt;br&gt;
npm install&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;2) Add TailwindCSS&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install -D tailwindcss postcss autoprefixer&lt;br&gt;
npx tailwindcss init -p&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Configure tailwind.config.js:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;export default {&lt;br&gt;
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],&lt;br&gt;
  theme: { extend: {} },&lt;br&gt;
  plugins: [],&lt;br&gt;
};&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Add to index.css:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@tailwind base;&lt;br&gt;
@tailwind components;&lt;br&gt;
@tailwind utilities;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;3) Create the UI&lt;br&gt;
A simple layout with:&lt;/p&gt;

&lt;p&gt;A textarea for input.&lt;/p&gt;

&lt;p&gt;Buttons for actions (Summarize/Translate/Detect).&lt;/p&gt;

&lt;p&gt;A results panel (conditional rendering with error handling).&lt;/p&gt;

&lt;p&gt;4) Integrate the Chrome AI API&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const handleSummarize = async () =&amp;gt; {&lt;br&gt;
  try {&lt;br&gt;
    const summary = await ai.summarizer.summarize(text);&lt;br&gt;
    setResult(summary);&lt;br&gt;
  } catch (error) {&lt;br&gt;
    setResult("Error: " + error.message);&lt;br&gt;
  }&lt;br&gt;
};&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;More languages – Expand translation support.&lt;/p&gt;

&lt;p&gt;Offline mode – Cache results for poor connectivity.&lt;/p&gt;

&lt;p&gt;Custom prompts – Let users tweak summarization style.&lt;/p&gt;

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

&lt;p&gt;The Chrome AI API opens up exciting possibilities for browser-based AI apps. While still experimental, it’s a glimpse into a future where frontend devs can leverage AI without backend complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the demo &amp;amp; code
&lt;/h2&gt;

&lt;p&gt;GitHub Repo Link &lt;a href="https://github.com/Duubemmm/hng12-stage3-AI-Text-Processing-App" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;br&gt;
Live Link &lt;a href="https://hng12-stage3-ai-text-processing-app.vercel.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Note: This implementation is currently available only for desktop platforms&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>api</category>
      <category>ai</category>
    </item>
    <item>
      <title>Recreating the Interswitch Homepage with React and TailwindCSS.</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Tue, 14 Jan 2025 13:24:30 +0000</pubDate>
      <link>https://dev.to/duubemmm/recreating-the-interswitch-homepage-with-react-and-tailwindcss-2ppf</link>
      <guid>https://dev.to/duubemmm/recreating-the-interswitch-homepage-with-react-and-tailwindcss-2ppf</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recreating a modern, visually appealing landing page is always an exciting challenge. This week, I focused on building a replica of the Interswitch homepage using React and TailwindCSS. This article provides a technical walkthrough of the process, from project setup to implementing reusable components and styling. Here's how I approached it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Setup with Vite&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vite has become my go-to tool for React projects due to its blazing-fast build times and simplicity. The setup process involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm create vite@latest interswitch-clone --template react
cd interswitch-clone
npm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the development server running, I was ready to start coding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structuring Components&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Breaking the homepage into reusable components was essential for maintainability and scalability. Below are a few key components I implemented. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NavBar Component&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;import { useState } from "react";
import { FaBars, FaTimes } from "react-icons/fa";
import { FiChevronDown } from "react-icons/fi";

const Navbar = () =&amp;gt; {
  const [isOpen, setIsOpen] = useState(false);
  const [dropdownOpen, setDropdownOpen] = useState(false);

  const navLinks = [
    { title: "About Us", hasDropdown: true },
    { title: "What We Do", hasDropdown: true },
    { title: "Financial Inclusion", hasDropdown: false },
    { title: "Corporate Responsibility", hasDropdown: false },
    { title: "News &amp;amp; Insights", hasDropdown: false },
  ];

export default Navbar;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stats Component&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;const Stats = () =&amp;gt; {
  return (
    &amp;lt;div className="bg-blue-50 py-12"&amp;gt;
      &amp;lt;div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"&amp;gt;
        &amp;lt;div className="flex flex-col gap-8"&amp;gt;
          &amp;lt;div className="flex flex-col md:flex-row items-start gap-8"&amp;gt;
            &amp;lt;h2 className="text-3xl md:text-4xl font-semibold text-gray-900 flex-1"&amp;gt;
              Pushing the boundaries of innovation to deliver payment solutions that enable commerce across Africa
            &amp;lt;/h2&amp;gt;
            &amp;lt;div className="flex-1 flex flex-col gap-4"&amp;gt;
              &amp;lt;p className="text-xl text-gray-700"&amp;gt;
                Bespoke payment solutions for your modern lifestyle, business collections, disbursements, and payment processing.
              &amp;lt;/p&amp;gt;
              &amp;lt;button className="bg-blue-950 text-white px-6 py-3 rounded-md hover:bg-blue-900 transition w-fit"&amp;gt;
                Learn More
              &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
export default Stats;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Styling with TailwindCSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TailwindCSS made styling the components seamless. By leveraging utility classes, I could focus on functionality without writing custom CSS. For example, the hero section below uses Tailwind’s gradient and typography utilities to create an eye-catching design.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const Hero = () =&amp;gt; {
  return (
    &amp;lt;div className="text-blue-950 pt-6 relative"&amp;gt;
      &amp;lt;div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"&amp;gt;
        &amp;lt;div className="grid md:grid-cols-2 gap-12 items-center"&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;h1 className="text-2xl md:text-7xl mb-6 mt-16 font- text-blue-950"&amp;gt;
              The Gateway To Africa&amp;amp;apos;s Payment Ecosystem
            &amp;lt;/h1&amp;gt;
            &amp;lt;p className="text-xl md:text-xl mb-8 text-black-200"&amp;gt;
              We create and sustain a payment ecosystem that helps 
  commmerce evolve, businesses grow and individuals thrive.
            &amp;lt;/p&amp;gt;
          &amp;lt;/div&amp;gt;
       &amp;lt;/div&amp;gt;
     &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;


export default Hero;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Componentization: Breaking the UI into reusable components ensured better maintainability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TailwindCSS: The utility-first approach made styling intuitive and efficient.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vite: Its fast build times improved the development experience.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recreating the Interswitch homepage was a rewarding experience that solidified my understanding of React and TailwindCSS. By leveraging modern tools and best practices, I built a scalable and visually appealing landing page. If you’re working on a similar project or have questions, let’s connect in the comments!&lt;/p&gt;

</description>
      <category>react</category>
      <category>frontend</category>
      <category>tailwindcss</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Elevate Your JavaScript Journey: Tackling Projects and Unlocking Features 🚀</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Thu, 05 Dec 2024 17:04:36 +0000</pubDate>
      <link>https://dev.to/duubemmm/elevate-your-javascript-journey-tackling-projects-and-unlocking-features-3mk9</link>
      <guid>https://dev.to/duubemmm/elevate-your-javascript-journey-tackling-projects-and-unlocking-features-3mk9</guid>
      <description>&lt;p&gt;JavaScript is an ever-evolving journey where each project adds a new layer to your skills. Whether you're building a ticketing system or adding functionality to a to-do list, there's always something new to learn! Let me walk you through my recent experiments, lessons, and insights.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building a Ticketing System 🎟️&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When working on a ticket system, I wanted to level it up by allowing users to input any number of tickets instead of relying on predefined buttons. The goal was simple: improve flexibility while keeping the system intuitive.&lt;/p&gt;

&lt;p&gt;Here’s what I learned:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DOM Manipulation Is Key&lt;/strong&gt;: Capturing input values dynamically and using them effectively in your JavaScript logic is vital.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validations Matter&lt;/strong&gt;: When you let users enter values, ensure you're checking for edge cases like negative numbers or empty inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iterate &amp;amp; Improve&lt;/strong&gt;: Building on the foundational ticket counter I developed earlier, I realized how important it is to refactor code for scalability.&lt;br&gt;
If you're considering a project like this, start small. Add basic functionality, then challenge yourself to build on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding Delete Functionality to a To-Do List ✔️❌&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To-do lists are a staple project for beginners—and for good reason! They offer a practical way to learn about adding and removing elements from the DOM. Recently, I wanted to add a delete button to each item dynamically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Approach&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generate Buttons on Item Creation&lt;/strong&gt;: When the user adds a new task, a delete button is created alongside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Event Listeners Dynamically&lt;/strong&gt;: I attached an event listener to each button to handle the deletion logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parent-Child DOM Interaction&lt;/strong&gt;: Using parentElement, I ensured clicking the delete button would remove its corresponding list item.&lt;br&gt;
Here’s a snippet of the core idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function addTask(taskText) {

  const li = document.createElement("li");
  li.textContent = taskText;

  const deleteBtn = document.createElement("button");
  deleteBtn.textContent = "Delete";


  deleteBtn.addEventListener("click", () =&amp;gt; {
    li.remove();
  });

  li.appendChild(deleteBtn);
  document.querySelector("#task-list").appendChild(li);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this approach, I learned how important it is to structure your DOM interactions cleanly and avoid cluttered logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons Learned 🧠&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.Experimentation Drives Learning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Projects like these reinforce that theory is nothing without practice. By experimenting with features, I gained a deeper understanding of DOM manipulation, event listeners, and JavaScript logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.Simple Features, Big Impact&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The smallest changes—like a delete button—can significantly enhance your project's user experience. Don't underestimate the value of small wins!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.Learn in Layers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building on previous projects is the best way to reinforce your skills. I started with a basic ticket counter, then expanded it. Similarly, I took my to-do list from "add-only" to "add-and-delete."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's Next?🔮&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JavaScript is all about iteration. I’m excited to explore more advanced concepts like input validations, animations, and even local storage to persist data between sessions.&lt;/p&gt;

&lt;p&gt;If you're on a similar journey, start small and stay consistent. Every project you complete is another step forward.&lt;/p&gt;

&lt;p&gt;Let me know in the comments—what’s your favorite beginner-friendly JavaScript project?&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Installing And Running NPM Packages Using The Terminal</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Mon, 04 Nov 2024 08:57:52 +0000</pubDate>
      <link>https://dev.to/duubemmm/installing-and-running-npm-packages-using-the-terminal-1i09</link>
      <guid>https://dev.to/duubemmm/installing-and-running-npm-packages-using-the-terminal-1i09</guid>
      <description>&lt;p&gt;When working on a JavaScript project, chances are you'll need to install and use npm (Node Package Manager) packages. &lt;/p&gt;

&lt;p&gt;npm is essential for managing libraries and tools that enhance your development process. If you're new to this, here's a step-by-step guide to installing and running npm packages using the terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;br&gt;
Install Node.js: Ensure that the recent version of Node.js is installed on your machine, as npm comes bundled with it. You can check if it's installed by running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node -v&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm -v&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj639rbpje0kay263cxoa.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%2Fj639rbpje0kay263cxoa.png" alt="Installing npm" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you don't have it installed, download it from &lt;a href="https://nodejs.org/https://nodejs.org/" rel="noopener noreferrer"&gt;https://nodejs.org/https://nodejs.org/&lt;/a&gt; and follow the installation instructions.&lt;/p&gt;

&lt;p&gt;After confirming it is successfully installed, create a folder for your project or navigate to an existing one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialize The Project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To start using npm, you need a package.json file, which tracks the project's dependencies and scripts. You can create one by running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm init&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjniccapjrd2bdnfe9wn.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%2Fdjniccapjrd2bdnfe9wn.png" alt="Initializing npm" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll be prompted with some basic questions (like project name, version, description, etc.). You can either answer each or press Enter to accept the defaults.&lt;br&gt;
Alternatively, you can use &lt;code&gt;npm init -y&lt;/code&gt; to create a default package.json file without prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installing npm Packages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With npm, you can install packages either globally or locally to your project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local Installation: Installs the package in the project directory and adds it to your package.json.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;npm install package-name&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
This installs the package locally. You'll find it under the node_modules folder, and it will be listed in your package.json file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global Installation: Installs the package globally on your system, making it available from any directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;npm install -g package-name&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This installs the package globally, allowing you to run the package commands directly in any project without needing to install it locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using npm Scripts to Run Commands&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scripts defined in package.json can simplify common tasks like running tests or starting the server. In your package.json, there’s a "scripts" section where you can define custom commands.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
"scripts": {&lt;br&gt;
  "start": "node index.js",&lt;br&gt;
  "test": "echo \"Error: no test specified\" &amp;amp;&amp;amp; exit 1"&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
In this example:&lt;/p&gt;

&lt;p&gt;"start" will run "node index.js".&lt;br&gt;
You can replace "test" or "start" with other custom scripts.&lt;/p&gt;

&lt;p&gt;To run a script, use:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm run script-name&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running Installed Packages from the Terminal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you installed a package globally, you can use it directly in the terminal.&lt;br&gt;
For locally installed packages, use npx, which comes bundled with npm:&lt;br&gt;
&lt;code&gt;npx package-name&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx create-react-app my-app&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
This command runs the create-react-app package without needing to install it globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updating and Removing Packages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To update a package, use:&lt;br&gt;
&lt;code&gt;npm update package-name&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
To remove a package, use:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm uninstall package-name&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
This command will remove the package from the node_modules folder and from package.json.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
By understanding these basic npm commands, you can confidently install, run, and manage packages in any Node.js project.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
