<?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%2F3a0c042d-7d46-4d4d-9a1e-32b1833b0bee.png</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>100 Days of DevOps and Cloud (AWS), 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>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 19: Apache Deploys, and a Policy Is Just a Document Until It's Attached</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Tue, 28 Jul 2026 19:32:11 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-19-apache-deploys-and-a-policy-is-just-a-document-until-3h9</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-19-apache-deploys-and-a-policy-is-just-a-document-until-3h9</guid>
      <description>&lt;p&gt;A policy sitting in your AWS account grants nothing to anyone. It is a document until you attach it to a person, and that attach step is the moment the permissions become real. Day 19 had that on the AWS side, and its Linux twin, application files that do nothing until they land in the right directory and the server reloads.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Deploy a web application with Apache httpd, moving the files across with SCP, then attach an IAM policy to a user. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apache: set the port, ship the files, reload
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; httpd
systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; httpd

&lt;span class="c"&gt;# Point Apache at the required port&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; listen /etc/httpd/conf/httpd.conf
vi /etc/httpd/conf/httpd.conf     &lt;span class="c"&gt;# change Listen 80 to the required port&lt;/span&gt;
systemctl reload httpd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache's listen port lives in &lt;code&gt;httpd.conf&lt;/code&gt; on the &lt;code&gt;Listen&lt;/code&gt; line. Running &lt;code&gt;grep&lt;/code&gt; with &lt;code&gt;-n&lt;/code&gt; gives you the line number so you change that one directive and leave the rest alone. Then reload rather than restart, so the new port applies without dropping live connections. Next, the files:&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;# From the jump server, copy the app files over&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;scp &lt;span class="nt"&gt;-r&lt;/span&gt; /home/thor/media user@stapp01:/tmp
&lt;span class="nb"&gt;sudo &lt;/span&gt;scp &lt;span class="nt"&gt;-r&lt;/span&gt; /home/thor/apps  user@stapp01:/tmp

&lt;span class="c"&gt;# On the app server, move them into the web root&lt;/span&gt;
&lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/media /var/www/html/
&lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/apps  /var/www/html/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The files arrive from a jump server over &lt;code&gt;scp&lt;/code&gt;, a two-hop pattern you see any time the app server has no direct internet access. They land in &lt;code&gt;/tmp&lt;/code&gt; first, then move into the web root at &lt;code&gt;/var/www/html&lt;/code&gt;. Nothing serves until the files are actually under the web root. Then confirm it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-4&lt;/span&gt; http://stapp01:&amp;lt;port&amp;gt;/media
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;curl -4&lt;/code&gt; forces IPv4, which saves you from odd IPv6 resolution behavior in a dual-stack setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM: attaching is what makes a policy active
&lt;/h2&gt;

&lt;p&gt;On AWS, the task was to attach an existing policy to a user. The policy already existed, the user already existed, and on their own they did nothing for each other. The attach is the 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;# Find the user and the policy&lt;/span&gt;
aws iam list-users
aws iam list-policies &lt;span class="nt"&gt;--no-only-attached&lt;/span&gt; &lt;span class="nt"&gt;--max-items&lt;/span&gt; 3

&lt;span class="c"&gt;# Attach the policy to the user&lt;/span&gt;
aws iam attach-user-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-name&lt;/span&gt; iam_userBob &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::123456789012:policy/my-policy

&lt;span class="c"&gt;# Verify it stuck&lt;/span&gt;
aws iam list-attached-user-policies &lt;span class="nt"&gt;--user-name&lt;/span&gt; iam_userBob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy ARN is the exact identifier, and attaching the wrong one is how you hand out access you never meant to, so you check the ARN before attaching and list the attached policies after. And a callback to Day 17: this is exactly the case groups solve. Attaching directly to Bob works, but if five people need the same access you are back to five attach commands and five things to unpick later. Attach the policy to a group and add the users to the group instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Created is not connected
&lt;/h2&gt;

&lt;p&gt;Both tasks share one quiet trap. Creating a thing and activating it are separate steps. The app files existed on the jump server and served nothing until they reached the web root. The policy existed in the account and granted nothing until it was attached. Half-finished work looks a lot like finished work, right up until you test it and nothing happens.&lt;/p&gt;

&lt;p&gt;So here is the Day 19 question. When you call something done, do you mean the pieces exist, or do you mean you watched it work end to end?&lt;/p&gt;

&lt;p&gt;Day 19 down. Eighty-one to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>iam</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 18: MariaDB's Secure Install, and Why 'Read-Only' Needs More Than ec2:Describe</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Mon, 27 Jul 2026 07:34:20 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-18-mariadbs-secure-install-and-why-read-only-needs-3h93</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-18-mariadbs-secure-install-and-why-read-only-needs-3h93</guid>
      <description>&lt;p&gt;The most secure version of a fresh install is the one with its convenient defaults stripped back out. New database software ships open enough to get you started fast, which is exactly the state an attacker is hoping to find. Day 18 was about closing that gap on MariaDB, then granting its mirror image on AWS access so narrow it can only look.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Install MariaDB and secure it, then write a read-only IAM policy for EC2. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  MariaDB: install, then take the defaults away
&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; mariadb-server
&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; mariadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the step that matters most:&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;# Strip the insecure defaults&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mysql_secure_installation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single command is the whole security story of a fresh MariaDB. Left alone, a new install has an empty root password, an anonymous user anyone can connect as, a test database open to everyone, and remote root login switched on. &lt;code&gt;mysql_secure_installation&lt;/code&gt; walks you through removing every one of those. Skipping it is how databases end up reachable on the internet with no password, which happens far more than it should. With that done, create the database, user, and grants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;kodekloud_db2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'kodekloud_cap'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'securepassword'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;kodekloud_db2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'kodekloud_cap'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MariaDB users have two parts: the name and the host they connect from. &lt;code&gt;'kodekloud_cap'@'localhost'&lt;/code&gt; is a different user from &lt;code&gt;'kodekloud_cap'@'%'&lt;/code&gt;, even with the same name and password. &lt;code&gt;localhost&lt;/code&gt; means local connections only, &lt;code&gt;%&lt;/code&gt; means from any host. If the app connects locally, you grant to localhost, if it connects over the network, you grant to the remote host or &lt;code&gt;%&lt;/code&gt;. Get this wrong, and you get access-denied errors that have nothing to do with the password. And &lt;code&gt;FLUSH PRIVILEGES&lt;/code&gt; reloads the grant tables so the changes take effect now rather than after a restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM read-only: narrow enough to only look
&lt;/h2&gt;

&lt;p&gt;On the AWS side, a read-only policy for EC2. This is the least-privilege idea from Day 16 pointed at a specific job, someone who needs to see the EC2 console but must not change anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"ec2:Describe*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"elasticloadbalancing:Describe*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"autoscaling:Describe*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"cloudwatch:Describe*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"cloudwatch:GetMetricStatistics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"cloudwatch:ListMetrics"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-name&lt;/span&gt; ec2-readonly-console-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://ec2-readonly-console.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the part I did not expect. &lt;code&gt;ec2:Describe*&lt;/code&gt; on its own gives read-only API access, but it does not make the EC2 console usable. The console page pulls in load balancers, auto scaling groups, and CloudWatch metric graphs, and each of those is a separate service. Without &lt;code&gt;elasticloadbalancing:Describe*&lt;/code&gt;, &lt;code&gt;autoscaling:Describe*&lt;/code&gt;, and the CloudWatch read actions, the console throws permission errors across the page even though the core EC2 data loads fine. "Read-only for EC2" in practice means read access across the handful of services the console stitches together. AWS even ships a managed policy, &lt;code&gt;AmazonEC2ReadOnlyAccess&lt;/code&gt;, that bundles exactly these, worth reaching for unless you need something trimmed or custom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two sides of the same discipline
&lt;/h2&gt;

&lt;p&gt;MariaDB's secure install and a read-only policy are the same instinct facing opposite directions. One takes away access that was open by default. The other grants only the access a job genuinely needs. Both start from closed and open up on purpose, which is the reverse of how most work gets done: start open and lock it down later if anyone remembers.&lt;/p&gt;

&lt;p&gt;So here is the Day 18 question. Would you rather begin locked down and grant access deliberately, or begin open and hope you close the doors before someone else finds them?&lt;/p&gt;

&lt;p&gt;Day 18 down. Eighty-two to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>security</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 17: Postgres Privileges and IAM Groups Are the Same Lesson</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Fri, 24 Jul 2026 06:10:38 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-17-postgres-privileges-and-iam-groups-are-the-same-lesson-2l08</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-17-postgres-privileges-and-iam-groups-are-the-same-lesson-2l08</guid>
      <description>&lt;p&gt;Access control is the same problem everywhere. Decide who gets in, and decide exactly what they can do once they are. Day 17 came at that idea from two directions, a Postgres database on Linux and an IAM group on AWS, and the shape underneath both is identical.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Install PostgreSQL, then create a database, a user, and grant that user access. On AWS, create an IAM group to manage permissions for a set of users at once. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL: a database, a user, and a grant
&lt;/h2&gt;

&lt;p&gt;PostgreSQL runs its own user system, separate from Linux logins, and it operates as the &lt;code&gt;postgres&lt;/code&gt; OS user. So you drop into its shell as that user:&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;# Log in as the postgres superuser&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres psql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, it is three statements: a database, a user with a password, and a grant that links them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;my_database&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;my_user&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;ENCRYPTED&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'securepassword'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;my_database&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;my_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WITH ENCRYPTED PASSWORD&lt;/code&gt; stores a hash rather than the plain text, which is the difference between a password and a liability. The &lt;code&gt;GRANT&lt;/code&gt; is what actually connects the user to the database.&lt;/p&gt;

&lt;p&gt;Here is the part that catches people, and it caught me. &lt;code&gt;GRANT ALL PRIVILEGES ON DATABASE&lt;/code&gt; does less than it sounds like. It grants database-level rights, connecting, creating schemas, and making temp tables. It does not grant access to the tables inside. For that, you need a second grant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="n"&gt;TABLES&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;my_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So "I granted the user everything" and "the user can actually read the tables" are two separate statements. Miss the second and your user connects fine, then cannot touch a single row, which is a confusing way to lose an afternoon. If you would rather stay out of the psql shell, &lt;code&gt;createuser&lt;/code&gt; and &lt;code&gt;createdb&lt;/code&gt; do the same job from the command line, and you set the password with &lt;code&gt;ALTER USER&lt;/code&gt; afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM groups: manage permissions for people, not per person
&lt;/h2&gt;

&lt;p&gt;On AWS, the task was an IAM group. Groups exist for the same reason grants do, managing access without repeating yourself. Attach a policy to five users one by one, and you now have five things to update every time the rules change. Attach it to a group, and you have one.&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;# Create the group&lt;/span&gt;
aws iam create-group &lt;span class="nt"&gt;--group-name&lt;/span&gt; Nautilus_Admin

&lt;span class="c"&gt;# Confirm it exists&lt;/span&gt;
aws iam get-group &lt;span class="nt"&gt;--group-name&lt;/span&gt; Nautilus_Admin
aws iam list-groups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating the group is only the opening move. A group with no policy and no members does nothing at all. The next two commands are what make it useful, attaching a policy and adding people:&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 attach-group-policy &lt;span class="nt"&gt;--group-name&lt;/span&gt; Nautilus_Admin &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; &amp;lt;policy-arn&amp;gt;
aws iam add-user-to-group &lt;span class="nt"&gt;--group-name&lt;/span&gt; Nautilus_Admin &lt;span class="nt"&gt;--user-name&lt;/span&gt; Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing. A group is not an identity, you cannot sign in as a group, only as a user or a role. And groups do not nest, you cannot put a group inside another group. It is a flat container for users, and that is deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same idea, two systems
&lt;/h2&gt;

&lt;p&gt;Both tasks are access control wearing different clothes. Postgres asks who connects and what they can touch. IAM asks who you are and what you are allowed to do. The habit that carries across both is setting access at the level that scales, a grant on the right scope, a policy on the group instead of the individual. Do it, person by person, and it works right up until it becomes a mess nobody can audit.&lt;/p&gt;

&lt;p&gt;So here is the Day 17 question. Would you rather set permissions once at the level that scales, or keep patching them one user at a time until you have lost track of who can do what?&lt;/p&gt;

&lt;p&gt;Day 17 down. Eighty-three to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>postgres</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 16: A Load Balancer in Ten Lines of Nginx, and Least-Privilege IAM</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Wed, 22 Jul 2026 04:44:35 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-16-a-load-balancer-in-ten-lines-of-nginx-and-1nka</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-16-a-load-balancer-in-ten-lines-of-nginx-and-1nka</guid>
      <description>&lt;p&gt;You do not need a cloud load balancer to load balance. Ten lines of Nginx will spread traffic across a pool of servers, and understanding those ten lines teaches you what the managed services are quietly doing for you. Day 16 was that, plus setting up IAM the way it is meant to be done, with least privilege instead of a blank check.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Turn Nginx into a load balancer in front of three app servers, then create an IAM user, group, and read-only policy. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nginx load balancer: an upstream block and a proxy_pass
&lt;/h2&gt;

&lt;p&gt;On the load balancer host, install Nginx and get it running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;nginx
systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On each app server: confirm the port Apache is running on&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;ss &lt;span class="nt"&gt;-tulp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;httpd
&lt;span class="c"&gt;# Alternative: systemctl status httpd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the config that turns it into a load balancer:&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;# On the load balancer: edit the Nginx config to set up upstream load balancing&lt;/span&gt;
vi /etc/nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define the pool of backend servers&lt;/span&gt;
&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;app_servers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;stapp01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;stapp02&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;stapp03&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://app_servers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt; &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;upstream&lt;/code&gt; block names a group of backend servers. The &lt;code&gt;proxy_pass&lt;/code&gt; points incoming requests at that group. That is the load balancer. By default Nginx spreads requests across the pool in round-robin order, one server after another in turn, and if you want a different strategy like least-connections or IP hash, it is a single extra directive. The &lt;code&gt;proxy_set_header&lt;/code&gt; lines matter more than they look. Without them, every backend sees the request as coming from the load balancer itself, so these pass along the client's real IP and protocol, which is what keeps your backend logs and any IP-based logic honest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;                 &lt;span class="c"&gt;# test the config&lt;/span&gt;
systemctl reload nginx   &lt;span class="c"&gt;# apply it without dropping connections&lt;/span&gt;
curl http://stlb01:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same discipline as Day 15, test, then reload. Reload rather than restart here, so the new config applies without cutting live connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  IAM: grant exactly what is needed, nothing more
&lt;/h2&gt;

&lt;p&gt;The AWS task was IAM done the right way, a user, a group, and a policy that grants only what is needed:&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-user &lt;span class="nt"&gt;--user-name&lt;/span&gt; Bob
aws iam create-group &lt;span class="nt"&gt;--group-name&lt;/span&gt; Admins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the policy document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeInstances"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeImages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeTags"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"ec2:DescribeSnapshots"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-name&lt;/span&gt; ec2-readonly-policy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://policy.json

aws iam list-policies &lt;span class="nt"&gt;--scope&lt;/span&gt; Local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is least privilege in practice. The policy allows four specific read-only actions, the &lt;code&gt;Describe&lt;/code&gt; calls, and nothing else. The lazy version would be &lt;code&gt;"Action": "ec2:*"&lt;/code&gt;, which hands over full control of EC2 for what is supposed to be read-only access. Do not do that. Grant the exact actions the role needs and widen only when a real need shows up. The &lt;code&gt;"Resource": "*"&lt;/code&gt; looks broad, but most EC2 Describe actions do not support resource-level restrictions, so it is expected here, the actions are already the tight part.&lt;/p&gt;

&lt;p&gt;One small syntax trap. The &lt;code&gt;--policy-document&lt;/code&gt; value is &lt;code&gt;file://policy.json&lt;/code&gt;, with the double slash. Miss it, and the CLI treats your filename as a literal policy string and fails.&lt;/p&gt;

&lt;p&gt;The task stopped at creating these pieces. To actually put them to work, you take two more steps, attach the policy to the group with &lt;code&gt;attach-group-policy&lt;/code&gt;, and add the user to the group with &lt;code&gt;add-user-to-group&lt;/code&gt;, so Bob inherits the read-only access through the group. Managing permissions at the group level instead of per user is the habit that keeps IAM sane as a team grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves us
&lt;/h2&gt;

&lt;p&gt;Both tasks were about doing the fundamental version properly. A load balancer you build yourself teaches you what the managed one abstracts away. An IAM policy scoped to four actions teaches you the security posture that &lt;code&gt;ec2:*&lt;/code&gt; quietly throws away. This whole first stretch of the challenge has been exactly that, the boring, correct version of each skill, because that is the version that holds up when it counts.&lt;/p&gt;

&lt;p&gt;So here is the Day 16 question. Would you rather reach for the broad, convenient option and hope it never bites you, or build the tight version now while the stakes are still just a lab?&lt;/p&gt;

&lt;p&gt;Day 16 down. Eighty-four to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>iam</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 15: HTTPS on Nginx, and the Incremental Trick That Makes Snapshots Cheap</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Mon, 20 Jul 2026 21:10:23 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-15-https-on-nginx-and-the-incremental-trick-that-makes-nc5</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-15-https-on-nginx-and-the-incremental-trick-that-makes-nc5</guid>
      <description>&lt;p&gt;Test before you reload. That one habit is the whole difference between a config change and an outage, and it is baked into how you work with Nginx. Day 15 was TLS on Nginx, where a single broken line can take the site down if you skip the check, and then EBS snapshots, which are quietly one of the best deals in AWS storage.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Configure Nginx to serve HTTPS with provided certificates, then take a point-in-time snapshot of an EBS volume. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nginx TLS: place the certs, wire them up, test before reload
&lt;/h2&gt;

&lt;p&gt;Nginx on RHEL and CentOS comes from the EPEL repository:&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;yum &lt;span class="nb"&gt;install &lt;/span&gt;epel-release &lt;span class="nt"&gt;-y&lt;/span&gt;
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;nginx
systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then put the certificate and key where the system keeps them:&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;# Public certificate goes with the certs&lt;/span&gt;
&lt;span class="nb"&gt;mv &lt;/span&gt;cert.pem /etc/pki/tls/certs/
&lt;span class="c"&gt;# Private key goes in the private directory, root-only&lt;/span&gt;
&lt;span class="nb"&gt;mv &lt;/span&gt;key.pem  /etc/pki/tls/private/
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /etc/pki/tls/private/key.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A word on placement, because it matters. The public certificate belongs in &lt;code&gt;/etc/pki/tls/certs/&lt;/code&gt;. The private key should go in &lt;code&gt;/etc/pki/tls/private/&lt;/code&gt;, readable only by root. My lab notes dropped both into the certs directory to keep it simple, but in practice the private key gets stricter treatment than the certificate, because it is the secret half of the pair. Keep them separate. Then point Nginx at both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vi /etc/nginx/nginx.conf
&lt;span class="c"&gt;# In the TLS server block:&lt;/span&gt;
&lt;span class="c"&gt;# ssl_certificate     /etc/pki/tls/certs/cert.pem;&lt;/span&gt;
&lt;span class="c"&gt;# ssl_certificate_key /etc/pki/tls/private/key.pem;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the habit worth burning in:&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;# ALWAYS validate the config before applying it&lt;/span&gt;
nginx &lt;span class="nt"&gt;-t&lt;/span&gt;

&lt;span class="c"&gt;# Only if the test passes&lt;/span&gt;
systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nginx -t&lt;/code&gt; parses the config and tells you if anything is wrong without touching the running server. Restart on a broken config, and Nginx can fail to come back up, taking the site with it. Test first, every single time, then restart. Finally, confirm HTTPS is answering:&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;# -k skips cert verification, fine for a self-signed test cert&lt;/span&gt;
curl &lt;span class="nt"&gt;-Ik&lt;/span&gt; https://hostname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-k&lt;/code&gt; flag skips certificate verification, which is fine for a self-signed cert in a lab. In production you want the real chain to validate, so treat skipping the check as a test-only shortcut.&lt;/p&gt;

&lt;h2&gt;
  
  
  EBS snapshots: full backups at incremental cost
&lt;/h2&gt;

&lt;p&gt;The AWS task was a point-in-time snapshot of an EBS volume, the standard way to back up a cloud disk:&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;# Snapshot a volume, with a description and a tag&lt;/span&gt;
aws ec2 create-snapshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--volume-id&lt;/span&gt; vol-0e9bbb3943d871f56 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"Nautilus Snapshot"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag-specifications&lt;/span&gt; &lt;span class="s1"&gt;'ResourceType=snapshot,Tags=[{Key=purpose,Value=nautilus-vol-ss}]'&lt;/span&gt;

&lt;span class="c"&gt;# Watch it move from pending to completed&lt;/span&gt;
aws ec2 describe-snapshots &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:purpose,Values=nautilus-vol-ss"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what makes snapshots such a good deal. They are incremental. The first snapshot copies the whole volume, but every snapshot after that stores only the blocks that changed since the last one. Take a daily snapshot of a 100 GB volume where a few hundred megabytes change each day, and you pay for those few hundred megabytes, not 100 GB a day. AWS still lets you restore any single snapshot as a full volume, it tracks the block references behind the scenes, so you get full backups at incremental cost.&lt;/p&gt;

&lt;p&gt;Two practical notes. Snapshot creation is asynchronous, the call returns immediately, and the status moves from &lt;code&gt;pending&lt;/code&gt; to &lt;code&gt;completed&lt;/code&gt; on its own schedule, so a script should wait on &lt;code&gt;completed&lt;/code&gt; rather than assume. And a snapshot is more than a backup, you can build a new volume from it or copy it to another region for disaster recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The through-line
&lt;/h2&gt;

&lt;p&gt;Both tasks were about protecting something valuable with a cheap, repeatable action. &lt;code&gt;nginx -t&lt;/code&gt; protects your uptime for the cost of two seconds before every reload. Snapshots protect your data for the cost of a few changed blocks. The good habits in this work are rarely expensive, they are just easy to skip until the day skipping them hurts.&lt;/p&gt;

&lt;p&gt;So here is the Day 15 question. Would you rather build the cheap safety checks into your routine now, or explain later why the one time you skipped them was the time it mattered?&lt;/p&gt;

&lt;p&gt;Day 15 down. Eighty-five to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>nginx</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 14: Restoring a Broken httpd, and the One EC2 Command With No Undo</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sun, 19 Jul 2026 21:08:36 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-14-restoring-a-broken-httpd-and-the-one-ec2-command-with-1fpg</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-14-restoring-a-broken-httpd-and-the-one-ec2-command-with-1fpg</guid>
      <description>&lt;p&gt;Some commands you can walk back. Terminating an EC2 instance is not one of them. Day 14 paired a recoverable problem, a web server knocked over by a rogue process, with an unrecoverable one, deleting a server on purpose, and the contrast is the whole lesson.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Track down the process blocking Apache and restore the service, then terminate an EC2 instance and confirm it is gone. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  httpd: diagnose in order, then restore
&lt;/h2&gt;

&lt;p&gt;When httpd will not start, resist the urge to guess. Work in order. Start with what the service itself reports:&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;# What does httpd think is wrong&lt;/span&gt;
systemctl status httpd
systemctl start httpd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The status output usually names the problem, and a failed bind on the port is the classic one. Before assuming a rogue process, check that httpd's own config is sane, because a wrong port or hostname produces the same "won't start" symptom:&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;# Check the configured listen port and server name&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; listen /etc/httpd/conf/httpd.conf
vi /etc/httpd/conf/httpd.conf
&lt;span class="c"&gt;# Fix the ServerName directive if it is wrong: ServerName hostname:&amp;lt;port&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the config is fine and the port is genuinely taken, then you go hunting for the process holding it:&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;# Find the PID on the conflicting port&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;su -
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; net-tools
netstat &lt;span class="nt"&gt;-tulpn&lt;/span&gt;

&lt;span class="c"&gt;# Clear it, then bring httpd back&lt;/span&gt;
&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; &amp;lt;PID&amp;gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;httpd
systemctl start httpd
systemctl status httpd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kill -9&lt;/code&gt; is SIGKILL, the instant, no-cleanup kill. It is the right tool when a process is wedged and ignoring a polite request, but as a default habit, it is worth trying a plain &lt;code&gt;kill&lt;/code&gt; first. The order that matters here is diagnostic: config before process, gentle signal before forceful one. Rushing to &lt;code&gt;kill -9&lt;/code&gt; at the first sign of trouble is how you mask the real cause instead of fixing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terminating EC2: the command with no undo
&lt;/h2&gt;

&lt;p&gt;Day 9 was about protecting an instance from termination. Day 14 is the other side of that lever, actually terminating one, on purpose:&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;# Terminate the instance&lt;/span&gt;
aws ec2 terminate-instances &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-1234567890abcdef0

&lt;span class="c"&gt;# Confirm the state moved to 'terminated'&lt;/span&gt;
aws ec2 describe-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; i-1234567890abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Reservations[*].Instances[*].State.Name"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no recycle bin here. Once an instance is terminated, it and its instance-store data are gone for good. The thing worth checking before you run this is what happens to the storage. By default, the root EBS volume is set to delete on termination, so it goes with the instance. Extra volumes you attached usually survive, they detach instead of being deleted, but do not assume it. The &lt;code&gt;DeleteOnTermination&lt;/code&gt; flag is set per volume, and you should know its value before you pull the trigger.&lt;/p&gt;

&lt;p&gt;One quirk that confuses people. A terminated instance still appears in &lt;code&gt;describe-instances&lt;/code&gt; for a little while, in the &lt;code&gt;terminated&lt;/code&gt; state, before it disappears completely. That is normal, not a failed termination. And if this is a box you cannot afford to lose, that is precisely what Day 9's termination protection is for. Turn it on, and this command bounces instead of executing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contrast
&lt;/h2&gt;

&lt;p&gt;The two tasks sit at opposite ends of reversibility. A downed web server is a puzzle you can take your time on, because nothing is lost while you diagnose. A termination is a decision you make once and cannot take back. Knowing which kind of action you are about to perform, recoverable or final, is what should change how carefully you move.&lt;/p&gt;

&lt;p&gt;So here is the Day 14 question. Before you run a command, do you actually know whether you can undo it, or do you find out only when you try and can't?&lt;/p&gt;

&lt;p&gt;Day 14 down. Eighty-six to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>cloud</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 13: Firewall Rules Are About Order, and Creating an AMI Reboots the Instance</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Fri, 17 Jul 2026 19:12:36 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-13-firewall-rules-are-about-order-and-creating-an-ami-58kj</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-13-firewall-rules-are-about-order-and-creating-an-ami-58kj</guid>
      <description>&lt;p&gt;A firewall is not a list of rules. It is an ordered list of rules, and the order is the whole thing. Put the same two rules in the wrong sequence, and you either block traffic you meant to allow or allow traffic you meant to block. Day 13 drove that home, then taught me that snapshotting a server has a side effect the name does not warn you about.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Lock a port down so only the load balancer can reach it, then capture a running EC2 instance as an AMI. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  iptables: allow the one source, drop the rest, in that order
&lt;/h2&gt;

&lt;p&gt;The goal was to let one machine, the load balancer, reach a port and block everyone else. That is two rules, and their order decides whether it works. First, install and enable the iptables service:&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;yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; iptables-services
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;iptables
systemctl start iptables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the two rules that do the work:&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;# Allow the port FROM the load balancer's IP only&lt;/span&gt;
iptables &lt;span class="nt"&gt;-R&lt;/span&gt; INPUT 5 &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--destination-port&lt;/span&gt; 5004 &lt;span class="nt"&gt;-s&lt;/span&gt; 172.16.238.14 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT

&lt;span class="c"&gt;# Drop all other traffic to that port&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; INPUT &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--destination-port&lt;/span&gt; 5004 &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iptables reads the INPUT chain top to bottom and stops at the first rule that matches. So the ACCEPT for the load balancer has to come before the DROP for everyone else. If the DROP sat first, it would match the load balancer's packets too and drop them before the ACCEPT was ever reached. The &lt;code&gt;-R INPUT 5&lt;/code&gt; replaces the rule at position 5 to slot the ACCEPT into the right place, and &lt;code&gt;-A&lt;/code&gt; appends the DROP to the end, which is exactly where a catch-all belongs.&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;# Persist the rules so a reboot doesn't wipe them&lt;/span&gt;
service iptables save

&lt;span class="c"&gt;# Confirm what is saved&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables-save
iptables &lt;span class="nt"&gt;-L&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same persistence trap as Day 12. &lt;code&gt;service iptables save&lt;/code&gt; writes the rules to &lt;code&gt;/etc/sysconfig/iptables&lt;/code&gt;. Until you run it, everything you configured lives in memory only and vanishes on reboot. And a naming gotcha: &lt;code&gt;iptables-save&lt;/code&gt; does not save, it prints the current rules to your screen. The command that actually persists is the &lt;code&gt;service iptables save&lt;/code&gt; one.&lt;/p&gt;

&lt;h2&gt;
  
  
  AMI: a full-instance image, with a reboot in the middle
&lt;/h2&gt;

&lt;p&gt;The AWS task was to capture a running instance as an AMI, a reusable image you can launch fresh instances from:&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;# Create the image from the instance&lt;/span&gt;
aws ec2 create-image &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-1234567890abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"My server"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"An AMI for my server"&lt;/span&gt;

&lt;span class="c"&gt;# It takes a few minutes to become available&lt;/span&gt;
aws ec2 describe-images &lt;span class="nt"&gt;--image-ids&lt;/span&gt; ami-xxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the detail my notes glossed over, and it matters. By default, &lt;code&gt;create-image&lt;/code&gt; reboots the instance. AWS shuts it down and restarts it to snapshot the volumes in a consistent state, with all buffered and in-memory data flushed to disk first (per the AWS create-image docs). That is the safe default, but an unexpected reboot on a production box in the middle of the day is not something you want to trigger by accident. If you cannot take the reboot, you add &lt;code&gt;--no-reboot&lt;/code&gt;, and then AWS warns it cannot guarantee the filesystem integrity of the image. So it is a real tradeoff, consistency with a reboot or no reboot with a small risk, not a free action either way.&lt;/p&gt;

&lt;p&gt;Two more things. The AMI includes snapshots of every attached EBS volume by default, so it is a full-instance image, not just the root disk. And AMIs are region-specific. If you need one elsewhere, you copy it with &lt;code&gt;aws ec2 copy-image&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What connects them
&lt;/h2&gt;

&lt;p&gt;Both tasks looked like a single action and were really about a hidden mechanic. The firewall worked or failed on rule order, something invisible until you know to look for it. The AMI looked like a passive snapshot but carried an active reboot. The theme of the week keeps coming back: the command is easy, and the behaviour underneath it is where the real knowledge lives.&lt;/p&gt;

&lt;p&gt;So here is the Day 13 question. Would you rather run commands and get surprised by what they do underneath, or learn the mechanics so nothing about the outcome catches you off guard?&lt;/p&gt;

&lt;p&gt;Day 13 down. Eighty-seven to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>security</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 12: A Port Conflict Killed httpd, and an Attached Volume Isn't a Mounted One</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Thu, 16 Jul 2026 06:48:07 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-12-a-port-conflict-killed-httpd-and-an-attached-volume-3nom</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-12-a-port-conflict-killed-httpd-and-an-attached-volume-3nom</guid>
      <description>&lt;p&gt;Two services cannot listen on the same port. So when Apache refuses to start, the most common reason is boring and specific, something else grabbed the port first. Day 12 was about finding that something and clearing it, then handling the AWS storage version of a job that looks finished but isn't.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Work out why httpd will not come up on its port and fix the conflict, then attach an EBS volume to an EC2 instance. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  httpd: find the process on the port, then clear it
&lt;/h2&gt;

&lt;p&gt;When httpd fails to bind its port, you need to know what is already sitting there. &lt;code&gt;netstat&lt;/code&gt; shows every listening socket and the process behind it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install net-tools if netstat isn't present&lt;/span&gt;
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; net-tools

&lt;span class="c"&gt;# List listening TCP/UDP ports with the owning PID&lt;/span&gt;
netstat &lt;span class="nt"&gt;-tulpn&lt;/span&gt;

&lt;span class="c"&gt;# Narrow to the port you care about&lt;/span&gt;
netstat &lt;span class="nt"&gt;-tulpn&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-tulpn&lt;/code&gt; flags are worth learning as one unit: TCP and UDP, listening sockets, numeric ports, and the PID. That last piece, the PID, is what lets you act. Once you know which process is squatting on the port, clear it and start httpd:&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;# Stop the conflicting process&lt;/span&gt;
&lt;span class="nb"&gt;sudo kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; &amp;lt;PID&amp;gt;

&lt;span class="c"&gt;# Now httpd can bind the port&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;httpd
systemctl start httpd
systemctl status httpd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick word on &lt;code&gt;kill -9&lt;/code&gt;. It sends SIGKILL, which ends a process instantly with no chance to clean up. It works, but it is a blunt instrument. Try a plain &lt;code&gt;kill&lt;/code&gt; first, which sends SIGTERM and lets the process shut down gracefully, and reach for &lt;code&gt;-9&lt;/code&gt; only when that gets ignored. With the port free, the firewall still has to allow the traffic in:&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;# Allow inbound traffic on the port, inserted at the top of INPUT&lt;/span&gt;
iptables &lt;span class="nt"&gt;-I&lt;/span&gt; INPUT &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--destination-port&lt;/span&gt; &amp;lt;port&amp;gt; &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT

&lt;span class="c"&gt;# Persist the rules so they survive a reboot&lt;/span&gt;
service iptables save
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;iptables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;iptables -I&lt;/code&gt; inserts the ACCEPT rule at the top of the INPUT chain, so it is checked before any DROP rule further down. And the step people forget, &lt;code&gt;service iptables save&lt;/code&gt;, writes the rules to disk. Skip it, and every rule you just added disappears on the next reboot.&lt;/p&gt;

&lt;h2&gt;
  
  
  EBS: attaching is not mounting
&lt;/h2&gt;

&lt;p&gt;The AWS task was to attach an existing EBS volume to a running instance:&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;# Attach the volume (same AZ as the instance, always)&lt;/span&gt;
aws ec2 attach-volume &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--volume-id&lt;/span&gt; vol-xxxxxxxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-xxxxxxxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--device&lt;/span&gt; /dev/sdf

&lt;span class="c"&gt;# Check the attachment state&lt;/span&gt;
aws ec2 describe-volumes &lt;span class="nt"&gt;--volume-ids&lt;/span&gt; vol-xxxxxxxxxx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know. First, the volume and the instance have to be in the same availability zone, the same rule that governs snapshots and network interfaces. Second, and this is the one that catches people, attaching is not mounting. The API call succeeds, and the volume is now wired to the instance, but the operating system still sees a raw disk. It is not usable until you go inside the instance and finish the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inside the instance&lt;/span&gt;
lsblk                        &lt;span class="c"&gt;# see the new disk&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkfs &lt;span class="nt"&gt;-t&lt;/span&gt; xfs /dev/xvdf   &lt;span class="c"&gt;# format it, only if it is blank&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /dev/xvdf /data   &lt;span class="c"&gt;# mount it somewhere&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a modern wrinkle worth knowing. You asked AWS for &lt;code&gt;/dev/sdf&lt;/code&gt;, but on current Nitro-based instances, the OS often shows the volume as an NVMe device like &lt;code&gt;/dev/nvme1n1&lt;/code&gt; instead. The name you request and the name the kernel assigns can differ, so trust &lt;code&gt;lsblk&lt;/code&gt; over the device string you passed to the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared lesson
&lt;/h2&gt;

&lt;p&gt;Both tasks have the same shape. The command returning success is not the same as the job being done. httpd needed the port cleared and the firewall opened. The EBS volume needed formatting and mounting after the attach. Cloud APIs and system services both hand you a green result at the halfway mark, and knowing that is what separates "it says it worked" from "it actually works."&lt;/p&gt;

&lt;p&gt;So here is the Day 12 question. Would you rather trust the success message and move on, or know exactly which half of the job that success message actually covers?&lt;/p&gt;

&lt;p&gt;Day 12 down. Eighty-eight to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>networking</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 11: A WAR File Deploys Itself, and an ENI Is a Network Card You Can Move</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Wed, 15 Jul 2026 18:51:09 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-11-a-war-file-deploys-itself-and-an-eni-is-a-network-card-45o0</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-11-a-war-file-deploys-itself-and-an-eni-is-a-network-card-45o0</guid>
      <description>&lt;p&gt;Deploying a Java web app to Tomcat sounds like it should need a deploy tool. It does not. You copy one file into one folder, and Tomcat does the rest. Day 11 was a good reminder that a lot of what people call deployment is just putting the right file in the right place.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Install and configure Tomcat, then deploy a WAR file to it, and attach a second network interface to an EC2 instance. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tomcat: install, set the port, drop in the WAR
&lt;/h2&gt;

&lt;p&gt;Tomcat is a servlet container, the thing that actually runs Java web applications. Install it from the package manager, and you get the service plus a standard directory layout under &lt;code&gt;/usr/share/tomcat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su -
yum update &lt;span class="nt"&gt;-y&lt;/span&gt;
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; tomcat zip

&lt;span class="c"&gt;# Enable, start, verify&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;tomcat
systemctl start tomcat
systemctl status tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HTTP port lives in &lt;code&gt;server.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vi /usr/share/tomcat/conf/server.xml
&lt;span class="c"&gt;# Find the Connector with protocol="HTTP/1.1" and change its port&lt;/span&gt;
systemctl restart tomcat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the Connector with &lt;code&gt;protocol="HTTP/1.1"&lt;/code&gt;, and only that one. There is a second Connector for the AJP protocol further down, leave it alone, it is not your HTTP port. Restart Tomcat to apply the change. Then deploy the application:&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;# Copy the WAR into Tomcat's webapps directory&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/ROOT.war /usr/share/tomcat/webapps/

&lt;span class="c"&gt;# Optionally expand it in place to inspect or edit files&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /usr/share/tomcat/webapps
unzip ROOT.war

&lt;span class="c"&gt;# Test&lt;/span&gt;
curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:&amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the part that surprises people. Deploying is copying the WAR into the &lt;code&gt;webapps&lt;/code&gt; directory. Tomcat watches that folder and automatically expands and deploys anything you drop in. A WAR named &lt;code&gt;ROOT.war&lt;/code&gt; becomes the application at the root path. Unzipping it yourself is optional, handy when you want to look inside or change a file, but Tomcat would have expanded it on its own.&lt;/p&gt;

&lt;p&gt;If the app server has no direct internet access, you move the WAR onto it from a jump server with &lt;code&gt;scp&lt;/code&gt;, which is how the file got there to begin with. That jump-server-then-scp pattern shows up constantly once you work inside locked-down networks.&lt;/p&gt;

&lt;h2&gt;
  
  
  ENI: a network card you can move between instances
&lt;/h2&gt;

&lt;p&gt;An Elastic Network Interface is a virtual network card. Every instance has one by default at device index 0, the primary. This task attached a second, pre-existing ENI as a secondary interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List available interfaces to get the ENI ID&lt;/span&gt;
aws ec2 describe-network-interfaces

&lt;span class="c"&gt;# Attach it as a secondary interface (index 1; index 0 is the primary)&lt;/span&gt;
aws ec2 attach-network-interface &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-interface-id&lt;/span&gt; eni-0dc56a8d4640ad10a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-1234567890abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--device-index&lt;/span&gt; 1

&lt;span class="c"&gt;# Verify&lt;/span&gt;
aws ec2 describe-network-interfaces &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network-interface-ids&lt;/span&gt; eni-0dc56a8d4640ad10a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is what an ENI carries. It holds its own private IP, any Elastic IP, its MAC address, and its security groups, and it keeps all of that when you detach it from one instance and attach it to another. That is what makes it a failover tool. If an instance dies, you can move its ENI, and its entire network identity to a standby instance, and traffic follows without anything upstream needing to change.&lt;/p&gt;

&lt;p&gt;The one hard rule is the same one that governs EBS volumes. The ENI and the instance have to be in the same availability zone. Network interfaces do not cross zones.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern underneath
&lt;/h2&gt;

&lt;p&gt;Both tasks were about how little some things actually take once you understand them. A Java deployment is a file copied into a watched folder. A failover network setup is one interface moved between two machines. The complexity people imagine is often just unfamiliarity, and it shrinks fast once you have done the thing a single time.&lt;/p&gt;

&lt;p&gt;So here is the Day 11 question. Would you rather keep treating deployment and networking as intimidating black boxes, or open them once and find out how simple the moving parts really are?&lt;/p&gt;

&lt;p&gt;Day 11 down. Eighty-nine to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>tomcat</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 10: Automating Backups Over SSH, and What an Elastic IP Really Costs Now</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Mon, 13 Jul 2026 16:32:46 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-10-automating-backups-over-ssh-and-what-an-elastic-ip-l4p</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-10-automating-backups-over-ssh-and-what-an-elastic-ip-l4p</guid>
      <description>&lt;p&gt;A backup you have to babysit is not a backup. The whole point is that it runs without you, at 2 am, while you sleep, which means it cannot stop to ask anyone for a password. Day 10 was about making that automation genuinely hands-off and then giving a server an address that does not move.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Write a Bash script that zips a directory and ships it to a backup server over SSH, then associate an Elastic IP with an EC2 instance so its public address survives a reboot. The tasks come from the KodeKloud Engineer platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backup script: zip, ship, and never type a password
&lt;/h2&gt;

&lt;p&gt;The script itself is short. Zip a directory, then copy the archive to a remote backup server with &lt;code&gt;scp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Zip the target directory&lt;/span&gt;
zip &lt;span class="nt"&gt;-r&lt;/span&gt; /backup/xfusioncorp_official.zip /var/www/html/official

&lt;span class="c"&gt;# Copy the archive to the backup server&lt;/span&gt;
scp /backup/xfusioncorp_official.zip clint@stbkp01:/backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch is the &lt;code&gt;scp&lt;/code&gt; line. By default, it asks for the remote user's password, and a script cannot type one. So before this can run unattended, you set up key-based SSH between the two machines, exactly like Day 7:&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;# On the source server, once&lt;/span&gt;
ssh-keygen                       &lt;span class="c"&gt;# generate a key pair if you don't have one&lt;/span&gt;
ssh-copy-id clint@stbkp01        &lt;span class="c"&gt;# push the public key to the backup server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the source server logs into the backup server with its key, no prompt, and &lt;code&gt;scp&lt;/code&gt; runs silently. Make the script executable and run it:&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;chmod&lt;/span&gt; +x /scripts/official_backup.sh
/scripts/official_backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two practical notes. The &lt;code&gt;/backup&lt;/code&gt; directory has to exist before &lt;code&gt;zip&lt;/code&gt; writes to it, or the script fails on the first line. And this is the natural partner to Day 6's cron. Drop this script into a crontab line, and the backup runs itself every night. That is the moment a script stops being a command you run and becomes a system that runs on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Elastic IP: a fixed address, and a new line on the bill
&lt;/h2&gt;

&lt;p&gt;On the AWS side, the task was to give an instance a stable public IP. A normal EC2 public IP is temporary, it changes every time the instance stops and starts, which breaks anything pointing at it. An Elastic IP is a public IPv4 address you allocate and hold onto, and it stays put across reboots. Associating one is straightforward:&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;# Find the allocation ID of your Elastic IP&lt;/span&gt;
aws ec2 describe-addresses

&lt;span class="c"&gt;# Associate it with the instance&lt;/span&gt;
aws ec2 associate-address &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-0b263919b6498b123 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allocation-id&lt;/span&gt; eipalloc-64d5890a

&lt;span class="c"&gt;# Confirm the association&lt;/span&gt;
aws ec2 describe-addresses &lt;span class="nt"&gt;--allocation-ids&lt;/span&gt; eipalloc-64d5890a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is where I have to correct some old advice, including what my own notes said. It used to be true that an Elastic IP was free as long as it was attached to a running instance, and you only paid for idle ones. That is no longer the case. Since February 1, 2024, AWS charges for every public IPv4 address, about $0.005 an hour, whether it is attached or not, which works out to roughly $3.60 a month per address (per the AWS pricing announcement). There is a free tier of 750 hours a month for the first year, but the old attached-equals-free model is gone. The practical advice is what it always was: do not hoard public IPv4, except now it costs you even while in use.&lt;/p&gt;

&lt;p&gt;One more detail. The default quota is 5 Elastic IPs per region. It is adjustable, but it exists to discourage exactly that hoarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Day 10 was really about
&lt;/h2&gt;

&lt;p&gt;Both tasks were about doing permanence properly. The backup script only works if the trust between the two servers is set up once and then stays out of the way. The Elastic IP only helps if you treat it as a resource you own and pay for, not a free convenience. Automation and cloud both pay you back for setting things up deliberately and knowing what they cost.&lt;/p&gt;

&lt;p&gt;So here is the Day 10 question. Would you rather wire up automation that runs itself and know exactly what each piece costs, or keep doing the work by hand because the setup felt like too much effort up front?&lt;/p&gt;

&lt;p&gt;Day 10 down. Ninety to go.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>aws</category>
      <category>bash</category>
    </item>
    <item>
      <title>100 Days of DevOps and Cloud (AWS), Day 9: A Database That Won't Start Is Usually a Permissions Problem</title>
      <dc:creator>Nnamdi Felix Ibe</dc:creator>
      <pubDate>Sun, 12 Jul 2026 20:35:47 +0000</pubDate>
      <link>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-9-a-database-that-wont-start-is-usually-a-permissions-1cpk</link>
      <guid>https://dev.to/ndcodes/100-days-of-devops-and-cloud-aws-day-9-a-database-that-wont-start-is-usually-a-permissions-1cpk</guid>
      <description>&lt;p&gt;When a service refuses to start, the instinct is to blame the service. Usually, the service itself is fine, and something around it is broken. Day 9's dead MariaDB was a clean example of exactly that.&lt;/p&gt;

&lt;p&gt;One Linux task, one AWS task. Bring a failed MariaDB service back to life by reading the logs rather than guessing, then enable termination protection for an EC2 instance so it cannot be deleted on a bad day. The tasks come from the KodeKloud Engineer platform, the same as the rest of this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  MariaDB: read the logs, don't guess
&lt;/h2&gt;

&lt;p&gt;A database that will not start feels opaque, and that is what makes it stressful. It is not actually opaque. MariaDB, like most services, tells you exactly why it failed if you look in the right place, so the discipline is looking before you start changing things.&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;# Where does it stand right now&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status mariadb

&lt;span class="c"&gt;# Read the service's own log, newest lines first&lt;/span&gt;
&lt;span class="nb"&gt;sudo tail&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt; /var/log/mariadb/mariadb.log

&lt;span class="c"&gt;# Narrow the systemd journal to the failure window&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; mariadb &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"2025-11-09 12:43:00"&lt;/span&gt; &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s2"&gt;"2025-11-09 12:45:00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;systemctl status&lt;/code&gt; gives you the headline, the last few log lines and whether systemd even tried to start it. The service log and &lt;code&gt;journalctl&lt;/code&gt; give you the details. Reading the tail first is the shortcut, the newest lines are almost always the ones that matter.&lt;/p&gt;

&lt;p&gt;In this case, the log pointed at the runtime directory. MariaDB runs as the &lt;code&gt;mysql&lt;/code&gt; user, and it needs to write its socket and PID file into &lt;code&gt;/run/mariadb&lt;/code&gt;. That directory was owned by root, so the &lt;code&gt;mysql&lt;/code&gt; user could not write to it, and the service died on startup. The fix is one line:&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;# Who owns the runtime directory&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; /run/mariadb

&lt;span class="c"&gt;# Hand it to the user the service actually runs as&lt;/span&gt;
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;mysql:mysql /run/mariadb

&lt;span class="c"&gt;# Restart and confirm&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart mariadb
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status mariadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole lesson in miniature. The database engine was never broken. A directory it depended on had the wrong owner, and everything downstream fell over. While you are in there, confirm the service is set to come back after a reboot too:&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;# Make sure the fix survives a reboot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl is-enabled mariadb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  EC2 termination protection: a guard against the irreversible
&lt;/h2&gt;

&lt;p&gt;The AWS task is the safety net for the worst kind of mistake, deleting a server you still needed. Terminating an EC2 instance is permanent, so termination protection exists to make it hard to do by accident. Turn it on with one flag:&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;# Enable termination protection&lt;/span&gt;
aws ec2 modify-instance-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-1234567890abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--disable-api-termination&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same naming quirk I hit with stop protection on Day 8. The flag is &lt;code&gt;--disable-api-termination&lt;/code&gt;, which sounds destructive but is the switch that protects the instance. It disables the terminate API for that box. To lift it later, you use &lt;code&gt;--no-disable-api-termination&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The note verified this by actually calling &lt;code&gt;terminate-instances&lt;/code&gt; and watching it get refused. That works, but if the protection had not applied, you would have deleted the instance to find out. Ask AWS directly 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;# Confirm protection is on without risking the instance&lt;/span&gt;
aws ec2 describe-instance-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; i-1234567890abcdef0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute&lt;/span&gt; disableApiTermination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it returns true, you are protected, and terminate calls will bounce with an error. Two things to keep in mind. Termination protection and stop protection are separate switches, so a box you want fully guarded needs both. And this only blocks the API path, it does nothing about someone deleting the attached volumes, so treat it as one layer, not the whole wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thread
&lt;/h2&gt;

&lt;p&gt;Both halves of Day 9 were about the same instinct, slowing down before you act. The database rewarded a careful read of the logs over a panicked restart. The EC2 flag made the destructive action deliberately awkward to perform. Speed feels productive, but the operators who last are the ones who look first.&lt;/p&gt;

&lt;p&gt;So here is the Day 9 question. Would you rather move fast and hope the thing you are about to break is not the thing you needed, or spend two minutes confirming before you commit?&lt;/p&gt;

&lt;p&gt;Day 9 down. Ninety-one to go.&lt;/p&gt;

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