<?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: Nnamdi Felix Ibe</title>
    <description>The latest articles on DEV Community by Nnamdi Felix Ibe (@ndcodes).</description>
    <link>https://dev.to/ndcodes</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%2F3994627%2F3c1e9fc3-98cd-45c4-9e34-3ef72ece6604.jpg</url>
      <title>DEV Community: Nnamdi Felix Ibe</title>
      <link>https://dev.to/ndcodes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ndcodes"/>
    <language>en</language>
    <item>
      <title>Day 29: A Pull Request Needs Two People, and Peering Needs Two Routes</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Wed, 19 Aug 2026 19:51:41 +0000</pubDate>
      <link>https://dev.to/ndcodes/a-pull-request-needs-two-people-and-peering-needs-two-routes-5emj</link>
      <guid>https://dev.to/ndcodes/a-pull-request-needs-two-people-and-peering-needs-two-routes-5emj</guid>
      <description>&lt;p&gt;Some things cannot be finished alone, by design. A change that only you have seen is not reviewed. A network connection that only works in one direction is not a connection. Day 29 was two versions of the same idea: both sides have to agree, and half of it is worse than none because half of it looks like it should work.&lt;/p&gt;

&lt;p&gt;One Git task, one AWS task. Open a pull request, get it reviewed and merged, then peer a public VPC with a private one so instances on each side can talk. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull requests: the gate, not the merge
&lt;/h2&gt;

&lt;p&gt;The first thing worth saying is that a pull request is not a Git feature. Git has no idea what one is. It is a workflow layer that hosting platforms bolt on top — Gitea here, GitHub and GitLab elsewhere — and underneath, a merged PR is &lt;code&gt;git merge&lt;/code&gt; and nothing more.&lt;/p&gt;

&lt;p&gt;Which raises the obvious question: if the merge is trivial, what is the PR for?&lt;/p&gt;

&lt;p&gt;The gate. A named reviewer, a recorded approval, a discussion attached to the change, and a permanent record of who agreed to what. On this task, that meant opening the request against master from a feature branch, adding a second user as reviewer, then logging in as that user to read the diff, approve it, and merge with a merge commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Everything before and after the PR is ordinary git&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; fox-grapes        &lt;span class="c"&gt;# what you are proposing&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; master            &lt;span class="c"&gt;# what it is going into&lt;/span&gt;

&lt;span class="c"&gt;# ...PR happens in the web UI...&lt;/span&gt;

git checkout master
git pull origin master
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--decorate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things catch people out. &lt;strong&gt;Base and compare get reversed constantly&lt;/strong&gt; — base is where the code is going, compare is where it comes from. Flip them, and you propose merging master into your feature branch, which usually shows nothing and reads as a broken UI rather than a mistake.&lt;/p&gt;

&lt;p&gt;And the reviewer has to be a genuinely different user. Approving your own pull request is the one move that empties the mechanism of all meaning, which is why most platforms refuse it.&lt;/p&gt;

&lt;p&gt;Worth knowing the third option too: "create a merge commit" preserves the branch shape, exactly the &lt;code&gt;--no-ff&lt;/code&gt; behaviour from Day 25. Squash flattens the branch to one commit. Rebase replays them with no merge commit at all. Same code, three different stories in the history.&lt;/p&gt;

&lt;p&gt;One small thing that trips everyone once: merging on the server does nothing to your local clone. &lt;code&gt;git pull&lt;/code&gt; afterwards or your master sits silently behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  VPC peering: one connection, two route tables
&lt;/h2&gt;

&lt;p&gt;The AWS task was peering a public VPC with a private one so an instance in each could reach the other. And it opened with a problem that had nothing to do with peering at all.&lt;/p&gt;

&lt;p&gt;SSH to the public instance simply hung. No refusal, no error — just silence. The cause was in the security group:&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="nl"&gt;"IpPermissions"&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;"IpProtocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"FromPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ToPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"IpRanges"&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;"UserIdGroupPairs"&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;"GroupId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sg-xxxxxxxx"&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;That rule says "allow SSH from anything already inside this security group" — and the group it references is the one attached to the instance itself. Traffic from another instance in that group would match. My client host, sitting outside AWS entirely, never could.&lt;/p&gt;

&lt;p&gt;That is the tell worth memorising: &lt;strong&gt;&lt;code&gt;UserIdGroupPairs&lt;/code&gt; populated and &lt;code&gt;IpRanges&lt;/code&gt; empty means the rule is SG-referenced, not CIDR-based.&lt;/strong&gt; Nothing external gets in, and because security groups drop rather than reject, you get a hang instead of a message.&lt;/p&gt;

&lt;p&gt;Then a second problem, which I liked. The task wanted my public key in the instance's &lt;code&gt;authorized_keys&lt;/code&gt;. But writing that file needs SSH, and SSH needs that file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EC2 Instance Connect&lt;/strong&gt; breaks the loop. It pushes a public key to the instance out-of-band through the AWS API, valid for sixty seconds — long enough to get in and write the key permanently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2-instance-connect send-ssh-public-key &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; &amp;lt;instance-id&amp;gt; &lt;span class="nt"&gt;--instance-os-user&lt;/span&gt; ec2-user &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--availability-zone&lt;/span&gt; us-east-1b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ssh-public-key&lt;/span&gt; file:///root/.ssh/id_rsa.pub &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ssh &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="nt"&gt;-i&lt;/span&gt; /root/.ssh/id_rsa ec2-user@&amp;lt;public-ip&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"mkdir -p ~/.ssh &amp;amp;&amp;amp; chmod 700 ~/.ssh &amp;amp;&amp;amp; echo '&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /root/.ssh/id_rsa.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;' &amp;gt;&amp;gt; ~/.ssh/authorized_keys &amp;amp;&amp;amp; chmod 600 ~/.ssh/authorized_keys"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; is not stylistic. Sixty seconds is not enough to run two commands you are typing by hand, and the second one failing on an expired key looks exactly like a permissions problem.&lt;/p&gt;

&lt;p&gt;With access sorted, the peering itself is four checks, and this is the part that matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. The connection must be ACTIVE, not pending-acceptance&lt;/span&gt;
aws ec2 accept-vpc-peering-connection &lt;span class="nt"&gt;--vpc-peering-connection-id&lt;/span&gt; &amp;lt;pcx-id&amp;gt;

&lt;span class="c"&gt;# 2 and 3. A route on EACH side, pointing to the other's CIDR&lt;/span&gt;
aws ec2 create-route &lt;span class="nt"&gt;--route-table-id&lt;/span&gt; &lt;span class="nv"&gt;$DEF_RTB&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--destination-cidr-block&lt;/span&gt; 10.1.0.0/16 &lt;span class="nt"&gt;--vpc-peering-connection-id&lt;/span&gt; &amp;lt;pcx-id&amp;gt;

aws ec2 create-route &lt;span class="nt"&gt;--route-table-id&lt;/span&gt; &lt;span class="nv"&gt;$PRIV_RTB&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--destination-cidr-block&lt;/span&gt; 172.31.0.0/16 &lt;span class="nt"&gt;--vpc-peering-connection-id&lt;/span&gt; &amp;lt;pcx-id&amp;gt;

&lt;span class="c"&gt;# 4. The target SG must allow the traffic type from the source CIDR&lt;/span&gt;
aws ec2 authorize-security-group-ingress &lt;span class="nt"&gt;--group-id&lt;/span&gt; &amp;lt;private-sg&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; icmp &lt;span class="nt"&gt;--port&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--cidr&lt;/span&gt; 172.31.0.0/16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Peering is bidirectional by nature but &lt;strong&gt;not&lt;/strong&gt; by configuration. One connection object, two route tables, and you have to edit both. A route on only one side produces traffic that arrives and cannot reply, which presents as a total failure rather than a partial one.&lt;/p&gt;

&lt;p&gt;Three details worth carrying forward. &lt;code&gt;--port -1&lt;/code&gt; with ICMP means all ICMP types — ping is ICMP, not TCP, so opening TCP ports does precisely nothing for it. If all four checks pass and it still fails, look at network ACLs, which, unlike security groups, are stateless and need explicit rules in both directions. And peering is &lt;strong&gt;not transitive&lt;/strong&gt;: A peered to B and B peered to C does not let A reach C.&lt;/p&gt;

&lt;p&gt;One honest note. I opened port 22 to &lt;code&gt;0.0.0.0/0&lt;/code&gt; to get moving in a throwaway lab, which directly contradicts what I wrote on Day 22 about scoping SSH to &lt;code&gt;/32&lt;/code&gt;. In a lab, that is a shortcut. Anywhere real, it is the thing you get audited for, and I would rather flag it than leave it in a code block.&lt;/p&gt;

&lt;p&gt;Two small mistakes that cost me time, both embarrassing and both instructive. I pasted a placeholder straight into the shell — &lt;code&gt;&amp;lt;sg-id&amp;gt;&lt;/code&gt; makes bash attempt an input redirect and throw &lt;code&gt;No such file or directory&lt;/code&gt;. The angle brackets are notation, not syntax. And I ran &lt;code&gt;ping "$PRIVATE_IP"&lt;/code&gt; on the remote host, where that variable had never been set. Local shell variables do not travel over SSH.&lt;/p&gt;

&lt;h2&gt;
  
  
  Half a handshake is not a handshake
&lt;/h2&gt;

&lt;p&gt;An approval you gave yourself is not a review. A route on one side is not a path. Both tasks fail in the same quiet way: everything you configured is correct, and the thing still does not work, because the other half was never done.&lt;/p&gt;

&lt;p&gt;So here is the Day 29 question. Where in your setup have you built one direction of something and assumed the other side agreed?&lt;/p&gt;

&lt;p&gt;Day 29 down. Seventy-one to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>git</category>
      <category>aws</category>
      <category>networking</category>
    </item>
    <item>
      <title>Day 28: Cherry-Pick Takes One Commit, and the Tag Is the Address</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Mon, 17 Aug 2026 16:51:42 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-28-cherry-pick-takes-one-commit-and-the-tag-is-the-address-k33</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-28-cherry-pick-takes-one-commit-and-the-tag-is-the-address-k33</guid>
      <description>&lt;p&gt;Both of today's tasks came down to being precise about exactly what you are moving and exactly where it is going. In one, the answer is a single commit out of a branch full of them. In the other, it is an image whose destination is hidden somewhere you would not think to look.&lt;/p&gt;

&lt;p&gt;One Git task, one AWS task. Cherry-pick one commit onto master, then create a private ECR repository and push a Docker image to it. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cherry-pick: one commit, not the branch it came from
&lt;/h2&gt;

&lt;p&gt;Merging takes a whole branch. Sometimes you want one commit out of it and nothing else — the fix that has to ship today, sitting on a feature branch that is nowhere near ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /usr/src/kodekloudrepos/&amp;lt;repo&amp;gt;

git branch &lt;span class="nt"&gt;-a&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &amp;lt;feature-branch&amp;gt;
git show &amp;lt;commit-id&amp;gt;            &lt;span class="c"&gt;# confirm it is the change you actually mean&lt;/span&gt;

git checkout master
git cherry-pick &amp;lt;commit-id&amp;gt;

git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
git status
git push origin master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail worth understanding is what actually happens. Cherry-pick does not move the commit. It copies the &lt;strong&gt;change&lt;/strong&gt;, and the result gets a brand-new hash because a commit's identity includes its parent, and the parent is different now. The original stays exactly where it was on the source branch, untouched.&lt;/p&gt;

&lt;p&gt;That has a consequence people meet the hard way. If you cherry-pick a fix onto master and then later merge the whole feature branch, Git is looking at two different commits containing the same diff. It frequently conflicts on the second one, and the conflict is confusing precisely because both sides look correct. Pick or merge. Try not to do both to the same change.&lt;/p&gt;

&lt;p&gt;Two habits worth forming. Cherry-pick applies &lt;strong&gt;onto the branch you are standing on&lt;/strong&gt;, exactly like merge, so &lt;code&gt;git branch --show-current&lt;/code&gt; before you run it is two seconds well spent. And when you take several commits at once, order matters — &lt;code&gt;git cherry-pick A B C&lt;/code&gt; applies them in the order given, and picking a commit whose dependency you skipped produces conflicts that look inexplicable until you notice the gap.&lt;/p&gt;

&lt;p&gt;There is also a wider signal here. Cherry-picking is a precision instrument, and reaching for it constantly usually means the branching model is fighting you rather than helping.&lt;/p&gt;

&lt;h2&gt;
  
  
  ECR: docker push has nowhere to put an address
&lt;/h2&gt;

&lt;p&gt;The AWS task was creating a private ECR repository and pushing an image into it. The commands are unremarkable right up until the one that actually matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1
aws sts get-caller-identity

aws ecr create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; xfusion-ecr &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

&lt;span class="nv"&gt;REPO_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecr describe-repositories &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-names&lt;/span&gt; xfusion-ecr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'repositories[0].repositoryUri'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$REPO_URI&lt;/span&gt;
&lt;span class="c"&gt;# &amp;lt;account-id&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/xfusion-ecr&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then authenticate, build, tag, and push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 | &lt;span class="se"&gt;\&lt;/span&gt;
  docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &lt;span class="nv"&gt;$REPO_URI&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; /root/pyapp
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; xfusion-ecr:latest &lt;span class="nb"&gt;.&lt;/span&gt;

docker tag xfusion-ecr:latest &lt;span class="nv"&gt;$REPO_URI&lt;/span&gt;:latest
docker push &lt;span class="nv"&gt;$REPO_URI&lt;/span&gt;:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look closely at &lt;code&gt;docker push&lt;/code&gt;. It takes no destination. There is no &lt;code&gt;--registry&lt;/code&gt; flag, no target argument. &lt;strong&gt;The tag is the address.&lt;/strong&gt; That &lt;code&gt;docker tag&lt;/code&gt; line is not a cosmetic labelling step you can skip — it is the step that tells Docker which registry the image belongs to. Push a plain local name, and Docker assumes Docker Hub, tries there, and rejects you with an error that sounds like a permissions problem.&lt;/p&gt;

&lt;p&gt;Once that clicks, the repository URI stops looking like noise and starts reading like what it is: &lt;code&gt;&amp;lt;account-id&amp;gt;.dkr.ecr.&amp;lt;region&amp;gt;.amazonaws.com/&amp;lt;repo&amp;gt;&lt;/code&gt;. Your account, your region, your repository. An address, in address order.&lt;/p&gt;

&lt;p&gt;Two security details in that login line. ECR credentials are &lt;strong&gt;temporary&lt;/strong&gt; — &lt;code&gt;get-login-password&lt;/code&gt; mints a token valid for about twelve hours, which is why it is piped straight into &lt;code&gt;docker login&lt;/code&gt; rather than saved anywhere. And &lt;code&gt;--password-stdin&lt;/code&gt; keeps that token out of your shell history and out of the process list, where &lt;code&gt;--password&lt;/code&gt; on the command line would put it in both.&lt;/p&gt;

&lt;p&gt;One last thing worth saying out loud, because it causes real problems later: &lt;code&gt;latest&lt;/code&gt; is not a pointer that updates itself. It is a tag string like any other, and it moves only because you re-tag and re-push. Building a deployment on &lt;code&gt;latest&lt;/code&gt; means you cannot reliably say what is running. Pin a digest or a real version tag for anything that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Precision about what and where
&lt;/h2&gt;

&lt;p&gt;Cherry-pick is the answer to "which change, exactly." The image tag is the answer to "which registry, exactly." Both tasks punished vagueness in the same way, by doing something plausible with the wrong target rather than refusing outright.&lt;/p&gt;

&lt;p&gt;So here is the Day 28 question. When you move something — a change, an artifact, a config — is the destination stated somewhere you can read, or is it just implied by whatever the default happens to be?&lt;/p&gt;

&lt;p&gt;Day 28 down. Seventy-two to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>git</category>
      <category>aws</category>
      <category>docker</category>
    </item>
    <item>
      <title>Day 27: Undo Isn't a Switch, and Neither Is Public</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sat, 15 Aug 2026 07:46:34 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-27-undo-isnt-a-switch-and-neither-is-public-1b7l</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-27-undo-isnt-a-switch-and-neither-is-public-1b7l</guid>
      <description>&lt;p&gt;Two of the most common requests in this job are "undo that" and "make it public." Both sound like a single action. Neither one is. Day 27 was the day both of those turned out to be several things wearing one word.&lt;/p&gt;

&lt;p&gt;One Git task, one AWS task. Revert a commit that has already been pushed, then configure a VPC so an EC2 instance inside it can actually reach the internet. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revert: the undo that leaves a record
&lt;/h2&gt;

&lt;p&gt;Git gives you two ways to undo a commit, and picking the wrong one is how teams end up in a bad afternoon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /usr/src/kodekloudrepos/&amp;lt;repo&amp;gt;

git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
git show &amp;lt;commit-id&amp;gt;            &lt;span class="c"&gt;# read it before you undo it&lt;/span&gt;

git revert &amp;lt;commit-id&amp;gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
git push origin master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git revert&lt;/code&gt; does not delete anything. It writes a &lt;strong&gt;new&lt;/strong&gt; commit whose content is the exact inverse of the one you targeted. The original stays in the history, followed by a commit that undoes it. Two entries where you might have expected zero.&lt;/p&gt;

&lt;p&gt;That feels wrong at first, and it is the entire point. Compare it to &lt;code&gt;git reset&lt;/code&gt;, which moves the branch pointer backwards and rewrites what happened. Reset is genuinely useful and a &lt;strong&gt;local-only&lt;/strong&gt; tool. The rule underneath both:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never rewrite history you have already pushed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once a commit is on the remote, other people may have pulled it. Reset it away, force-push, and their next pull is a reconciliation problem they did not ask for. Revert is the version that is safe on shared history, because it only ever adds.&lt;/p&gt;

&lt;p&gt;The history reads a little strangely afterwards — "did a thing", then "undid a thing" — and that honesty is the feature. Hiding the mistake is what the dangerous option does.&lt;/p&gt;

&lt;p&gt;Two things that catch people. Reverting a &lt;strong&gt;merge commit&lt;/strong&gt; fails unless you specify which parent is the mainline with &lt;code&gt;-m 1&lt;/code&gt;. The error message reads like a bug the first time you hit it. And revert is itself revertible: revert the revert and you have reapplied the original change, with both moves on the record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Public subnets: there is no public checkbox
&lt;/h2&gt;

&lt;p&gt;The AWS task was making a VPC subnet genuinely reachable, and the first useful thing to internalise is that "public subnet" is not a setting. Nothing in the console is labelled public. A subnet is public if, and only if, its route table has a &lt;code&gt;0.0.0.0/0&lt;/code&gt; route pointing to an Internet Gateway. That is the whole definition.&lt;/p&gt;

&lt;p&gt;Which means it takes four separate pieces of wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Create the gateway&lt;/span&gt;
&lt;span class="nv"&gt;IGW_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 create-internet-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'InternetGateway.InternetGatewayId'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# 2. ATTACH it — an unattached IGW does nothing and says nothing&lt;/span&gt;
aws ec2 attach-internet-gateway &lt;span class="nt"&gt;--internet-gateway-id&lt;/span&gt; &lt;span class="nv"&gt;$IGW_ID&lt;/span&gt; &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; &lt;span class="nv"&gt;$VPC_ID&lt;/span&gt;

&lt;span class="c"&gt;# 3. Route table with a default route to the gateway&lt;/span&gt;
aws ec2 create-route &lt;span class="nt"&gt;--route-table-id&lt;/span&gt; &lt;span class="nv"&gt;$RTB_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--destination-cidr-block&lt;/span&gt; 0.0.0.0/0 &lt;span class="nt"&gt;--gateway-id&lt;/span&gt; &lt;span class="nv"&gt;$IGW_ID&lt;/span&gt;

&lt;span class="c"&gt;# 4. Associate that route table with the subnet&lt;/span&gt;
aws ec2 associate-route-table &lt;span class="nt"&gt;--route-table-id&lt;/span&gt; &lt;span class="nv"&gt;$RTB_ID&lt;/span&gt; &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &lt;span class="nv"&gt;$SUBNET_ID&lt;/span&gt;

&lt;span class="c"&gt;# and instances need an actual public address&lt;/span&gt;
aws ec2 modify-subnet-attribute &lt;span class="nt"&gt;--subnet-id&lt;/span&gt; &lt;span class="nv"&gt;$SUBNET_ID&lt;/span&gt; &lt;span class="nt"&gt;--map-public-ip-on-launch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every VPC comes with a &lt;strong&gt;main route table&lt;/strong&gt; that has only the local route and no path to the internet. A subnet you have not explicitly associated falls back to it, which is exactly why a freshly created subnet is silent.&lt;/p&gt;

&lt;p&gt;Here is the part that makes this genuinely awkward to debug: miss any one of those steps and the symptom is identical. No error, no warning, just an instance that cannot reach anything. Creating an Internet Gateway and forgetting to attach it produces no complaint at all. Neither does a perfect route table that was never associated with your subnet.&lt;/p&gt;

&lt;p&gt;So the only honest verification is from inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh ec2-user@&amp;lt;public-ip&amp;gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://aws.amazon.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything before that is configuration you believe is correct.&lt;/p&gt;

&lt;p&gt;Worth knowing the mirror image: a private subnet that needs outbound-only access uses the same shape with a NAT Gateway as the route target instead of an IGW. Same four steps, different destination.&lt;/p&gt;

&lt;h2&gt;
  
  
  One word, several moving parts
&lt;/h2&gt;

&lt;p&gt;"Undo it" is either an additive commit or a rewrite of shared history, and the difference decides whether your teammates have a good morning. "Make it public" is four separate resources that all have to agree, none of which is named public. In both cases, the word is a summary, and the summary is what hides the work.&lt;/p&gt;

&lt;p&gt;So here is the Day 27 question. When someone asks you for a one-word change, do you know how many things that word is actually standing in for?&lt;/p&gt;

&lt;p&gt;Day 27 down. Seventy-three to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>git</category>
      <category>aws</category>
      <category>vpc</category>
    </item>
    <item>
      <title>Day 26: Origin Is Just a Nickname, and Localhost Tells the Truth</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Wed, 12 Aug 2026 20:54:14 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-26-origin-is-just-a-nickname-and-localhost-tells-the-1il1</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-26-origin-is-just-a-nickname-and-localhost-tells-the-1il1</guid>
      <description>&lt;p&gt;A lot of confusion in this field comes from taking a name at face value. &lt;code&gt;origin&lt;/code&gt; sounds like it means something. A server that "isn't working" sounds like one problem. Day 26 was two tasks where the useful move was to stop trusting the label and go look at the thing itself.&lt;/p&gt;

&lt;p&gt;One Git task, one AWS task. Manage the remotes on a repository, then configure an EC2 instance as a web server with Nginx. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remotes: a nickname for a URL, and nothing else
&lt;/h2&gt;

&lt;p&gt;A remote is not a connection. It is not a sync relationship. It is a &lt;strong&gt;name stored in a config file that maps to a URL&lt;/strong&gt;. That is genuinely all it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote                      &lt;span class="c"&gt;# names only&lt;/span&gt;
git remote &lt;span class="nt"&gt;-v&lt;/span&gt;                   &lt;span class="c"&gt;# names plus URLs, fetch and push separately&lt;/span&gt;
git remote show origin          &lt;span class="c"&gt;# full detail, including tracked branches&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git remote add upstream &amp;lt;url&amp;gt;&lt;/code&gt; writes a few lines into &lt;code&gt;.git/config&lt;/code&gt;. It does not contact the server, verify the URL, or download anything. You can add a remote pointing at a URL that does not exist, and Git will let you, right up until you try to use it.&lt;/p&gt;

&lt;p&gt;Which means &lt;code&gt;origin&lt;/code&gt; is not special. It is not a keyword. It is simply the name &lt;code&gt;git clone&lt;/code&gt; happens to assign automatically, and you can rename it, retarget it, or work without one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add upstream http://git.example.com/&amp;lt;original-owner&amp;gt;/&amp;lt;repo&amp;gt;.git
git remote rename origin old-origin
git remote set-url origin git@github.com:user/repo.git
git remote remove upstream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;set-url&lt;/code&gt; is the one to reach for when a repository moves or you are switching from HTTPS to SSH. Removing and re-adding gets you to the same place but throws away the tracking configuration on any branch that pointed at it, which you then get to set up again for no reason.&lt;/p&gt;

&lt;p&gt;This is the other half of Day 23's fork. The convention on a fork is &lt;code&gt;origin&lt;/code&gt; for your copy, the one you can push to, and &lt;code&gt;upstream&lt;/code&gt; for the original, which you can usually only read. Nothing enforces those names. Everybody uses them anyway, and that shared habit is worth more than any rule.&lt;/p&gt;

&lt;p&gt;One distinction that saves real confusion: &lt;code&gt;git fetch&lt;/code&gt; downloads and stops, &lt;code&gt;git pull&lt;/code&gt; is fetch plus an immediate merge into your current branch. Fetching first means you get to look at what arrived before it touches your work. And remote-tracking branches like &lt;code&gt;origin/master&lt;/code&gt; are read-only local snapshots of where that branch stood &lt;strong&gt;the last time you fetched&lt;/strong&gt;. They are not live. When &lt;code&gt;origin/master&lt;/code&gt; looks stale, it usually is, because you have not fetched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nginx on EC2: ask localhost before you blame the network
&lt;/h2&gt;

&lt;p&gt;Installing Nginx is three commands, and none of them is the interesting part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx              &lt;span class="c"&gt;# Amazon Linux 2023, RHEL 9&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; nginx
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;Welcome to xFusion&amp;lt;/h1&amp;gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /usr/share/nginx/html/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;enable --now&lt;/code&gt; is &lt;code&gt;enable&lt;/code&gt; plus &lt;code&gt;start&lt;/code&gt; together. Use only &lt;code&gt;start&lt;/code&gt;, and you have a web server that quietly disappears on the next reboot, which is a miserable thing to discover weeks later.&lt;/p&gt;

&lt;p&gt;Then comes the command that is worth more than the rest of the task combined:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Run this on the instance itself, before you touch anything on the AWS side. It splits the problem cleanly in two. If localhost answers and your public IP does not, Nginx is completely fine, and your problem is networking. If localhost does not answer, do not go near a security group, because the web server itself is broken and no firewall change will help.&lt;/p&gt;

&lt;p&gt;Nine times out of ten localhost answers, and it is the same thing that bit me back on Day 22 with SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$SG_ID&lt;/span&gt; &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="nt"&gt;--cidr&lt;/span&gt; 0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fresh security group allows no inbound traffic at all. Port 80 has to be opened deliberately, exactly like port 22 did. Same lesson, different port, and it will keep being the same lesson.&lt;/p&gt;

&lt;p&gt;Two other things worth carrying forward. The document root differs by distribution — &lt;code&gt;/usr/share/nginx/html&lt;/code&gt; on the RHEL family, &lt;code&gt;/var/www/html&lt;/code&gt; on Debian and Ubuntu — which is how people end up carefully editing a file that nothing is serving. And never reload a config you have not validated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;                          &lt;span class="c"&gt;# validate&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx            &lt;span class="c"&gt;# apply without dropping connections&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An error caught by &lt;code&gt;nginx -t&lt;/code&gt; costs nothing. The same error applied to a live server takes the site down.&lt;/p&gt;

&lt;p&gt;Worth noting where this lands: this is exactly the kind of instance that becomes a target behind the ALB from Day 24. The health check needs a 200 from whatever path it probes, so what you serve here decides whether the load balancer will send it traffic at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the thing, not the label
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;origin&lt;/code&gt; tells you nothing about where your code goes; &lt;code&gt;git remote -v&lt;/code&gt; tells you. "The server is down" tells you nothing about where the failure is; &lt;code&gt;curl localhost&lt;/code&gt; tells you. Both tasks came down to one cheap command that replaces an assumption with a fact, and both of those commands take about two seconds.&lt;/p&gt;

&lt;p&gt;So here is the Day 26 question. Next time something is not working, what is the cheapest command that would tell you which half of the problem you are actually in?&lt;/p&gt;

&lt;p&gt;Day 26 down. Seventy-four to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>git</category>
      <category>aws</category>
      <category>nginx</category>
    </item>
    <item>
      <title>Day 25: Git Won't Guess, and an Unconfirmed Alarm Never Rings</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Mon, 10 Aug 2026 20:29:57 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-25-git-wont-guess-and-an-unconfirmed-alarm-never-rings-2e3b</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-25-git-wont-guess-and-an-unconfirmed-alarm-never-rings-2e3b</guid>
      <description>&lt;p&gt;Quarter of the way in, and Day 25 landed on a theme I did not expect: the last step of both tasks can only be completed by a person. Not a flag, not a retry, not a smarter command. A human has to decide something, and until they do, the system waits.&lt;/p&gt;

&lt;p&gt;One Git task, one AWS task. Merge a feature branch back into master and deal with a conflict, then launch an EC2 instance and put a CloudWatch alarm on it. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merging: two kinds, and Git picks for you
&lt;/h2&gt;

&lt;p&gt;The first thing to get straight is direction. A merge runs &lt;strong&gt;from&lt;/strong&gt; the branch you are merging &lt;strong&gt;into&lt;/strong&gt;. You stand on master and pull the feature branch towards you, not the other way round.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch master
git pull                        &lt;span class="c"&gt;# make sure master is current first&lt;/span&gt;
git merge feature-x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens next depends on something you did not choose. If master has not moved since the branch was created, Git performs a &lt;strong&gt;fast-forward&lt;/strong&gt;: it simply slides the master pointer up to the tip of your branch. No merge commit exists. The history looks like the branch never happened.&lt;/p&gt;

&lt;p&gt;If both branches have new commits, Git does a &lt;strong&gt;three-way merge&lt;/strong&gt; and creates a merge commit with two parents, recording that two lines of work came back together.&lt;/p&gt;

&lt;p&gt;That difference matters more than it sounds, because a fast-forward erases the shape of what you did. If you want the record kept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge &lt;span class="nt"&gt;--no-ff&lt;/span&gt; feature-x
git log &lt;span class="nt"&gt;--graph&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--decorate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--no-ff&lt;/code&gt; forces a merge commit even when a fast-forward was available. It costs one commit and buys a permanent, readable record that a branch existed and when it landed. Plenty of teams require it for exactly that reason.&lt;/p&gt;

&lt;p&gt;And then, sooner or later, this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;CONFLICT (content): Merge conflict in app.py
&lt;/span&gt;&lt;span class="gp"&gt;Automatic merge failed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;fix conflicts and &lt;span class="k"&gt;then &lt;/span&gt;commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A conflict is not an error, and it is not Git failing. It is Git refusing to guess. Two branches changed the same lines, and there is no correct answer available from the data alone, so it stops and hands the decision to you. That is the right behaviour, and it is worth reframing it that way, because the panic response is what causes damage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status                      &lt;span class="c"&gt;# exactly which files are stuck&lt;/span&gt;

&lt;span class="c"&gt;# Open each one, find the markers, decide what the file should say:&lt;/span&gt;
&lt;span class="c"&gt;#   &amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD           the version already on master&lt;/span&gt;
&lt;span class="c"&gt;#   =======&lt;/span&gt;
&lt;span class="c"&gt;#   &amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature-x      the version from your branch&lt;/span&gt;

git add app.py                  &lt;span class="c"&gt;# staging IS the resolution signal&lt;/span&gt;
git commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Editing the file is not enough. &lt;code&gt;git add&lt;/code&gt; is how you tell Git you have decided. Until the file is staged, the merge is still in progress no matter how clean the file looks.&lt;/p&gt;

&lt;p&gt;And the safety valve nobody mentions until you are already sweating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git merge &lt;span class="nt"&gt;--abort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That puts everything back exactly as it was. Knowing it exists is the difference between a conflict being a problem and a conflict being a nuisance.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alarm: a threshold, a duration, and a link somebody has to click
&lt;/h2&gt;

&lt;p&gt;The AWS side was launching an instance and alarming on its CPU. The alarm itself is one command, but two of its arguments carry all the meaning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudwatch put-metric-alarm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alarm-name&lt;/span&gt; nautilus-high-cpu &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; AWS/EC2 &lt;span class="nt"&gt;--metric-name&lt;/span&gt; CPUUtilization &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="s2"&gt;"Name=InstanceId,Value=&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statistic&lt;/span&gt; Average &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--period&lt;/span&gt; 300 &lt;span class="nt"&gt;--evaluation-periods&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--threshold&lt;/span&gt; 70 &lt;span class="nt"&gt;--comparison-operator&lt;/span&gt; GreaterThanThreshold &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alarm-actions&lt;/span&gt; &lt;span class="nv"&gt;$TOPIC_ARN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--period&lt;/code&gt; is the length of one measurement window in seconds. &lt;code&gt;--evaluation-periods&lt;/code&gt; is how many consecutive windows have to breach before the alarm fires. Together, 300 and 2 mean CPU must stay above 70% for a full ten minutes. That is not pedantry, it is the difference between an alarm that tells you something and an alarm that screams every time a cron job runs and gets muted within a week.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--dimensions&lt;/code&gt; is what scopes the metric to one instance. Leave it off, and you get the aggregate across everything reporting that metric, which is rarely what anyone means.&lt;/p&gt;

&lt;p&gt;The alarm notifies through an SNS topic, and an SNS email subscription does nothing at all until the recipient clicks a confirmation link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sns subscribe &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &lt;span class="nv"&gt;$TOPIC_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; email &lt;span class="nt"&gt;--notification-endpoint&lt;/span&gt; ops@example.com

aws sns list-subscriptions-by-topic &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &lt;span class="nv"&gt;$TOPIC_ARN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that comes back &lt;code&gt;PendingConfirmation&lt;/code&gt;, your monitoring is decorative. The alarm will transition to &lt;code&gt;ALARM&lt;/code&gt; exactly as designed, publish to the topic exactly as designed, and reach nobody. Every command succeeded. Nothing works.&lt;/p&gt;

&lt;p&gt;Two more things worth knowing. A new alarm sits in &lt;code&gt;INSUFFICIENT_DATA&lt;/code&gt; until enough data points arrive, which is expected rather than broken. And memory and disk usage are not default EC2 metrics at all, because the hypervisor cannot see inside the operating system. If you want those, you install the CloudWatch agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The step only a person can take
&lt;/h2&gt;

&lt;p&gt;Both tasks end the same way. Git stages a conflict and waits for a human to say what the file should be. CloudWatch wires up a notification and waits for a human to confirm they want it. In both cases, everything upstream reports success, and the whole thing is inert until somebody does the last small thing.&lt;/p&gt;

&lt;p&gt;So here is the Day 25 question. How much of your setup currently reports green while quietly waiting on a step nobody has taken?&lt;/p&gt;

&lt;p&gt;Day 25 down. Seventy-five to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>git</category>
      <category>aws</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Egusi Soup. One Bowl, One Checkbox, Zero JavaScript</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sat, 08 Aug 2026 18:18:10 +0000</pubDate>
      <link>https://dev.to/ndcodes/egusi-soup-one-bowl-one-checkbox-zero-javascript-1mei</link>
      <guid>https://dev.to/ndcodes/egusi-soup-one-bowl-one-checkbox-zero-javascript-1mei</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, CSS Art&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;Egusi soup with pounded yam. Jollof gets the headlines, but egusi is the quiet one that actually holds Nigerian homes together. Melon seeds, ugu, palm oil, and a dome of pounded yam you eat with your hands.&lt;/p&gt;

&lt;p&gt;I already sent this challenge a love letter to jollof for the Perfect Landing prompt. This is the companion piece, and it targets a different audience: not a flat poster, but a photograph with real depth. The entire table sits in a single CSS perspective plane, so the bowl is genuinely a bowl. You look down into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/editor/ND-codes/embed/019fbe61-4d9a-774b-a87f-61ace67b0cc2?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;br&gt;
A morsel of pounded yam is resting on top of the dome. Press "Dip the yam" and watch it lift off, cross the table, drop into the soup and come back stained. No JavaScript anywhere near it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;The rule I set myself: zero JavaScript. The one interactive moment runs on a checkbox and a sibling selector. The checkbox stays keyboard focusable, the label carries a visible focus ring, and if you've asked your system for reduced motion, the morsel skips the flight and just shows up stained.&lt;/p&gt;

&lt;p&gt;The whole scene is sized in container query units, so it scales as one object from a phone to a desktop without a single media query for layout. Some of the tricks I'm proud of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The table is one plane with &lt;code&gt;transform-style: preserve-3d&lt;/code&gt; and a &lt;code&gt;rotateX&lt;/code&gt;, so everything standing on it uses &lt;code&gt;translateZ&lt;/code&gt; to mean "up off the table"&lt;/li&gt;
&lt;li&gt;The bowl is six rings flaring up the Z axis. The top two are masked hollow, otherwise, they paint straight over the soup, and the whole thing reads as a solid disc. That bug is what taught me the technique&lt;/li&gt;
&lt;li&gt;The soup sits below the rim on the Z axis, so you see the inner wall and the shadow it throws across the curds&lt;/li&gt;
&lt;li&gt;The pounded yam is six contours stacked into a dome, each one a little brighter as it climbs toward the light&lt;/li&gt;
&lt;li&gt;The egusi curds are eleven stacked radial gradients, the palm oil pools at the rim through an inset shadow, and the oil sheen is a blurred specular layer sitting over everything wet&lt;/li&gt;
&lt;li&gt;The ugu leaves and the scattered raw egusi seeds are each one element cloned through a long box-shadow list&lt;/li&gt;
&lt;li&gt;The raffia mat is a repeating radial gradient woven through a repeating conic gradient&lt;/li&gt;
&lt;li&gt;The morsel is billboarded back toward the camera, because a sphere always projects as a circle, and its dip animates all three values of &lt;code&gt;translate&lt;/code&gt; so it genuinely lifts, crosses and drops in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two things I got wrong and had to fix. The bowl started as solid discs, which meant the soup was invisible until I masked the upper rings hollow. And I built the wood with plank seams running across the frame, which looked less like a table and more like a dark line drawn through my picture. It's one continuous surface now.&lt;/p&gt;

&lt;p&gt;Hardest part? Light. Flat colours are easy; making CSS gradients behave like palm oil under a window took more attempts than I will admit. The glossy layer over the soup ended up doing half the realism on its own.&lt;/p&gt;

&lt;p&gt;MIT licensed. The companion piece, &lt;a href="https://dev.to/ndcodes/jollof-a-love-letter-one-pot-zero-photographs-all-css-4b8i"&gt;Jollof, A Love Letter&lt;/a&gt;, is already up if you want the full table setting.&lt;/p&gt;

</description>
      <category>frontendchallenge</category>
      <category>devchallenge</category>
      <category>css</category>
    </item>
    <item>
      <title>Day 24: A Branch Is Just a Pointer, and a Load Balancer Is Three Things</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sat, 08 Aug 2026 17:56:17 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-24-a-branch-is-just-a-pointer-and-a-load-balancer-is-2j42</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-24-a-branch-is-just-a-pointer-and-a-load-balancer-is-2j42</guid>
      <description>&lt;p&gt;Almost everything useful in infrastructure is a layer of indirection. Something points at something else, and the pointing is what buys you the flexibility. Day 24 was two versions of that idea: a Git branch pointing at a commit, and a load balancer pointing at a pool of servers.&lt;/p&gt;

&lt;p&gt;One Git task, one AWS task. Create a branch and publish it with tracking, then put an Application Load Balancer in front of two EC2 instances. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branches: 41 bytes, not a copy
&lt;/h2&gt;

&lt;p&gt;The mental model most people arrive with is that a branch is a copy of the codebase. It is not, and once that clicks, a lot of Git stops feeling expensive.&lt;/p&gt;

&lt;p&gt;A branch is a file containing one commit hash. That is the whole thing. Creating a branch writes forty-odd bytes to disk, which is why it is instant on a repository of any size, and why branching freely is normal practice rather than something you ration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch                      &lt;span class="c"&gt;# local branches, * marks the current one&lt;/span&gt;
git branch &lt;span class="nt"&gt;-a&lt;/span&gt;                   &lt;span class="c"&gt;# include remote-tracking branches&lt;/span&gt;
git branch &lt;span class="nt"&gt;-v&lt;/span&gt;                   &lt;span class="c"&gt;# show the last commit on each&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating one has a catch that gets everybody once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Creates the branch — and leaves you exactly where you were&lt;/span&gt;
git branch feature-x

&lt;span class="c"&gt;# Creates AND switches&lt;/span&gt;
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature-x       &lt;span class="c"&gt;# classic&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature-x         &lt;span class="c"&gt;# modern, Git 2.23 and later&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git branch &amp;lt;name&amp;gt;&lt;/code&gt; creates without switching. You make the branch, carry on working, and every commit lands on the old branch. Nothing warns you. &lt;code&gt;git branch --show-current&lt;/code&gt; before you commit is a cheap habit that catches it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;switch&lt;/code&gt; and &lt;code&gt;restore&lt;/code&gt; commands were split out of &lt;code&gt;checkout&lt;/code&gt; in Git 2.23 for a good reason: &lt;code&gt;checkout&lt;/code&gt; did two completely unrelated jobs, moving between branches and discarding local file changes. One typo could destroy work. The newer commands each do one thing, and there is no reason to keep reaching for the old one.&lt;/p&gt;

&lt;p&gt;Then publish it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin feature-x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-u&lt;/code&gt; sets the upstream tracking reference. Skip it and every later &lt;code&gt;git push&lt;/code&gt; on that branch stops to tell you it does not know where to go.&lt;/p&gt;

&lt;p&gt;One last thing worth knowing before you need it: &lt;code&gt;git branch -d&lt;/code&gt; refuses to delete a branch with unmerged commits, while &lt;code&gt;-D&lt;/code&gt; forces it. That refusal is a safety net. If &lt;code&gt;-d&lt;/code&gt; complains, go and look at why before reaching for the capital letter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ALB: three parts, and all three have to exist
&lt;/h2&gt;

&lt;p&gt;An Application Load Balancer is not one resource. It is three, and the most common first failure is building two of them and wondering why nothing responds.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;target group&lt;/strong&gt; is the pool of backends plus the health check that decides who is in it. The &lt;strong&gt;load balancer&lt;/strong&gt; is the thing that accepts traffic. The &lt;strong&gt;listener&lt;/strong&gt; is the rule that connects them, saying which port and protocol to accept and what to do with what arrives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TG_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws elbv2 create-target-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; nautilus-tg &lt;span class="nt"&gt;--protocol&lt;/span&gt; HTTP &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vpc-id&lt;/span&gt; vpc-xxxxxxxx &lt;span class="nt"&gt;--target-type&lt;/span&gt; instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--health-check-path&lt;/span&gt; /health &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'TargetGroups[0].TargetGroupArn'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws elbv2 register-targets &lt;span class="nt"&gt;--target-group-arn&lt;/span&gt; &lt;span class="nv"&gt;$TG_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="nv"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;i-1111111111111111 &lt;span class="nv"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;i-2222222222222222

&lt;span class="nv"&gt;LB_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws elbv2 create-load-balancer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; nautilus-alb &lt;span class="nt"&gt;--type&lt;/span&gt; application &lt;span class="nt"&gt;--scheme&lt;/span&gt; internet-facing &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnets&lt;/span&gt; subnet-aaaaaaaa subnet-bbbbbbbb &lt;span class="nt"&gt;--security-groups&lt;/span&gt; sg-xxxxxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'LoadBalancers[0].LoadBalancerArn'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws elbv2 create-listener &lt;span class="nt"&gt;--load-balancer-arn&lt;/span&gt; &lt;span class="nv"&gt;$LB_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; HTTP &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--default-actions&lt;/span&gt; &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;forward,TargetGroupArn&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TG_ARN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A load balancer with no listener accepts nothing. It exists, it has a DNS name, and it does absolutely nothing with your traffic.&lt;/p&gt;

&lt;p&gt;The requirement that catches people on the first attempt is in the &lt;code&gt;--subnets&lt;/code&gt; line: an ALB needs &lt;strong&gt;at least two subnets in two different availability zones&lt;/strong&gt;. AWS enforces it, and it is not a suggestion. The load balancer is meant to survive an AZ failure, so it refuses to be built somewhere it could not.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws elbv2 describe-target-health &lt;span class="nt"&gt;--target-group-arn&lt;/span&gt; &lt;span class="nv"&gt;$TG_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'TargetHealthDescriptions[*].{Target:Target.Id,State:TargetHealth.State,Reason:TargetHealth.Reason}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If every target is &lt;code&gt;unhealthy&lt;/code&gt;, the cause is almost always security groups, and specifically the second one nobody thinks about. The ALB's security group has to allow inbound from the internet. The &lt;strong&gt;instances'&lt;/strong&gt; security group has to allow inbound from the ALB's security group on the target port. Get the first and miss the second and you have a working load balancer pointing at servers it cannot reach. The reason code tells you which: &lt;code&gt;Target.Timeout&lt;/code&gt; means the traffic is being blocked, &lt;code&gt;Target.ResponseCodeMismatch&lt;/code&gt; means it got through and the health check path returned the wrong status.&lt;/p&gt;

&lt;p&gt;One thing to internalise: an ALB has a DNS name, never a static IP. Its addresses change underneath you. Point DNS at the name with a CNAME or a Route 53 alias and never write one of those IPs down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Both are pointers
&lt;/h2&gt;

&lt;p&gt;A branch points at a commit, so moving work around costs nothing. A target group points at instances, so replacing a server costs nothing either. In both cases, the indirection is the feature, and in both cases the thing being pointed at can change without anything upstream noticing.&lt;/p&gt;

&lt;p&gt;So here is the Day 24 question. When something in your setup is expensive to change, is it actually expensive, or is it just missing the layer of indirection that would make it cheap?&lt;/p&gt;

&lt;p&gt;Day 24 down. Seventy-six to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>git</category>
      <category>aws</category>
      <category>networking</category>
    </item>
    <item>
      <title>Day 23: A Fork Is Not a Clone, and Sync Is Not a Copy</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Thu, 06 Aug 2026 05:36:58 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-23-a-fork-is-not-a-clone-and-sync-is-not-a-copy-731</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-23-a-fork-is-not-a-clone-and-sync-is-not-a-copy-731</guid>
      <description>&lt;p&gt;Copying something is easy. Copying it in a way that still knows where it came from is the part worth learning. Day 23 was two copies of exactly that kind, one on a Git server and one between two S3 buckets, and both have a smarter option sitting next to the obvious one.&lt;/p&gt;

&lt;p&gt;One Git task, one AWS task. Fork a repository and connect it back to the original, then migrate the contents of one S3 bucket into another. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forking: the Git operation that isn't a Git command
&lt;/h2&gt;

&lt;p&gt;Here is what surprised me. There is no &lt;code&gt;git fork&lt;/code&gt;. Type it, and Git tells you it is not a command, because forking does not happen on your machine at all. It is a feature of the Git server, Gitea or GitHub or GitLab, which makes a full copy of a repository under a different owner and quietly records where that copy came from.&lt;/p&gt;

&lt;p&gt;So the fork happens in a browser. What happens afterwards is where the Git work starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone YOUR fork, not the original&lt;/span&gt;
git clone http://git.example.com/&amp;lt;your-user&amp;gt;/&amp;lt;repo&amp;gt;.git
&lt;span class="nb"&gt;cd&lt;/span&gt; &amp;lt;repo&amp;gt;

&lt;span class="c"&gt;# origin points at your copy&lt;/span&gt;
git remote &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point you have a copy that has already forgotten its parent, as far as your local repo is concerned. One remote, pointing at you. The fix is to add the original back by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add the original repository as a second remote&lt;/span&gt;
git remote add upstream http://git.example.com/&amp;lt;original-owner&amp;gt;/&amp;lt;repo&amp;gt;.git

&lt;span class="c"&gt;# Pull new work from the original into your fork&lt;/span&gt;
git fetch upstream
git merge upstream/master

git push origin master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;origin&lt;/code&gt; and &lt;code&gt;upstream&lt;/code&gt; are just names, not Git keywords, but the convention is worth following because everyone uses it: &lt;code&gt;origin&lt;/code&gt; is your copy, the one you can push to, and &lt;code&gt;upstream&lt;/code&gt; is the original, which you can usually only read from.&lt;/p&gt;

&lt;p&gt;Three words get muddled constantly here, so it is worth being blunt about them. A &lt;strong&gt;fork&lt;/strong&gt; is a copy on the server under a new owner. A &lt;strong&gt;clone&lt;/strong&gt; is a copy on your local disk. A &lt;strong&gt;branch&lt;/strong&gt; is a pointer inside one repository. Only the fork changes who owns the code, and that is exactly why it exists: you cannot push to a repository you do not own, so you fork it, push to your own copy, and ask the owner to pull your work in. That is the entire pull request model on every public project you have ever contributed to.&lt;/p&gt;

&lt;p&gt;The trap is that a fork is a snapshot of one moment. It does not track the original. Leave it alone for a month, and it drifts behind, and you will find out when your pull request no longer applies cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 migration: sync knows what it already did
&lt;/h2&gt;

&lt;p&gt;The AWS task was moving the contents of one bucket into another. The obvious command works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;s3://source-bucket s3://destination-bucket &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The better one is barely different to type and behaves nothing alike:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rehearse first — transfers nothing, prints everything it would do&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;sync &lt;/span&gt;s3://source-bucket s3://destination-bucket &lt;span class="nt"&gt;--dryrun&lt;/span&gt;

aws s3 &lt;span class="nb"&gt;sync &lt;/span&gt;s3://source-bucket s3://destination-bucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cp --recursive&lt;/code&gt; copies everything, every time, whether it changed or not. &lt;code&gt;sync&lt;/code&gt; compares each object's size and last-modified time and transfers only the differences. On a small bucket, you will not notice. On a large one, that is the difference between re-running a failed transfer in seconds and starting the whole thing again from zero.&lt;/p&gt;

&lt;p&gt;Two details make this better than it first looks. The transfer is &lt;strong&gt;server-side&lt;/strong&gt;: S3 copies bucket to bucket inside AWS, so the objects never travel down to your terminal and back up again. Your laptop only issues API calls, which means a slow connection does not slow the migration. And because &lt;code&gt;sync&lt;/code&gt; is incremental, it is safe to run repeatedly, which is what makes it usable as a scheduled job rather than a one-off.&lt;/p&gt;

&lt;p&gt;Two flags deserve respect. &lt;code&gt;--delete&lt;/code&gt; turns a copy into a mirror, removing anything in the destination that has no counterpart in the source. And &lt;code&gt;aws s3 mv&lt;/code&gt; deletes from the source once each object lands, which without versioning enabled is not recoverable. Both are the right tool sometimes. Both should meet &lt;code&gt;--dryrun&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;One thing to note is that objects arrive in the destination bucket's default storage class unless you pass &lt;code&gt;--storage-class&lt;/code&gt;. If your source was sitting in something cheaper, a naive sync promotes the whole lot back to Standard, and you find out on the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Copies that remember
&lt;/h2&gt;

&lt;p&gt;Both tasks had a naive version that works and a better version that keeps context. Cloning a fork without adding &lt;code&gt;upstream&lt;/code&gt; gives you code with no way back to its source. Copying a bucket with &lt;code&gt;cp --recursive&lt;/code&gt; gives you data with no memory of what was already transferred. In both cases, the smarter option costs one extra line and saves you the moment where you realise you have lost the thread.&lt;/p&gt;

&lt;p&gt;So here is the Day 23 question. Would you rather make a copy that stands alone and hope you never need to reconcile it, or spend one extra command keeping the link to where it came from?&lt;/p&gt;

&lt;p&gt;Day 23 down. Seventy-seven to go&lt;/p&gt;

</description>
      <category>git</category>
      <category>aws</category>
      <category>linux</category>
      <category>devops</category>
    </item>
    <item>
      <title>Day 22: Cloning a Repo, and Why Your New EC2 Refuses Your SSH</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Mon, 03 Aug 2026 14:01:28 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-22-cloning-a-repo-and-why-your-new-ec2-refuses-your-ssh-4ooj</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-22-cloning-a-repo-and-why-your-new-ec2-refuses-your-ssh-4ooj</guid>
      <description>&lt;p&gt;You can do every step of launching a server correctly and still be locked out of it. The instance is up, the key is right, and SSH still times out, because a firewall rule you never set is quietly turning you away. Day 22 closed the first stretch of this challenge on exactly that lesson.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Clone a Git repository into a target directory, then launch an EC2 instance with SSH key access and fix the security group when the connection is refused. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git clone: two forms, one rule about the directory
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone into the current directory (must be empty)&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/target
git clone &amp;lt;repo-url&amp;gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Or clone into a new named subdirectory (must not exist yet)&lt;/span&gt;
git clone &amp;lt;repo-url&amp;gt; directory-name

git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two forms, and the difference is the directory. &lt;code&gt;git clone &amp;lt;url&amp;gt; .&lt;/code&gt; clones into the directory you are already in, which has to be empty. &lt;code&gt;git clone &amp;lt;url&amp;gt; name&lt;/code&gt; creates a new subdirectory of that name, which must not already exist. Mix them up, and Git refuses rather than making a mess, which is the polite kind of failure. &lt;code&gt;git log --oneline&lt;/code&gt; afterwards confirms the history actually came down.&lt;/p&gt;

&lt;h2&gt;
  
  
  EC2 with SSH: the launch is easy, the security group is the catch
&lt;/h2&gt;

&lt;p&gt;The AWS side had more to it. Generate a key, import it to AWS, launch an instance with it, then connect. The launch used two patterns worth calling out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Latest Amazon Linux 2 AMI, straight from Parameter Store (never stale)&lt;/span&gt;
&lt;span class="nv"&gt;AMI_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ssm get-parameter &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Parameter.Value"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Import your existing public key, then launch with it&lt;/span&gt;
aws ec2 import-key-pair &lt;span class="nt"&gt;--key-name&lt;/span&gt; id_rsa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--public-key-material&lt;/span&gt; fileb:///root/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two good habits here. Pulling the AMI ID from SSM Parameter Store means you always launch on the current Amazon Linux image instead of hardcoding one that goes stale, the exact problem I flagged back on Day 6. And &lt;code&gt;import-key-pair&lt;/code&gt; uses &lt;code&gt;fileb://&lt;/code&gt; rather than &lt;code&gt;file://&lt;/code&gt;, because a key is binary, so &lt;code&gt;fileb&lt;/code&gt; reads raw bytes where &lt;code&gt;file&lt;/code&gt; reads text.&lt;/p&gt;

&lt;p&gt;Then the launch succeeds, the instance is running, and SSH hangs. The instance is fine. The problem is that the security group, which is the instance's firewall, has no rule allowing inbound port 22, so your connection never gets through. This is the single most common reason a fresh EC2 rejects you, and the fix is one rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Your own public IP&lt;/span&gt;
&lt;span class="nv"&gt;MY_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://checkip.amazonaws.com&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Allow SSH from just your IP, not the whole internet&lt;/span&gt;
aws ec2 authorize-security-group-ingress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-id&lt;/span&gt; &lt;span class="nv"&gt;$SG_ID&lt;/span&gt; &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 22 &lt;span class="nt"&gt;--cidr&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MY_IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/32"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important detail is the &lt;code&gt;/32&lt;/code&gt;. Opening port 22 to &lt;code&gt;0.0.0.0/0&lt;/code&gt; would let the entire internet knock on your SSH door. Scoping it to your own IP with &lt;code&gt;/32&lt;/code&gt; means only your machine can reach it, least privilege applied to a firewall rule. The scripted connect also uses &lt;code&gt;ssh -o StrictHostKeyChecking=no&lt;/code&gt; to skip the host-key prompt. That is fine inside automation where nothing can answer a prompt, but know what it gives up: that check is your protection against a man-in-the-middle, so it is a convenience with a real tradeoff, not a free one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last mile is access
&lt;/h2&gt;

&lt;p&gt;Both tasks came down to the last mile, the small thing between "set up" and "actually reachable." The clone works or fails on one empty directory. The server is useless until one firewall rule lets you in. Everything upstream can be perfect, and the last rule is what decides whether you get to use any of it.&lt;/p&gt;

&lt;p&gt;That is Day 22; the pattern across all of them has been the same one: the command is the easy part, and the thing it quietly depends on is where the real learning sits.&lt;/p&gt;

&lt;p&gt;So here is the Day 22 question. Would you rather build everything and discover the access gap when you cannot get in, or check the last mile first, before you need it?&lt;/p&gt;

&lt;p&gt;Day 22 down. Seventy-eight to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>git</category>
      <category>ssh</category>
    </item>
    <item>
      <title>JOLLOF, A Love Letter. One Pot, Zero Photographs, All CSS</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sat, 01 Aug 2026 19:47:23 +0000</pubDate>
      <link>https://dev.to/ndcodes/jollof-a-love-letter-one-pot-zero-photographs-all-css-4b8i</link>
      <guid>https://dev.to/ndcodes/jollof-a-love-letter-one-pot-zero-photographs-all-css-4b8i</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, Perfect Landing&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;A love letter to Jollof rice, the dish I cook in Manchester when I need Lagos back for an hour.&lt;/p&gt;

&lt;p&gt;It's a single landing page that tells the story of one pot. Where jollof came from, what goes in it layer by layer, the Nigeria vs Ghana argument that will outlive us all, and the five steps to cook it yourself. There isn't a single photograph on the page. Every dish, the top-down pot, the steam, the cross-section, is drawn in CSS.&lt;/p&gt;

&lt;p&gt;Three things I cared about most:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Accessibility first. Semantic landmarks, a working skip link that moves focus, one open accordion pattern with proper aria wiring, a vote result announced through a live region, gold focus rings on everything, and a reduced-motion mode where nothing moves.&lt;/li&gt;
&lt;li&gt;Interaction that earns its place. Open a layer in "Anatomy of the Pot", and the pot cross-section lights up that layer. Vote in the Jollof Wars and the tally stays on your device, because this war needs no central server.&lt;/li&gt;
&lt;li&gt;A visual identity that isn't a food template. Flat editorial poster style, hard-edged shadows, two-tone display type split across the ground. If it looks like a restaurant theme from a template store, I failed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/editor/ND-codes/embed/019fbc2f-a6e7-71f0-a861-268977ff4e04?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;I started with a rule: no photographs. Food sites lean on photography because it's the easy win, and I wanted to see how far layout, type, and flat CSS shapes could carry appetite on their own. The pot became one big orange circle, the rice became hard-stop radial gradients, and the steam became three animated wisps that go still if you prefer reduced motion.&lt;/p&gt;

&lt;p&gt;The part I'm proud of is the anatomy section. The accordion drives the illustration, so the page explains the dish the way I'd explain it across a kitchen counter: layer by layer, base to smoke. The part that humbled me was the two-tone headline, which needs the text split to line up with the background split at every viewport width.&lt;/p&gt;

&lt;p&gt;And the personal bit, since this challenge is about comfort. I'm Nigerian, I live in Greater Manchester, and jollof is the one dish that resets me when the week has been too much. Building this page was the cheapest therapy I've had all year.&lt;/p&gt;

&lt;p&gt;Stack: hand-written HTML, CSS, and about 120 lines of vanilla JS. No frameworks, no images, no dependencies. Built with Claude as my pair, reviewed and argued over by me. MIT licensed.&lt;/p&gt;

&lt;p&gt;So, one question before you go: Naija or Ghana? That is, if you had tasted both. If not, why not give it a go? The vote button is waiting.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Day 21: A Bare Git Repo Is the Hub, and an Elastic IP Is the Anchor</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sat, 01 Aug 2026 18:47:56 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-21-a-bare-git-repo-is-the-hub-and-an-elastic-ip-is-the-21oj</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-21-a-bare-git-repo-is-the-hub-and-an-elastic-ip-is-the-21oj</guid>
      <description>&lt;p&gt;Every team's Git history lives on a repository nobody actually works in. It has no files you can open and edit, just the raw version data, and that emptiness is the entire point. Day 21 was building that central hub, and pairing it with the AWS equivalent of a fixed address, an Elastic IP that stays put.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Set up a bare Git repository on a storage server, then launch an EC2 instance and give it an Elastic IP. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bare Git repo: a hub with no working tree
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/media.git
&lt;span class="nb"&gt;sudo &lt;/span&gt;git init &lt;span class="nt"&gt;--bare&lt;/span&gt; /opt/media.git
&lt;span class="nb"&gt;ls&lt;/span&gt; /opt/media.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--bare&lt;/code&gt; flag is the whole idea. A normal Git repo has two parts, your working files and the hidden &lt;code&gt;.git&lt;/code&gt; folder tracking their history. A bare repo has only the second part, the history, with no working tree at all. That is why bare repos are named with a &lt;code&gt;.git&lt;/code&gt; suffix and why you never edit inside one directly.&lt;/p&gt;

&lt;p&gt;Why would you want a repo you cannot work in? Because it is the shared hub. Developers push to it and pull from it, but nobody commits inside it, so there is never a checked-out branch to collide with an incoming push. This is exactly what GitHub and GitLab host on their side, a bare repo you sync against. Running &lt;code&gt;git init --bare&lt;/code&gt; yourself is a good way to see there is no magic to a remote repo, it is just a repo with the working files left off. Keep the one distinction straight: &lt;code&gt;git init&lt;/code&gt; makes a normal repo with a working directory for local development, &lt;code&gt;git init --bare&lt;/code&gt; makes the central one everyone shares.&lt;/p&gt;

&lt;h2&gt;
  
  
  EC2 with an Elastic IP: launch, allocate, associate
&lt;/h2&gt;

&lt;p&gt;On AWS, the task was to launch an instance and give it a static public IP, scripted end to end. The nice part is how it strings the steps together with bash variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Launch, then capture the instance ID once it is running&lt;/span&gt;
aws ec2 run-instances &lt;span class="nt"&gt;--image-id&lt;/span&gt; ami-xxxx &lt;span class="nt"&gt;--instance-type&lt;/span&gt; t2.micro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=instance,Tags=[{Key=Name,Value=xfusion-ec2}]'&lt;/span&gt;

&lt;span class="nv"&gt;INSTANCE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=xfusion-ec2"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Reservations[*].Instances[*].InstanceId'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws ec2 &lt;span class="nb"&gt;wait &lt;/span&gt;instance-running &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;

&lt;span class="c"&gt;# Allocate an Elastic IP, capture its allocation ID, then associate it&lt;/span&gt;
&lt;span class="nv"&gt;ALLOCATION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ec2 allocate-address &lt;span class="nt"&gt;--domain&lt;/span&gt; vpc &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'AllocationId'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws ec2 associate-address &lt;span class="nt"&gt;--instance-id&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt; &lt;span class="nt"&gt;--allocation-id&lt;/span&gt; &lt;span class="nv"&gt;$ALLOCATION_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two habits worth stealing. Capturing each ID into a variable with &lt;code&gt;$(...)&lt;/code&gt; turns the whole thing into a script instead of a copy-paste relay, so you never paste the wrong &lt;code&gt;i-0abc&lt;/code&gt; halfway through. And &lt;code&gt;aws ec2 wait instance-running&lt;/code&gt; blocks until the instance is genuinely up before you try to associate the address, so you are not racing the API. One reminder from Day 10, because it applies the moment you allocate: every public IPv4 address is billed now, attached or not, since AWS changed that in early 2024. An Elastic IP you allocate and forget still costs you, so release the ones you are not using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the fixed point on purpose
&lt;/h2&gt;

&lt;p&gt;Both tasks were about creating something stable for others to depend on. The bare repo is the fixed point a whole team's history hangs off. An Elastic IP is the fixed address that a service retains across restarts. Neither is glamorous, and both are the kind of quiet infrastructure everything else assumes is simply there.&lt;/p&gt;

&lt;p&gt;So here is the Day 21 question. Would you rather build the stable centre deliberately and know exactly what depends on it, or let it emerge by accident and find out what breaks the day it moves?&lt;/p&gt;

&lt;p&gt;Day 21 down. Seventy-nine to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>git</category>
      <category>aws</category>
      <category>linux</category>
    </item>
    <item>
      <title>Day 20: Wiring Nginx to PHP Over a Socket, and Giving EC2 a Role Without Keys</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Thu, 30 Jul 2026 06:53:10 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-20-wiring-nginx-to-php-over-a-socket-and-giving-ec2-a-1fkm</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-20-wiring-nginx-to-php-over-a-socket-and-giving-ec2-a-1fkm</guid>
      <description>&lt;p&gt;The best way to give one component access to another is usually the one that leaves no secret lying around. A local socket instead of an open network port. A temporary role instead of a hardcoded key. Day 20 was two versions of that idea, Nginx talking to PHP over a Unix socket, and EC2 getting AWS permissions through a role.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Wire Nginx to PHP-FPM through a Unix socket, then create an IAM role that EC2 instances can assume. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nginx and PHP-FPM: connected by a socket, not a port
&lt;/h2&gt;

&lt;p&gt;Nginx serves static files itself, but it cannot run PHP. It hands PHP requests off to PHP-FPM, a separate process that does the actual execution. The question is how the two talk, and here the answer is a Unix socket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;\.php$&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;include&lt;/span&gt; &lt;span class="s"&gt;fastcgi_params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_pass&lt;/span&gt; &lt;span class="s"&gt;unix:/var/run/php-fpm/default.sock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# PHP-FPM over a Unix socket&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_index&lt;/span&gt; &lt;span class="s"&gt;index.php&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;fastcgi_param&lt;/span&gt; &lt;span class="s"&gt;SCRIPT_FILENAME&lt;/span&gt; &lt;span class="nv"&gt;$document_root$fastcgi_script_name&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 &lt;code&gt;location ~ \.php$&lt;/code&gt; regex sends only &lt;code&gt;.php&lt;/code&gt; requests to PHP-FPM; everything else Nginx serves statically. &lt;code&gt;fastcgi_pass&lt;/code&gt; points at a socket file, not an IP and port. You could connect them over TCP with &lt;code&gt;127.0.0.1:9000&lt;/code&gt; instead, but the Unix socket is the better local choice. It is a file on disk, so there is no network stack in the path and nothing listening on a port that could be reached from off the box. Less overhead, smaller surface.&lt;/p&gt;

&lt;p&gt;The trap is ownership. Nginx and PHP-FPM have to agree on who owns that socket file, or Nginx gets permission-denied when it tries to connect. So PHP-FPM is configured to run as the nginx user and create the socket with matching ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In /etc/php-fpm.d/www.conf&lt;/span&gt;
&lt;span class="c"&gt;# user = nginx&lt;/span&gt;
&lt;span class="c"&gt;# group = nginx&lt;/span&gt;
&lt;span class="c"&gt;# listen = /var/run/php-fpm/default.sock&lt;/span&gt;
&lt;span class="c"&gt;# listen.owner = nginx&lt;/span&gt;
&lt;span class="c"&gt;# listen.group = nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If PHP downloads as a file instead of running, or you get a 502, socket ownership is the first place to look.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM roles: permissions without a stored key
&lt;/h2&gt;

&lt;p&gt;On AWS, the task was an IAM role for EC2. A role is how you grant an instance permission to call AWS services without putting an access key on the box. The instance assumes the role and receives temporary, rotating credentials instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Trust policy: who is allowed to assume this role (the EC2 service)&lt;/span&gt;
aws iam create-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; EC2-Custom-Role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--assume-role-policy-document&lt;/span&gt; file://ec2-role-trust-policy.json

&lt;span class="c"&gt;# Permissions policy: what the role can do once assumed&lt;/span&gt;
aws iam attach-role-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; EC2-Custom-Role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; &amp;lt;policy-arn&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A role has two policies doing two different jobs. The trust policy says who can assume it, here &lt;code&gt;ec2.amazonaws.com&lt;/code&gt;. The permissions policy says what it can do once assumed. Two documents, two questions, do not mix them up.&lt;/p&gt;

&lt;p&gt;Here is the step the task leaves out, and the one that trips people moving from the console to the CLI. A role by itself cannot be attached to an EC2 instance. EC2 uses an instance profile, a container that wraps the role, and in the console AWS creates that wrapper for you invisibly. From the CLI you do it by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-instance-profile &lt;span class="nt"&gt;--instance-profile-name&lt;/span&gt; EC2-Custom-Profile
aws iam add-role-to-instance-profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-profile-name&lt;/span&gt; EC2-Custom-Profile &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-name&lt;/span&gt; EC2-Custom-Role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you attach the instance profile, not the role, to the instance. Miss this, and you have a perfectly good role that EC2 simply cannot use (per the AWS IAM docs).&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect without leaking
&lt;/h2&gt;

&lt;p&gt;Both tasks were about connecting two things the clean way. The Unix socket links Nginx and PHP without opening a port or leaving a network path exposed. The role links EC2 to AWS without a stored key that could leak. The lazy versions, a TCP port and a hardcoded credential, both work and both widen your attack surface for nothing in return.&lt;/p&gt;

&lt;p&gt;So here is the Day 20 question. Would you rather connect your components the convenient way and carry the exposure, or the clean way that leaves nothing lying around to steal?&lt;/p&gt;

&lt;p&gt;Day 20 down. Eighty to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>nginx</category>
    </item>
  </channel>
</rss>
