<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Pascal Attama on Medium]]></title>
        <description><![CDATA[Stories by Pascal Attama on Medium]]></description>
        <link>https://medium.com/@attamapascalpedro?source=rss-61e9b93b82d9------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*p-ktqjOfNJQOpVKYxsbIYA.jpeg</url>
            <title>Stories by Pascal Attama on Medium</title>
            <link>https://medium.com/@attamapascalpedro?source=rss-61e9b93b82d9------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 28 May 2026 22:34:40 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@attamapascalpedro/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Kubernetes Cluster Broke After a Node IP Change? Here’s the Fix (and Lessons Learned)]]></title>
            <link>https://attamapascalpedro.medium.com/kubernetes-cluster-broke-after-a-node-ip-change-heres-the-fix-and-lessons-learned-98b73604863b?source=rss-61e9b93b82d9------2</link>
            <guid isPermaLink="false">https://medium.com/p/98b73604863b</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[baremetal]]></category>
            <category><![CDATA[troubleshooting]]></category>
            <dc:creator><![CDATA[Pascal Attama]]></dc:creator>
            <pubDate>Fri, 05 Sep 2025 08:43:39 GMT</pubDate>
            <atom:updated>2025-09-05T12:57:07.888Z</atom:updated>
            <content:encoded><![CDATA[<h4>A step-by-step troubleshooting guide covering etcd, kubeconfigs, and API server certificates</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/413/1*ve1jbgam28KyNTdvgbnvkg.jpeg" /></figure><h3>Introduction</h3><p>Running Kubernetes on bare-metal or self-managed servers can be both exciting and painful. Recently, I ran into an issue that completely broke my control plane — all <strong>because my Linux server’s IP address changed</strong>. Kubernetes taught me a lesson: <strong>never underestimate an IP change, </strong>and I learned this the hard way after reassigning my server’s IP address.</p><p>The symptoms were confusing at first: <strong>kubectl refused to connect, control plane pods were crash-looping, and even etcd couldn’t bind to its endpoint</strong>. It looked like my cluster was dead. What seemed like a small network reconfiguration turned into an API server certificate mismatch problem that prevented my cluster from working.</p><p>It turned out the issue came from <strong>stale IPs hardcoded into Kubernetes configuration files and API server certificates</strong>.</p><p>In this article, I’ll walk you through:</p><ul><li>What caused the issue</li><li>How I discovered the root cause</li><li>The detailed troubleshooting steps I took</li><li>The fix I applied to bring my cluster back to life</li><li>Lessons learned to prevent this in the future</li></ul><p><strong>If you ever have to change the IP address or hostname of your control-plane node, this guide should save you hours of frustration.</strong></p><h3>What led to the Issue</h3><p>I had a single-node Kubernetes cluster running on my Ubuntu server. Initially, the control-plane node was assigned the IP address:</p><ul><li>My server’s old IP address: 192.168.166.33</li></ul><p>Later, I reconfigured my network and changed the IP to:</p><ul><li>My server’s new IP address: 192.168.0.8</li></ul><p>Kubernetes doesn’t automatically reconfigure itself when a host IP changes. Instead, it stores the control plane’s <strong>IP address</strong> in multiple places:</p><ul><li><strong>etcd manifest</strong> (/etc/kubernetes/manifests/etcd.yaml)</li><li><strong>kubeconfig files</strong> (/etc/kubernetes/admin.conf, kubelet.conf, etc.)</li><li><strong>API server certificates</strong>, which include the old IP in their SANs</li></ul><p>Once the IP changed, the cluster couldn’t start because all references still pointed to the old address.</p><h4><strong>Why does changing node IPs break Kubernetes (on bare metal)?</strong></h4><p><strong>On Cloud-managed K8s:</strong></p><ul><li>Kubernetes clusters are usually provisioned through managed services like EKS, GKE, and AKS.</li><li>These services abstract away the underlying IPs — they use load balancers, DNS names, and service endpoints instead of binding critical cluster components directly to a machine’s raw IP address.</li><li>So if a node IP changes, the control plane or kubelet can usually still find each other via DNS or cloud networking.</li></ul><h4>On Bare Metal or VM K8s:</h4><ul><li>The API server, etcd, kubelet configs, and certs are often tied to the node’s static IP.</li><li>If that IP changes, Kubernetes components will still try to talk to the old IP, causing failures. Also, Certificates (like the API server cert) include specific IPs in their SANs.</li><li>If the IP changes and isn’t in the SAN, TLS verification fails.</li></ul><p>That’s why it’s mostly a problem on bare metal / self-hosted clusters — because<strong> you are the cloud provider</strong>, and <strong>Kubernetes isn’t abstracting IP changes for you.</strong></p><h3>First Signs Something Was Wrong</h3><p>After the change, I noticed that <strong>etcd was failing to start</strong> and kubectl could no longer connect to the API server. Checking the kube-system pods revealed that <strong>etcd</strong> and <strong>kube-apiserver</strong> were in a <strong>CrashLoopBackOff</strong> state.</p><p>Logs confirmed the issue:</p><ul><li><strong>etcd</strong> failed to bind to 192.168.166.33</li><li><strong>kube-apiserver</strong> threw certificate validation errors</li></ul><p>This was the first clue — etcd was still trying to bind to the <strong>old IP</strong> (192.168.166.33) even though the host was now using 192.168.0.8.</p><p>When I tried to run:</p><pre>kubectl cluster-info<br>kubectl get nodes</pre><p>I also got TLS errors about failing to connect to the API server.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ExdiPCbCWqkB9zs9AGX0Ig.jpeg" /><figcaption>This confirmed that the <strong>API server certificate</strong> was still tied to the old IP.</figcaption></figure><h3>Troubleshooting Process</h3><h4>Step 1: Inspecting my current Kubernetes cluster</h4><p>I started by inspecting and verifying the API server address of my current Kubernetes cluster, and also listed all the cluster names that I have defined.</p><pre>kubectl config view --minify | grep server:<br>kubectl config get-clusters</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/964/1*T-KTawNxsAUrku5p-Ff7-w.jpeg" /></figure><h4>Step 2: Checking Cluster Component Status</h4><p>I started by inspecting containers managed by kubelet:</p><pre>sudo crictl ps -a</pre><p>I saw that <strong>etcd</strong> and <strong>apiserver</strong> containers were constantly restarting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*X8JlK7AgLUPXRNIQCw-szw.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hoIApa1RfD8IF-Z0LfqUmg.jpeg" /></figure><p>Looking at their logs (journalctl -xeu kubelet), the recurring issue was that they couldn’t bind to the old IP.</p><h4>Step 3: Checking etcd and kubeconfigs Files</h4><p>Etcd runs as a static pod managed by kubelet, so I inspected:</p><pre>sudo cat /etc/kubernetes/manifests/kube-apiserver.yaml<br>sudo grep -R --line-number &quot;192.168.166.33&quot; /etc/kubernetes || true</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Z4pB7E_Yi9CXDgLdZhw6ww.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/939/1*8JREli7N5QHVd0qQQ56YEg.jpeg" /></figure><p>Every major kubeconfig was still pointing to the <strong>old IP</strong>.</p><h4>Step 4: Updating Configurations</h4><p>I updated the kubeconfigs and the etcd manifest to reference my new IP:</p><pre>sudo sed -i &#39;s/192.168.166.33/192.168.0.8/g&#39; /etc/kubernetes/*.conf<br>sudo grep -R &quot;192.168&quot; /etc/kubernetes/*.conf<br><br>sudo sed -i &#39;s/192.168.166.33/192.168.0.8/g&#39; /etc/kubernetes/manifests/etcd.yaml<br>sudo grep -E &#39;listen-|advertise|initial-cluster|initial-advertise&#39; /etc/kubernetes/manifests/etcd.yaml</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/960/1*NyQKwvDTegVm7S81dHOFTQ.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*J7ZnA76uh7Gv0dOLlzMGng.jpeg" /></figure><h4>Step 4: Restart kubelet</h4><p>I reloaded systemd and restarted kubelet:</p><pre>sudo systemctl daemon-reexec<br>sudo systemctl restart kubelet</pre><p>After restartingkubelet, etcd began starting properly and was able to bind successfully, but kubectl still failed due to <strong>TLS certificate mismatch</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y1B15NbbtWDd3-T3-J4wiA.jpeg" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7x_bfuYrnrkecRlfJvymHQ.jpeg" /></figure><p>The API server still wouldn’t come up because its TLS certificates were signed with the <strong>old IP</strong> as a Subject Alternative Name (SAN).</p><h4>Step 5: Regenerating Certificates</h4><p>To fix the certificate issue:</p><ol><li>Backed up existing API server certificates using</li></ol><pre>sudo cp -r /etc/kubernetes/pki /etc/kubernetes/pki.bak</pre><p>2. Remove old API server certs/keys</p><pre>sudo rm /etc/kubernetes/pki/apiserver.*</pre><p>3. Regenerate the <strong>apiserver certificates</strong> with kubeadm</p><pre>sudo kubeadm init phase certs apiserver \<br>  --apiserver-advertise-address=192.168.0.8 \<br>  --apiserver-cert-extra-sans=192.168.0.8,pedroops,PedroOps \<br>  --control-plane-endpoint=192.168.0.8</pre><p>This generated a new certificate that included both my <strong>IP</strong> and <strong>hostname</strong> (PedroOps) in the SAN list. This way, the cert will be valid whether you connect via: “<strong>192.168.0.8, pedroops, or PedroOps&quot;</strong></p><p>4. Restart control-plane pods</p><p>They are static pods, so kubelet will restart them automatically when the cert changes. To speed it up:</p><pre>sudo systemctl restart kubelet</pre><h4>Step 6: Test</h4><p>After regenerating the certs, the kube-apiserver finally started successfully. I verified the cluster status:</p><pre>sudo kubectl cluster-info<br>sudo kubectl get nodes</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zzW7ysKickQy79qIHJjA0Q.jpeg" /></figure><p>The node was back in the <strong>Ready</strong> state, which shows that the control plane is healthy again.</p><h3>Key Takeaways</h3><ol><li><strong>Plan for IP stability</strong>: Kubernetes is very sensitive to IP changes. <strong>Changing node IPs in Kubernetes is not straightforward. </strong>The IPs are baked into etcd, kubeconfigs, and API server certificates. <strong>Always set a static IP or use DNS names.</strong></li><li><strong>Know your critical files</strong>:</li></ol><pre>/etc/kubernetes/manifests/*.yaml<br>/etc/kubernetes/*.conf<br>Certificates under /etc/kubernetes/pki/</pre><p><strong>3. Use kubeadm tooling</strong>: Instead of manually hacking certs, kubeadm init phase is your friend.</p><p><strong>4. Always check etcd manifests and kubeconfigs</strong> when troubleshooting control-plane startup failures.</p><p><strong>5. Certificates must be regenerated</strong> if the advertised IP or hostname changes.</p><h3>Conclusion</h3><p>This issue <strong>served as a timely reminder that Kubernetes is highly sensitive to control-plane IP/hostname changes</strong>. If you ever reconfigure your network or migrate a master node to a new IP, don’t forget to:</p><ul><li>Update manifests and kubeconfigs</li><li>Regenerate the API server certificate with the correct SANs</li></ul><p>Doing so will save you from frustrating TLS errors and etcd crashes.</p><p>A <strong>minor IP change brought down my entire control plane</strong> — but through systematic troubleshooting, I gained valuable insights into Kubernetes internals and learned how to recover.</p><p>If you’re running your own cluster, I strongly recommend <strong>practicing disaster recovery scenarios</strong> like this. It’ll save you when the unexpected happens.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=98b73604863b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How I Migrated My Cloud Engineer Portfolio to AWS with S3, CloudFront, Route 53 & GitHub Actions…]]></title>
            <link>https://attamapascalpedro.medium.com/how-i-migrated-my-cloud-engineer-portfolio-to-aws-with-s3-cloudfront-route-53-github-actions-4e38c5831341?source=rss-61e9b93b82d9------2</link>
            <guid isPermaLink="false">https://medium.com/p/4e38c5831341</guid>
            <category><![CDATA[ci-cd-pipeline]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[github]]></category>
            <category><![CDATA[finops]]></category>
            <category><![CDATA[security]]></category>
            <dc:creator><![CDATA[Pascal Attama]]></dc:creator>
            <pubDate>Wed, 03 Sep 2025 00:31:08 GMT</pubDate>
            <atom:updated>2025-09-03T00:31:08.106Z</atom:updated>
            <content:encoded><![CDATA[<h4>From manual S3 syncs to automated OIDC pipelines—a real-world example of cloud engineering in action.</h4><h3>How I Migrated My Cloud Engineer Portfolio to AWS with S3, CloudFront, Route 53 &amp; GitHub Actions CI/CD.</h3><h4>A practical cloud engineering project showcasing AWS migrations, automation, and DevOps best practices.</h4><figure><img alt="High-level architecture of my AWS-hosted portfolio project (‘pedroops.com’)." src="https://cdn-images-1.medium.com/max/805/1*jp7hcskEXSGptT2e9lPjfg.png" /><figcaption>high-level architecture of my AWS-hosted frontend portfolio project, www.pedroops.com.</figcaption></figure><p>🔗<a href="https://www.pedroops.com">My Cloud Portfolio</a></p><h3>OVERVIEW</h3><p>In cloud engineering, even something as simple as a <a href="https://www.pedroops.com">personal portfolio</a> can become a <strong>real-world project</strong>. Instead of keeping mine on Netlify, I treated it like a production-grade migration: hosting on <strong>AWS S3 + CloudFront</strong>, securing it with <strong>ACM</strong>, moving DNS to <strong>Route 53</strong>, and setting up <strong>GitHub Actions CI/CD with OIDC </strong>for automated deployments. The site is served over HTTPS, supports SPA deep routes, has www → apex redirect at the edge, and is deployed using CI/CD automation.</p><p>This wasn’t just about a website—it was about applying <strong>the same principles I’d use for enterprise cloud infrastructure</strong>. In this article, I’ll walk you through the full migration process—step by step—so you can learn how to host static sites on AWS the right way.</p><p><a href="https://www.pedroops.com">Pascal Attama | Cloud Engineer</a></p><p>The migration leverages:</p><ul><li><strong>Amazon S3</strong> → Static website hosting for the frontend.</li><li><strong>Amazon CloudFront</strong> → Global CDN distribution with HTTPS.</li><li><strong>AWS Certificate Manager (ACM)</strong> → Free SSL/TLS certificates.</li><li><strong>Amazon Route 53</strong> → Domain and DNS management (migrated from Namecheap).</li><li><strong>GitHub Actions with OIDC</strong> → Automated deployments without storing AWS credentials.</li></ul><p>The project was executed in <strong>two phases</strong>:</p><ol><li><strong>Phase 1: The Migration + Manual Deployment with AWS CLI</strong></li><li><strong>Phase 2: Automated CI/CD using GitHub Actions + OIDC </strong>(no long-lived keys)</li></ol><h4>WHAT I GAINED:</h4><ul><li><strong>Speed:</strong> Global CDN edge caching via CloudFront.</li><li><strong>Security: (</strong>S3 bucket stays private; only CloudFront’s OAC can read it and there are no IAM keys in GitHub.)</li><li><strong>Cheap</strong>: Mostly within the free tier for low traffic.</li><li><strong>Reliability:</strong> (AWS infra).</li><li><strong>Automation: </strong>One-push deploys (build → sync → invalidate → done).</li><li><strong>Cohesion:</strong> a single provider (AWS) will provide tighter integration and lower vendor coupling.</li></ul><h4>PREREQUISITES</h4><ul><li>An <strong>AWS account</strong> (free tier).</li><li><strong>AWS CLI</strong> configured locally.</li><li>A local Linux server running with <strong>Node.js, npm/yarn, and git</strong>.</li><li>A React app locally built in the frontend/ directory with npm run build output in frontend/build/.</li><li>A custom domain (<strong>pedroops.com</strong>) registered at Namecheap (or any registrar) and currently pointing to Netlify (or any other DNS hosting provider).</li><li>A GitHub account containing the Git repo of the source code for the portfolio website.</li></ul><h4>HIGH-LEVEL PLAN</h4><ul><li>Build the <strong>React app</strong> on your local Linux server.</li><li>Set up <strong>AWS CLI</strong> on the local server.</li><li>Create an S3 bucket and upload build/content (use aws s3 sync).</li><li>Configure <strong>S3 for private origin</strong> and allow<strong> CloudFront to read (OAC).</strong></li><li>Create an <strong>ACM cert</strong> in us-east-1 and validate with DNS.</li><li>Create a <strong>CloudFront distribution</strong> (S3 REST origin + OAC; default root object index.html; custom error responses for SPA).</li><li>Create a <strong>Route 53 hosted zone</strong>, reproduce all DNS records (MX, SPF, DKIM split, verification TXT), and create an A (Alias) for apex and a CNAME/Alias for www pointing to CloudFront.</li><li>Create a <strong>CloudFront function</strong> to redirect www → pedroops.com (viewer request).</li><li>Add the <strong>GitHub OIDC provider</strong> in IAM.</li><li>Add the <strong>GitHub Actions</strong> workflow.</li><li>Verify the pipeline with <strong>dig and curl.</strong></li></ul><h3>FULL STEP-BY-STEP PROCESS:</h3><h3><strong>(A). Phase 1 (Migration):</strong></h3><h4><strong>1. DNS Query</strong></h4><p><strong>Query the DNS server</strong> to verify where ‘pedroops.com’ is currently being hosted and its associated IP address using the <strong>dig and curl</strong> commands:</p><pre>dig pedroops.com<br>dig www.pedroops.com<br>dig pedroops.com +short<br>dig www.pedroops.com CNAME +short<br>curl -I https://pedroops.com<br>curl -I http://www.pedroops.com<br>curl -I pedroops.com<br>curl -I https://pedroops.com/some/deep/route</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1uAS-VahE_rVm5vmbczjAg.jpeg" /><figcaption>curl request</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1022/1*nnbRLERdHLneA3FQzUzjfQ.jpeg" /><figcaption>dig request</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xRO-IJIzY0pKW0RIwah11w.jpeg" /></figure><p>It indicates that the server is pointing at <strong>Netlify.</strong></p><h4><strong>2. Create the S3 bucket (private, origin for CloudFront)</strong></h4><p><strong><em>Why private?</em></strong><em> Best practice. CloudFront will read from S3 using </em><strong><em>Origin Access Control (OAC)</em></strong><em>, not public S3 URLs.</em></p><p><strong>Console path</strong></p><ul><li>S3 → <strong>Create bucket</strong> →</li><li>Name: pedroops or yourdomain.com(should be globally unique).</li><li><strong>AWS Region:</strong> any (CloudFront is global).</li><li><strong>Block Public Access:</strong> <strong>Keep ON</strong> (all four checks).</li><li><strong>Bucket Versioning:</strong> <strong>Enable</strong> (easy rollback).</li><li><strong>Default encryption:</strong> <strong>Enable (SSE-S3)</strong>.</li><li>Click <strong>Create bucket.</strong></li></ul><h4><strong>3. Deploying the S3</strong></h4><ul><li>Navigate to the directory where your source code is located on your local machine, or you can clone my GitHub repo and build the React app on your local Linux server.</li></ul><pre>git clone https://github.com/Pascalpedro/my-portfolio-website.git<br>cd ~/my-portfolio-website/frontend<br><br>npm ci             # or npm install<br>npm run build      # creates ./build/<br># confirm:<br>ls -la build/<br></pre><ul><li>Install Dependencies &amp; Build</li></ul><pre># Make sure Node.js and npm/yarn are installed. Then:<br>npm ci             # or npm install<br>npm run build      # creates ./build/<br># confirm:<br>ls -la build/</pre><ul><li>Install &amp; Configure AWS CLI on the Linux Server</li></ul><pre># if not already installed:<br>sudo apt update &amp;&amp; sudo apt install awscli -y<br><br># Then configure it:<br>aws configure</pre><p>Provide your:</p><blockquote><strong>AWS Access Key ID</strong></blockquote><blockquote><strong>AWS Secret Access Key</strong></blockquote><blockquote><strong>Default region</strong> (e.g., us-east-1)</blockquote><blockquote><strong>Output format</strong> → json</blockquote><p>(Use an <strong>IAM user</strong> with S3 permissions, not root.)</p><ul><li>Sync Your Build Folder to the S3 Bucket</li></ul><pre>aws s3 sync build/ s3://your-s3-bucket-name/ --delete</pre><h4>4. Request an SSL Certificate in AWS ACM</h4><p>CloudFront requires HTTPS certificates <strong>in the us-east-1 region</strong>, even if your S3 bucket is elsewhere. So, the region must be in <strong>us-east-1 (N. Virginia)</strong> for CloudFront.</p><ul><li>Go to <strong>AWS Certificate Manager (ACM)</strong> → <strong>Request a certificate</strong>.</li><li>Choose <strong>Request a public certificate</strong> → <strong>Next</strong>.</li><li>Enter your domain:</li></ul><pre>pedroops.com<br>www.pedroops.com</pre><ul><li>Choose <strong>DNS validation</strong> → Next.</li><li>Click <strong>Create</strong>.</li></ul><p><strong>ACM will provide CNAME records</strong> that you need to add to Namecheap DNS. Copy the four NS servers Route53 gives you.</p><p><strong>In Namecheap</strong>, add and update your domain’s nameservers to use these four Route 53 NS. Log in to Namecheap → <strong>Domain List → Manage → Advanced DNS</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*sxAD7i3GCLGE9mXmQlTRyA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-b2btdGDQxv2OpCgdo8QAw.jpeg" /><figcaption>Namecheap DNS Hosting</figcaption></figure><p>Wait a few minutes; ACM will validate automatically. Status will become <strong>&quot;Issued.&quot;</strong> When issued, go back to CloudFront and attach this certificate to your distribution (Settings → Edit → Custom SSL certificate).</p><h4><strong>5. Create a CloudFront distribution with OAC (secure S3 origin)</strong></h4><p><strong>Goal:</strong> Serve your site via <strong>HTTPS</strong> at a fast global edge.</p><p><strong>Console path</strong></p><ul><li>CloudFront → <strong>Create distribution</strong>.</li><li><strong>Origin Settings</strong>:</li><li>Origin domain: choose your S3 bucket (the <em>bucket</em> endpoint, not the “website” endpoint).</li><li><strong>Origin access</strong>: <strong>Origin access control (OAC)</strong> → <strong>Create new OAC</strong> → Save.</li><li>CloudFront prompts to <strong>update the S3 bucket policy</strong>. Choose <strong>“Yes, update bucket policy” </strong>so that<strong> </strong>CloudFront will update the S3 bucket policy automatically.</li><li><strong>Default root object</strong>: index.html</li><li><strong>Viewer protocol policy:</strong> Redirect HTTP to HTTPS</li><li><strong>Allowed HTTP methods:</strong> GET, HEAD</li><li><strong>Cache policy:</strong> CachingOptimized (default)</li><li>Alternate domain names (CNAMEs): add pedroops.com and <a href="http://www.pedroops.com">www.pedroops.com</a> (N/B: use your own custom domain)</li><li>Custom SSL certificate: choose the ACM certificate you just validated.</li><li><strong>Create distribution.</strong> Wait until the status becomes <strong>Enabled</strong> and note the <strong>Distribution domain</strong> and <strong>Distribution ID.</strong></li></ul><h4>6. Create a Route53 hosted zone.</h4><blockquote><em>Important: re-create all required DNS records inside Route 53 before changing Namecheap nameservers—this avoids downtime for email and verification records.</em></blockquote><ul><li>Route 53 → Hosted zones → Create a hosted zone<strong> </strong>called<strong> ‘pedroops.com.’</strong></li><li>Type: choose <strong>Public hosted zone</strong></li><li>Re-create every record you need (copy from Namecheap).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V6iEvibDD3PMYGBuJh3AVA.png" /><figcaption>Route53 Records</figcaption></figure><h4>7. SPA routing and add a CloudFront function.</h4><p>Set CloudFront custom error responses for SPA deep routes. React SPAs need index fallbacks to avoid 404 on refresh:</p><ul><li>Add custom error response rule:<strong>CloudFront → Distribution → Error pages → Create custom respons</strong>e</li></ul><pre>403 → Response page /index.html, HTTP response code 200<br>404 → Response page /index.html, HTTP response code 200<em><br>(this ensures SPA deep routes don’t 404)</em></pre><p>This ensures React Router handles deep links.</p><ul><li>Create a tiny CloudFront function to <strong>redirect www → apex:</strong></li></ul><p><strong>CloudFront → Function → Create function</strong></p><ul><li>Function name: redirect-www-to-apex</li><li>Function code (Viewer request):</li></ul><pre>function handler(event) {<br>  var request = event.request;<br>  var host = request.headers.host.value.toLowerCase();<br><br>  // Only redirect the www host (leave apex and everything else untouched)<br>  if (host === &#39;www.pedroops.com&#39;) {<br>    // Rebuild query string (supports single and multivalue)<br>    var qsObj = request.querystring || {};<br>    var q = &#39;&#39;;<br>    for (var k in qsObj) {<br>      if (qsObj.hasOwnProperty(k)) {<br>        var item = qsObj[k];<br>        if (item &amp;&amp; item.value) {<br>          q += (q ? &#39;&amp;&#39; : &#39;&#39;) + k + &#39;=&#39; + item.value;<br>        } else if (item &amp;&amp; item.multiValue &amp;&amp; item.multiValue.length &gt; 0) {<br>          for (var i = 0; i &lt; item.multiValue.length; i++) {<br>            q += (q ? &#39;&amp;&#39; : &#39;&#39;) + k + &#39;=&#39; + item.multiValue[i].value;<br>          }<br>        }<br>      }<br>    }<br><br>    var location = &#39;https://pedroops.com&#39; + request.uri + (q ? &#39;?&#39; + q : &#39;&#39;);<br><br>    return {<br>      statusCode: 301,<br>      statusDescription: &#39;Moved Permanently&#39;,<br>      headers: {<br>        location: { value: location },<br>        // (Optional) HSTS if you want strict HTTPS:<br>        // &#39;strict-transport-security&#39;: { value: &#39;max-age=31536000; includeSubDomains; preload&#39; }<br>      }<br>    };<br>  }<br><br>  // Otherwise, continue to origin<br>  return request;<br>}</pre><p>Publish and associate with <strong>Viewer </strong>request for the default cache behavior.</p><h4>8. Verification checklists</h4><p>To verify if the DNS has been rerouted, let’s use the <strong>dig</strong> and <strong>curl</strong> commands that we ran at the beginning of the project.</p><ul><li>Using the terminal, run the below commands:</li></ul><pre># Check DNS resolution<br>dig pedroops.com<br>dig www.pedroops.com<br>dig pedroops.com +short<br>dig www.pedroops.com CNAME +short<br><br># Check certificate &amp; headers<br>curl -I https://pedroops.com<br>curl -I http://www.pedroops.com<br>curl -I pedroops.com<br><br># Check deep route (SPA)<br>curl -I https://pedroops.com/some/deep/route<br></pre><p>You should see <strong>server: AmazonS3 (origin) + via: … cloudfront.net</strong> and x-cache: Hit/Miss from cloudfront.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TWlNsdm7L--bse20p7pmGg.jpeg" /><figcaption>curl request</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/954/1*y0N7ddK72lP1heP4J_nD1g.jpeg" /><figcaption>curl request</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xKEqZ7f2iYk3PvEkTCe4cA.jpeg" /><figcaption>dig request</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fINN3A7C60kZp9d_UKkxaA.jpeg" /><figcaption>ping request</figcaption></figure><p>Alternatively, you can use the browser.</p><h3>(B). Phase 2: (CI/CD with GitHub Actions + OIDC)</h3><p>By using <strong>GitHub Actions + OIDC</strong> in this project:</p><ul><li><strong>CI/CD pipeline</strong>—Every time you push to main, your site rebuilds and deploys automatically.</li><li><strong>No long-lived keys</strong>—secure authentication (temporary credentials only).</li><li><strong>Automatic CloudFront cache invalidation</strong>—Ensures new changes appear instantly.</li><li><strong>Scalability</strong>—Can add test steps, linting, or deployment to staging.</li></ul><h4>1. Add the GitHub OIDC Provider in IAM (GitHub OIDC → AWS IAM Role)</h4><ul><li><strong>IAM → Identity providers → Add provider</strong></li><li><strong>Provider type</strong>: OpenID Connect</li><li><strong>Provider URL</strong>: <a href="https://token.actions.githubusercontent.com">https://token.actions.githubusercontent.com</a></li><li><strong>Audience:</strong> sts.amazonaws.com</li><li>Click <strong>Save.</strong></li></ul><h4>2. Create an IAM Role the workflow can assume</h4><ul><li><strong>IAM → Roles → Create role</strong></li><li><strong>Trusted entity type</strong>: Web identity</li><li><strong>Identity provider</strong>: select <strong>token.actions.githubusercontent.com </strong>(the one you just added).</li><li><strong>Audience:</strong> sts.amazonaws.com</li><li>Click <strong>Next</strong> to attach a custom policy</li></ul><h4>3. Trust policy (restrict to your repo + branch)</h4><ul><li>Open the new role → <strong>Trust relationships</strong> → Edit<strong> trust policy</strong> and paste the trust JSON below:</li></ul><pre>{<br>  &quot;Version&quot;: &quot;2012-10-17&quot;,<br>  &quot;Statement&quot;: [<br>    {<br>      &quot;Sid&quot;: &quot;GitHubOIDCTrust&quot;,<br>      &quot;Effect&quot;: &quot;Allow&quot;,<br>      &quot;Principal&quot;: {<br>        &quot;Federated&quot;: &quot;arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com&quot;<br>      },<br>      &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,<br>      &quot;Condition&quot;: {<br>        &quot;StringEquals&quot;: {<br>          &quot;token.actions.githubusercontent.com:aud&quot;: &quot;sts.amazonaws.com&quot;<br>        },<br>        &quot;StringLike&quot;: {<br>          &quot;token.actions.githubusercontent.com:sub&quot;: &quot;repo:Pascalpedro/my-portfolio-website:ref:refs/heads/main&quot;<br>        }<br>      }<br>    }<br>  ]<br>}</pre><p>This means only the <strong>main</strong> branch of Pascalpedro/my-portfolio-website can assume this role.</p><h4>4. Permissions policy (least-privilege)</h4><p>Create a <strong>customer-managed</strong> policy (IAM → Policies → Create policy → JSON) and attach it to the role. This grants:</p><ul><li>Upload/delete/list in your bucket; pedroops</li><li>Create invalidations on your distribution; E4K*******UIL</li></ul><pre>{<br>  &quot;Version&quot;: &quot;2012-10-17&quot;,<br>  &quot;Statement&quot;: [<br>    {<br>      &quot;Sid&quot;: &quot;S3ListBucket&quot;,<br>      &quot;Effect&quot;: &quot;Allow&quot;,<br>      &quot;Action&quot;: [&quot;s3:ListBucket&quot;],<br>      &quot;Resource&quot;: &quot;arn:aws:s3:::your-s3-bucket-name&quot;<br>    },<br>    {<br>      &quot;Sid&quot;: &quot;S3ObjectRW&quot;,<br>      &quot;Effect&quot;: &quot;Allow&quot;,<br>      &quot;Action&quot;: [<br>        &quot;s3:PutObject&quot;,<br>        &quot;s3:DeleteObject&quot;,<br>        &quot;s3:GetObject&quot;<br>      ],<br>      &quot;Resource&quot;: &quot;arn:aws:s3:::your-s3-bucket-name/*&quot;<br>    },<br>    {<br>      &quot;Sid&quot;: &quot;CloudFrontInvalidate&quot;,<br>      &quot;Effect&quot;: &quot;Allow&quot;,<br>      &quot;Action&quot;: &quot;cloudfront:CreateInvalidation&quot;,<br>      &quot;Resource&quot;: &quot;arn:aws:cloudfront::123456789012:distribution/YOUR_CLOUDFRONT_DIST_ID&quot;<br>    }<br>  ]<br>}</pre><p>Attach this policy to the role.</p><ul><li>Role name: GitHubActionsOIDC-PedroOps</li><li>Create the role.</li></ul><h4>5. Add GitHub Actions Workflow</h4><p>In your repo, create .github/workflows/deploy.ymlat the root project directory.</p><pre>name: Deploy React App to S3 + CloudFront<br><br>on:<br>  push:<br>    branches:<br>      - main<br><br>permissions:<br>  id-token: write    # Required for OIDC<br>  contents: read<br><br>env:<br>  AWS_REGION: us-east-1<br>  S3_BUCKET: your-s3-bucket-name<br>  CF_DISTRIBUTION_ID: YOUR_CLOUDFRONT_DIST_ID<br><br>jobs:<br>  deploy:<br>    runs-on: ubuntu-latest<br><br>    steps:<br>      # 1. Checkout code<br>      - name: Checkout code<br>        uses: actions/checkout@v4<br><br>      # 2. Setup Node.js<br>      - name: Setup Node.js<br>        uses: actions/setup-node@v4<br>        with:<br>          node-version: &#39;20&#39;<br>          cache: &#39;npm&#39;<br>          cache-dependency-path: &#39;frontend/package-lock.json&#39;<br><br>      # 3. Install dependencies &amp; build React<br>      - name: Install dependencies &amp; build<br>        working-directory: frontend<br>        run: |<br>          npm ci<br>          npm run build<br><br>      # 4. Configure AWS credentials (OIDC)<br>      - name: Configure AWS credentials (OIDC)<br>        uses: aws-actions/configure-aws-credentials@v4<br>        with:<br>          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsOIDC-PedroOps<br>          aws-region: ${{ env.AWS_REGION }}<br><br>      # 5. Sync build folder to S3<br>      - name: Sync to S3<br>        run: |<br>          aws s3 sync ./frontend/build s3://${{ env.S3_BUCKET }} --delete --exact-timestamps<br><br>       # 6. Optional: make index.html less cachey (recommended for SPAs)<br>      - name: Set Cache-Control for index.html<br>        run: |<br>          aws s3 cp s3://${{ env.S3_BUCKET }}/index.html /tmp/index.html<br>          aws s3 cp /tmp/index.html s3://${{ env.S3_BUCKET }}/index.html \<br>            --cache-control &quot;no-cache, no-store, must-revalidate&quot; \<br>            --content-type &quot;text/html&quot; \<br>            --metadata-directive REPLACE<br><br>      # 7. Invalidate CloudFront cache<br>      - name: Invalidate CloudFront cache<br>        run: |<br>         aws cloudfront create-invalidation \<br>          --distribution-id ${{ env.CF_DISTRIBUTION_ID }} \<br>          --paths &quot;/*&quot;<br><br>      # 8. Notify<br>      - name: Deployment complete<br>        run: echo &quot;🚀 Deployed pedroops.com from frontend/ via OIDC!&quot;</pre><h4>6. Verify the pipeline</h4><p>On GitHub:</p><ul><li><strong>GitHub → Actions:</strong> confirm the Deploy React workflow ran and succeeded.</li></ul><p>On AWS CLI:</p><pre># S3<br>aws s3 ls s3://pedroops/ - recursive | tail   #shows fresh timestamps<br><br># CloudFront<br>aws cloudfront list-invalidations - distribution-id YOUR_CLOUDFRONT_DIST_ID     # shows your invalidation</pre><h3>CONCLUSION</h3><p>This migration project showcases the full lifecycle of deploying and managing a <strong>cloud-native portfolio site</strong>:</p><ul><li>From <strong>manual S3 sync + CloudFront invalidation</strong> →</li><li>To secure automated<strong> CI/CD with GitHub Actions OIDC</strong>.</li></ul><p>The final setup is <strong>scalable, secure, and automated</strong>, reflecting modern <strong>AWS Cloud + DevOps practices</strong>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4e38c5831341" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[#  Building My Portfolio with React, FastAPI & MongoDB — A Full-Stack Walkthrough]]></title>
            <link>https://attamapascalpedro.medium.com/building-my-portfolio-with-react-fastapi-mongodb-a-full-stack-walkthrough-1d626633f67f?source=rss-61e9b93b82d9------2</link>
            <guid isPermaLink="false">https://medium.com/p/1d626633f67f</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[web-hosting]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Pascal Attama]]></dc:creator>
            <pubDate>Fri, 25 Jul 2025 00:35:20 GMT</pubDate>
            <atom:updated>2025-09-01T16:57:26.146Z</atom:updated>
            <content:encoded><![CDATA[<h3>How I Built a Cloud Portfolio with React, FastAPI &amp; MongoDB</h3><h4>Practical cloud engineering project for showcasing full-stack skills!!!</h4><h3><strong>INTRODUCTION</strong></h3><p>In today’s cloud-native world, showcasing your professional skills requires more than just a static web page. This project is a<strong> dynamic, full-stack portfolio web application</strong> built to help<strong> engineers, developers, and tech professionals</strong> present their work, certifications, and credentials with real-time interactivity, analytics, and cloud-native deployment.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MNq5A4FMHJD-XBXZhK-BAw.jpeg" /><figcaption><a href="https://pascalattama.netlify.app">Portfolio Overview</a></figcaption></figure><p>🔗<a href="https://github.com/Pascalpedro/my-portfolio-website"> GitHub Repo</a> | 🌐 <a href="https://pascalattama.netlify.app">Live Demo</a></p><p>As a cloud engineer, this project not only helps you demonstrate your DevOps and full-stack abilities but also serves as a template for deploying modern web applications using scalable, cloud-based services.</p><p>In this article, I walk you through how I built my <strong>full-stack developer portfolio</strong>, hosted on the cloud with <strong>React, FastAPI, MongoDB, Netlify, and Railway.</strong></p><h3><strong>TECH STACK OVERVIEW</strong></h3><p>This project uses a modern full-stack architecture:</p><pre><strong>|</strong> <strong>Layer</strong>    <strong> |</strong> <strong>Stack                                      </strong> <strong>|</strong><br><strong>| - - - - - | - - - - - - - - - - - - - - - - - - - - - - |</strong><br><strong>|</strong> Frontend <strong> |</strong> React, TailwindCSS, Framer Motion           <strong>|</strong><br><strong>|</strong> Backend   <strong>|</strong> FastAPI (Python), Motor (MongoDB async)     <strong>|</strong><br><strong>|</strong> Database  <strong>|</strong> MongoDB Atlas                               <strong>|</strong><br><strong>|</strong> Hosting   <strong>|</strong> Netlify (frontend), Railway (backend)<strong>       |</strong><br><strong>|</strong> Analytics<strong> |</strong> Google Analytics 4                          <strong>|</strong><br><strong>|</strong> DevOps    <strong>|</strong> Git, Docker + Docker Compose                <strong>|<br></strong></pre><h3>FEATURES &amp; FUNCTIONAL HIGHLIGHTS</h3><h4>🔹 Modern Full-Stack Architecture</h4><ul><li><strong>Frontend</strong>: Built with <strong>React</strong>, styled using <strong>TailwindCSS,</strong> and enhanced with <strong>Framer Motion</strong> for UI animation and user experience.</li><li><strong>Backend:</strong> A scalable <strong>FastAPI</strong> service written in <strong>Python</strong>, exposing a <strong>REST API</strong> to serve portfolio data and handle form submissions.</li><li><strong>Database</strong>: <strong>MongoDB Atlas</strong>, a cloud-native <strong>NoSQL database</strong> that stores dynamic portfolio content and contact form data.</li></ul><h4>🔹 Cloud-Ready Deployment &amp; CI/CD</h4><ul><li><strong>Frontend Hosting</strong>: Deployed to <strong>Netlify</strong>, utilizing build automation and CDN-powered delivery.</li><li><strong>Backend Hosting</strong>: Deployed to <strong>Railway</strong>, supporting seamless continuous deployment from <strong>GitHub</strong>.</li><li><strong>Infrastructure-as-Code Ready</strong>: The stack can be containerized using <strong>Docker</strong> and deployed via <em>Docker-Compose</em>, aligning with <strong>IaC </strong>and <strong>Kubernetes</strong> adoption patterns.</li><li><strong>Environment Management</strong>: Uses <em>.env</em> files to securely manage configuration across environments — ideal for <strong>multi-stage cloud deployments.</strong></li></ul><h4>🔹 DevOps &amp; Monitoring Integration</h4><ul><li><strong>Cloud Integration Patterns:<br>- </strong>Simulates real-world separation of frontend/backend concerns.<br>- Can be deployed on <strong>AWS </strong>using <strong>S3 + CloudFront (frontend)</strong> and <strong>EC2/Fargate/Lambda (backend)</strong></li><li><strong>Analytics &amp; Monitoring:<br>- </strong>Integrated with <strong>Google Analytics 4 (GA4)</strong> to track user interactions like CV views and downloads, reflecting cloud observability best practices.</li><li><strong>Secure Data Handling:<br>- MongoDB</strong> hosted on Atlas with secure connection strings.<br>- Supports future integrations like <strong>AWS Secrets Manager, CloudWatch</strong>, or<strong> SES</strong> for alerting and mailing.</li></ul><h4>🔹 Dynamic &amp; Maintainable</h4><ul><li>Projects, certifications, and CVs are served dynamically from the backend.</li><li>Google Drive is used for CV hosting to reduce static asset load</li><li>Adminless and scalable — ideal for solo deployment or resume infrastructure.</li></ul><h3>Why This Portfolio Matters for a Cloud Engineer</h3><p>This isn’t just a personal website — it’s a miniature cloud-native application, built from the ground up to highlight:</p><ul><li>✅ Proficiency in <strong>multi-tier architecture</strong>.</li><li>✅ Skill in deploying full-stack applications to the cloud.</li><li>✅ Demonstrates containerization potential using <strong>Dockerfile</strong> and <strong>docker-compose.yml</strong>.</li><li>✅ Separates concerns across services, mirroring <strong>microservice architecture</strong> best practices.</li><li>✅ The ability to build <strong>developer-facing tools</strong> and user interfaces.</li><li>✅ Real-world implementation of <strong>API integration</strong>, <strong>async database handling,</strong> and <strong>CI/CD-ready architecture</strong>.</li><li>✅ Illustrates how to securely handle <strong>environment variables</strong> and <strong>backend secrets</strong>.</li></ul><p>This application is designed to be <strong>migratable to AWS infrastructure</strong>, enabling:</p><ul><li><strong>S3 + CloudFront</strong> for static frontend hosting</li><li><strong>API Gateway</strong> + <strong>Lambda or ECS</strong> for backend logic</li><li><strong>DynamoDB</strong> or <strong>DocumentDB</strong> for backend persistence</li><li><strong>CloudWatch</strong> for event monitoring</li><li><strong>SES</strong> or <strong>SNS</strong> for contact form alerting</li></ul><p>In essence, it helps you showcase your DevOps maturity while hosting your credentials in a live, interactive, and cloud-native environment.</p><h3>🗂️ Project Structure</h3><pre>bash<br><br>my-portfolio-website<br>├── frontend/ # React App<br>│ ├── src/<br>│ ├── public/<br>│ ├── netlify.toml<br>│ ├── Dockerfile #Frontend Dockerfile<br>│ └── .env<br>├── backend/ # FastAPI App<br>│ ├── server.py<br>│ ├── email_utils.py<br>│ ├── venv/ #Virtual Env<br>│ ├── Dockerfile # Backend Dockerfile<br>│ └── requirements.txt # Python dependencies<br>│ └── .env<br>│── nginx.conf<br>├── .gitignore<br>├── render.yaml<br>├── docker-compose.yml<br>└── README.md</pre><h3>Getting Started</h3><p>1. Clone the Project Repo</p><pre>bash<br><br>git clone https://github.com/Pascalpedro/my-portfolio-website.git<br>cd my-portfolio-website</pre><p>2. Backend Setup (FastAPI)</p><pre>bash<br><br>cd backend<br>python -m venv venv<br>source venv/bin/activate # On Windows: venv\Scripts\activate<br>pip install -r requirements.txt</pre><ul><li>create .env in backend/:</li></ul><pre>env<br><br>MONGO_URL=mongodb+srv://&lt;username&gt;:&lt;password&gt;@cluster.mongodb.net/<br>DB_NAME=portfolio_db</pre><ul><li>Run the backend:</li></ul><pre>bash<br><br>uvicorn server:app - reload - port 8000</pre><p>3. Frontend Setup (React)</p><pre>bash<br><br>cd frontend<br>npm install</pre><ul><li>Create .env in frontend/:</li></ul><pre>env<br><br>REACT_APP_API_URL=http://localhost:8000/api<br>REACT_APP_GA_MEASUREMENT_ID=G-XXXXXXX</pre><ul><li>Start the frontend server:</li></ul><pre>bash<br><br>npm start</pre><h3>⚙️ Step-by-Step Setup</h3><p>🔗 Kindly find the complete, well-detailed step-by-step setup instructions on my <a href="https://github.com/Pascalpedro/my-portfolio-website.git">GitHub Project Repo</a></p><h3>📷 Screenshots</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pokrntgTPHdzPqyZP5RbwA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WRqvk0mzXWG5-JP6O_vnnQ.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*38nb2SaqNsDcqJGhvAn6rw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C_jzX6_rJRchA3tJ09eSFg.png" /></figure><h3>🎯 Conclusion</h3><p>This project goes beyond a typical portfolio—it’s a cloud-engineered personal showcase platform. Built with production-ready practices, it provides cloud engineers with a valuable asset to:</p><ul><li>Exhibit their projects and certifications</li><li>Practice full-stack cloud deployment workflows</li><li>Demonstrate real-world skills across frontend, backend, database, and DevOps tooling</li></ul><p>Whether you’re aiming to impress recruiters or clients or looking to build out your cloud development portfolio, this project equips you with everything you need to stand out in today’s competitive tech landscape.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1d626633f67f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Stepping into Ubuntu: A VirtualBox Installation Guide.]]></title>
            <link>https://attamapascalpedro.medium.com/how-to-setup-ubuntu-linux-on-a-virtualbox-edd9ca9fe5ff?source=rss-61e9b93b82d9------2</link>
            <guid isPermaLink="false">https://medium.com/p/edd9ca9fe5ff</guid>
            <category><![CDATA[linux-tutorial]]></category>
            <category><![CDATA[virtualbox]]></category>
            <category><![CDATA[ubuntu-desktop]]></category>
            <category><![CDATA[linux]]></category>
            <dc:creator><![CDATA[Pascal Attama]]></dc:creator>
            <pubDate>Fri, 17 May 2024 05:33:50 GMT</pubDate>
            <atom:updated>2024-09-14T13:36:44.681Z</atom:updated>
            <content:encoded><![CDATA[<p><strong>How to Setup Ubuntu Linux on a VirtualBox.</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GYsUU3SvLCacBDe-ZYEclw.jpeg" /></figure><h3>INTRODUCTION</h3><p>Have you ever felt curious about Ubuntu Linux and wanted to try it out? Perhaps you’re a developer wishing to test software or a student yearning to explore its vast capabilities but, at the same time, worried about tampering with your main system. VirtualBox offers a fantastic solution!</p><p>Whatever your reason, VirtualBox offers a fantastic platform to experience Ubuntu in a safe and isolated environment.</p><p>This guide will walk you through the installation process, transforming your computer into a virtual playground for exploring the wonders of Ubuntu.</p><p>Are you ready to embark on this adventure? Alright, follow the steps below; let’s rock&amp; roll...🤝</p><h3>(1). PREPARATION:</h3><p>However, some prerequisites must be completed before we can begin the installation procedures on the Ubuntu Linux machine running in VirtualBox. These consist of:</p><ul><li>First, we must find the VirtualBox software, ensure its compatibility with your system, and download it from its website: <a href="https://www.virtualbox.org/wiki/Downloads">https://www.virtualbox.org/wiki/Downloads</a>, and then install it with the right settings. This is a straightforward process.</li><li>Also, we need to obtain and download the desired Ubuntu ISO file from the official website: <a href="https://ubuntu.com/download/desktop">https://ubuntu.com/download/desktop</a> and choose a 64-bit version for optimal performance.</li></ul><p>Have you finished downloading the two essential pieces of software mentioned above? Then stay tuned for the next level. 😉</p><p>Below is a well-detailed explanation with pictorial illustrations and a step-by-step guide on the Ubuntu Linux installation and setup.</p><h3>(2). CREATING THE VIRTUAL MACHINE:</h3><p>Now, to begin your setup, these are the steps to follow:</p><blockquote>(2.1). Launch the newly installed VirtualBox application on your computer and click the “New” button on the homepage to create a new machine.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GYsUU3SvLCacBDe-ZYEclw.jpeg" /><figcaption>VirtualBox Homepage</figcaption></figure><blockquote>(2.2). Provide a name for your virtual machine. A best practice is using the OS name and version for uniqueness and simplicity. (e.g., “Ubuntu-22.04”)</blockquote><blockquote>(2.3). Select “Linux” as the type and choose “Ubuntu (64-bit)” or the appropriate version you downloaded as the version.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wR0NrmNvfo12YLV3etEp_w.jpeg" /><figcaption>Name and Operating system settings</figcaption></figure><blockquote>(2.4). Assign a memory size of 2048MB (2 GB) for your RAM to allocate a memory size.</blockquote><p>By default, it will assign you a memory size of 1024MB (1 GB), but a good starting point for RAM allocation is 2048MB (2 GB), which can also be adjusted based on your system&#39;s available memory and the desired performance of the virtual machine. The memory can be allocated using the slider or by entering a value in the memory field.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8-rPgO-wXHqi6B7GFV54sw.jpeg" /><figcaption>Memory size allocation</figcaption></figure><blockquote>(2.5). Select “Create a virtual hard disk now” and click the ‘create’ button to create a Virtual Hard Disk.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3_0DGGq1UztJ6whbJckJAg.jpeg" /><figcaption>Virtual Harddisk creation</figcaption></figure><blockquote>(2.6). Keep the default option “VDI (VirtualBox Disk Image)” selected and click the ‘next’ button.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EA7jklsEJepZdxWP2j7etw.jpeg" /><figcaption>Hard disk file type selection</figcaption></figure><blockquote>(2.7). Choose “dynamically allocated” for disk storage type. This will allow the virtual disk to grow as needed, and click the ‘next’ button.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N2QtYqruCSxD9v2MNSJvSQ.jpeg" /><figcaption>Physical hard disk storage type selection</figcaption></figure><blockquote>(2.8). Set the desired location and size for the virtual disk, and click the ‘create’ button. A minimum of 20 GB is recommended, but you can allocate more depending on your needs.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W5VCMTRrujECE8XKF6dd-g.jpeg" /><figcaption>File location and size</figcaption></figure><p>The above steps will add a brand new Ubuntu machine to your VirtualBox machine.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dvP8UdBQBDL3975immCybw.jpeg" /><figcaption>Newly created Ubuntu Virtual HardDisk</figcaption></figure><h3>(3). INSTALLING UBUNTU</h3><p>After creating the virtual hard disk, we will have to install and set up Ubuntu ISO files on the newly created machine.</p><p>To attach the Ubuntu ISO:</p><blockquote>(3.1). On the virtual machine homepage, navigate to the storage section and click the empty (optical drive) controller icon under “IDE secondary device.”.</blockquote><blockquote>(3.2). Select “Choose virtual disk file...” from the drop-down menu and locate and select the Ubuntu ISO file downloaded earlier, and load it into the IDE device.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XfGYwjBLsfWWW8rQ5GInUQ.jpeg" /><figcaption>Attaching the ISO file</figcaption></figure><p>To start and install the virtual machine:</p><blockquote>(3.3). Select your newly created virtual machine on the VirtualBox homepage and click the “Start” button.</blockquote><blockquote>(3.4). Click on the first option, “Try or install Ubuntu,” and wait for a while for the booting process to activate.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eEDdjFlmVclht-wMLQbMfQ.jpeg" /><figcaption>Starting the machine</figcaption></figure><blockquote>(3.5). Select your language.</blockquote><blockquote>(3.6). Choose “Try Ubuntu” or “Install Ubuntu,” depending on your preference, and select your keyboard language and layout.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*99Z9fS1CghB5Ybb-6XQlKA.jpeg" /><figcaption>Installing ubuntu</figcaption></figure><blockquote>(3.7). Choose the installation type (normal installation is recommended) tick ‘Download updates while installing Ubuntu’ and click continue.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Lt9J2gN8FPdLeu9qlt9adw.jpeg" /><figcaption>Installation type</figcaption></figure><blockquote>(3.8). Choose “Erase disk and install Ubuntu.” and click the “Install Now” button.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Mi2h2V6VXSW2DMEZAZ5ANA.jpeg" /><figcaption>Installation type</figcaption></figure><p><em>N/B: The message on this page appears because you’re setting up a new virtual machine with Ubuntu, and VirtualBox hasn’t detected an existing operating system on the virtual disk you created. This is perfectly normal for a fresh installation. This option is exactly what you want since you’re creating a new Ubuntu system on the virtual machine. Don’t worry, it won’t erase anything on your physical computer. It will erase the virtual disk (which is currently empty anyway) and prepare it for the Ubuntu installation. It refers to formatting the virtual disk you allocated for the Ubuntu installation within VirtualBox.</em></p><p>(3.9). Select your location and click continue.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UCi3SKiV0LQN36bKHE08dA.jpeg" /><figcaption>Setting the location</figcaption></figure><blockquote>(3.10). Set up your user account credentials and authentication details, where:</blockquote><ul><li>“Your name”: Your real name, which is optional and used for display purposes.</li><li>“Your computer’s name”: hostname, used for network identification, sharing, and remote access.</li><li>“Username”: Username, used for login credentials for user authentication.</li><li>“Password”: choose a strong password that comprises “capital, small, digits, and symbols.”</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*HRKF2jABuu-Mt_cFtEXGNg.jpeg" /></figure><p>The installation process might take some time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gXgiuTuLxZpFuWSboRmmww.jpeg" /></figure><blockquote>(3.11). Once complete, you’ll be prompted to restart the virtual machine to use the new installation. Remove the virtual media (ISO file) from the settings before restarting.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*B5Gv6nZNIxlVdcgDTT6eZA.jpeg" /></figure><h3>(4). BOOTING INTO UBUNTU</h3><blockquote>(4.1). Select your virtual machine and click “Start.”</blockquote><blockquote>(4.2). You’ll be presented with the Ubuntu login screen. Enter your username and password created during installation.</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*whd5Xsfmu9HjHgCb3HEg2w.jpeg" /></figure><p>The virtual machine will boot into the newly installed Ubuntu system.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TWOTJIZ_yDDX2y-cYgJNEA.jpeg" /></figure><p><strong>CONGRATULATIONS!!!</strong> You’ve successfully installed Ubuntu on your VirtualBox virtual machine. Now you can explore the Ubuntu desktop and start using it. <strong>CHEERS!!!😁😁😁</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=edd9ca9fe5ff" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>