<?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: Ilyas Rufai</title>
    <description>The latest articles on DEV Community by Ilyas Rufai (@rufilboss).</description>
    <link>https://dev.to/rufilboss</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%2F548489%2F0921b258-f827-4615-a9cf-d5c6f016875b.jpg</url>
      <title>DEV Community: Ilyas Rufai</title>
      <link>https://dev.to/rufilboss</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rufilboss"/>
    <language>en</language>
    <item>
      <title>IAM Role Design Patterns for Multi-Account AWS Environments</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Wed, 24 Jun 2026 04:09:26 +0000</pubDate>
      <link>https://dev.to/rufilboss/iam-role-design-patterns-for-multi-account-aws-environments-3eng</link>
      <guid>https://dev.to/rufilboss/iam-role-design-patterns-for-multi-account-aws-environments-3eng</guid>
      <description>&lt;p&gt;Your team starts with one AWS account. Six months later, you have five: production, staging, shared services, security, and a sandbox nobody remembers creating. Someone creates an &lt;code&gt;Admin&lt;/code&gt; IAM user in each account. CI/CD stores access keys in three different secret stores. An auditor asks who assumed full access in production last Tuesday, and the answer is a spreadsheet.&lt;/p&gt;

&lt;p&gt;Multi-account AWS is a security win, but only if &lt;strong&gt;access is role-based, scoped, and observable&lt;/strong&gt;. Copying single-account IAM habits into an organization creates credential sprawl, unclear blast radius, and reviews that fail the moment someone says "shared admin user."&lt;/p&gt;

&lt;p&gt;In this article, you will learn five IAM role design patterns that scale across AWS Organizations, when to use each one, and how to implement them with trust policies, guardrails, and Terraform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Platform engineers, DevSecOps engineers, and cloud architects building or maturing multi-account AWS environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you will learn:&lt;/strong&gt; How to design roles for humans, automation, audit, cross-account resources, and third-party access, without defaulting to long-lived keys or duplicated admin policies.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Basic familiarity with IAM roles and policies&lt;/li&gt;
&lt;li&gt;An AWS Organization (or plans to adopt one)&lt;/li&gt;
&lt;li&gt;Optional: Terraform for the implementation snippets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related work:&lt;/strong&gt; If you are new to role-based EC2 access in a single account, start with &lt;a href="https://dev.to/rufilboss/terraform-ec2-and-s3-upload-access-a-simple-iam-role-setup-without-access-keys-4k99"&gt;Terraform EC2 and S3 Upload Access&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;IAM Identity Center&lt;/strong&gt; (successor to AWS SSO) for human access across accounts, not IAM users in every account.&lt;/li&gt;
&lt;li&gt;Give CI/CD a &lt;strong&gt;dedicated role in a tooling account&lt;/strong&gt; that assumes &lt;strong&gt;short-lived deployment roles&lt;/strong&gt; in workload accounts.&lt;/li&gt;
&lt;li&gt;Centralize audit with &lt;strong&gt;read-only cross-account roles&lt;/strong&gt; into a security/logging account wired to CloudTrail and AWS Config.&lt;/li&gt;
&lt;li&gt;For shared resources (S3, KMS), combine &lt;strong&gt;resource policies&lt;/strong&gt; with &lt;strong&gt;role trust&lt;/strong&gt; and do not make roles overly broad to "make it work."&lt;/li&gt;
&lt;li&gt;Lock the org with &lt;strong&gt;Service Control Policies (SCPs)&lt;/strong&gt;; use &lt;strong&gt;permission boundaries&lt;/strong&gt; on custom roles so teams cannot escalate past intent.&lt;/li&gt;
&lt;li&gt;Every cross-account trust policy should name a &lt;strong&gt;specific principal&lt;/strong&gt;, add &lt;strong&gt;conditions&lt;/strong&gt; (&lt;code&gt;aws:PrincipalOrgID&lt;/code&gt;, &lt;code&gt;aws:PrincipalAccount&lt;/code&gt;), and be visible in CloudTrail.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why single-account IAM thinking fails at scale
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Habit from one account&lt;/th&gt;
&lt;th&gt;What breaks in multi-account&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM users with access keys per engineer&lt;/td&gt;
&lt;td&gt;Keys multiply; rotation and offboarding become error-prone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One &lt;code&gt;AdministratorAccess&lt;/code&gt; role cloned everywhere&lt;/td&gt;
&lt;td&gt;No separation between prod and sandbox; blast radius is the whole org&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD keys stored per environment&lt;/td&gt;
&lt;td&gt;Leaked key in staging can affect production if policies are loose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"We'll clean it up later" bucket policies&lt;/td&gt;
&lt;td&gt;Cross-account access becomes permanent &lt;code&gt;Principal: "*"&lt;/code&gt; debt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; In multi-account AWS, identity lives in one place, authorization is &lt;strong&gt;scoped per account and per job function&lt;/strong&gt;, and guardrails apply &lt;strong&gt;above&lt;/strong&gt; IAM with SCPs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reference account model
&lt;/h2&gt;

&lt;p&gt;You do not need a perfect landing zone on day one, but roles make more sense when accounts have intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Organization
├── Security OU
│   └── security-audit (log archive, read-only aggregation)
├── Infrastructure OU
│   └── shared-networking (optional)
├── Workloads OU
│   ├── prod
│   ├── staging
│   └── dev
└── Sandbox OU
    └── sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfy361cpw03qpz9g9zzg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfy361cpw03qpz9g9zzg.png" alt="AWS Organizations OU hierarchy with security, workload, and sandbox accounts" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6fz4c5e7vzsps8iwi0l3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6fz4c5e7vzsps8iwi0l3.png" alt="Multi-account IAM role patterns across security, tooling, and workload accounts" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each pattern below maps to a &lt;strong&gt;different trust direction&lt;/strong&gt;. Mixing them into one "super role" is the most common design mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1 — Human access with IAM Identity Center
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; Engineers, operators, or auditors need console or CLI access to multiple accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not:&lt;/strong&gt; Create IAM users in every account and email access keys.&lt;/p&gt;

&lt;p&gt;IAM Identity Center issues short-lived credentials via permission sets mapped to groups (for example, &lt;code&gt;PlatformAdmins&lt;/code&gt;, &lt;code&gt;Developers&lt;/code&gt;, &lt;code&gt;ReadOnlyAuditors&lt;/code&gt;). AWS automatically creates corresponding roles in member accounts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2iavgq2tnw0gpy8csq1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2iavgq2tnw0gpy8csq1z.png" alt="IAM Identity Center human access flow across permission sets and member accounts" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Map job function to permission sets, not individuals to admin.&lt;/li&gt;
&lt;li&gt;Prefer &lt;code&gt;ReadOnlyAccess&lt;/code&gt; or custom read sets for most engineers; reserve power access for break-glass workflows.&lt;/li&gt;
&lt;li&gt;Use account assignments so &lt;code&gt;Developers&lt;/code&gt; get &lt;code&gt;PowerUserAccess&lt;/code&gt; in &lt;code&gt;dev&lt;/code&gt; but &lt;code&gt;ReadOnlyAccess&lt;/code&gt; in &lt;code&gt;prod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enable IAM Identity Center in the &lt;strong&gt;management account&lt;/strong&gt; (or a dedicated identity account if your org design uses one).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; A user in the &lt;code&gt;Developers&lt;/code&gt; group can open the &lt;code&gt;dev&lt;/code&gt; account console but receives &lt;code&gt;AccessDenied&lt;/code&gt; when attempting the same action in &lt;code&gt;prod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;IAM Identity Center → Permission sets; predefined&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzg4vlwn4gugcbr785075.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzg4vlwn4gugcbr785075.png" alt="IAM Identity Center Predefined1" width="800" height="376"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bvemldgtnpy2pb21oro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bvemldgtnpy2pb21oro.png" alt="IAM Identity Center Predefined2" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;IAM Identity Center → Permission sets; custom&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6ow7ogpy5eo0kp0bl1v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6ow7ogpy5eo0kp0bl1v.png" alt="IAM Identity Center Custom" width="800" height="376"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Pattern 2 — CI/CD deployment role (tooling → workload)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; GitHub Actions, GitLab CI, Jenkins, or CodePipeline deploys infrastructure or applications into workload accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tooling account (CI role)
        |
        | sts:AssumeRole
        v
Workload account (deployment role)
        |
        v
Deploy EC2 / ECS / Lambda / Terraform resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11ox6srmv0neaobyoxug.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11ox6srmv0neaobyoxug.png" alt="CI/CD deployment role trust flow from tooling account to workload account" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Trust policy in the workload account (deployment role)
&lt;/h3&gt;

&lt;p&gt;This role lives in &lt;strong&gt;production&lt;/strong&gt; (or staging) and trusts a &lt;strong&gt;specific role&lt;/strong&gt; in the tooling account, not the whole organization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowToolingDeployRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::111111111111:role/github-actions-deploy"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalOrgID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"o-a1b2c3d4e5"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Permissions policy on the deployment role (example)
&lt;/h3&gt;

&lt;p&gt;Scope to what the pipeline actually does, not &lt;code&gt;AdministratorAccess&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DeployApplicationStack"&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;"ecs:UpdateService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ecs:DescribeServices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:PassRole"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&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;"iam:PassedToService"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ecs-tasks.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Terraform: workload account role
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"tooling_account_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"organization_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"tooling_deploy_role_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github-actions-deploy"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"workload_deploy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"deployment-role"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowToolingDeployRole"&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;AWS&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::${var.tooling_account_id}:role/${var.tooling_deploy_role_name}"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
      &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="s2"&gt;"aws:PrincipalOrgID"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization_id&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One deployment role per environment (&lt;code&gt;deployment-role-prod&lt;/code&gt;, &lt;code&gt;deployment-role-staging&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;CI authenticates to the tooling account first (prefer &lt;strong&gt;OIDC federation&lt;/strong&gt;, not stored keys).&lt;/li&gt;
&lt;li&gt;Pipeline assumes the workload role immediately before deploy steps.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;aws:PrincipalAccount&lt;/code&gt; if multiple accounts exist in the org and you want extra-tight trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; CloudTrail shows &lt;code&gt;AssumeRole&lt;/code&gt; from the tooling role ARN into the workload deployment role, followed only by expected API calls, no stray &lt;code&gt;iam:CreateUser&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3 — Central audit and security read role
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; A security account aggregates CloudTrail, AWS Config, GuardDuty, or Security Hub findings from member accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust flow:&lt;/strong&gt; Member accounts expose a &lt;strong&gt;read-only role&lt;/strong&gt; that the security account (or a security automation role) can assume, or use organization-wide services (Security Hub, GuardDuty delegated admin) where applicable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfzllesomm2xzpufgt7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjfzllesomm2xzpufgt7e.png" alt="Central audit read role flow from member accounts to security account" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example member-account role trust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::999999999999:role/security-read-automation"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalOrgID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"o-a1b2c3d4e5"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attach a read-only policy (&lt;code&gt;SecurityAudit&lt;/code&gt; or tighter custom read).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Security account is for &lt;strong&gt;read and detect&lt;/strong&gt;, not daily deploys.&lt;/li&gt;
&lt;li&gt;Prefer organization-level service delegation (GuardDuty, Security Hub) where available instead of hundreds of custom trust relationships.&lt;/li&gt;
&lt;li&gt;Log every cross-account assumption; alert on unexpected principals.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; A security account can describe Config rules in member accounts but cannot &lt;code&gt;ec2:TerminateInstances&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4 — Cross-account resource access (S3 and KMS)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; A workload in account A must read/write a bucket or key in account B (logs, artifacts, shared datasets).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not:&lt;/strong&gt; Give the compute role in account A &lt;code&gt;s3:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt; and call it done.&lt;/p&gt;

&lt;p&gt;You need &lt;strong&gt;both sides&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identity policy&lt;/strong&gt; on the role in account A (what the role is allowed to request).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource policy&lt;/strong&gt; on the bucket or KMS key in account B (what the resource allows).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw0gnfwlu4iae7gxz9ctt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw0gnfwlu4iae7gxz9ctt.png" alt="Cross-account S3 access requiring identity policy and resource policy on both sides" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  S3 bucket policy (account B)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowWorkloadRoleUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::222222222222:role/app-upload-role"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::shared-artifacts-prod/uploads/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Identity policy (account A, on &lt;code&gt;app-upload-role&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WriteSharedArtifacts"&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;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::shared-artifacts-prod/uploads/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For KMS, include &lt;code&gt;kms:Encrypt&lt;/code&gt;, &lt;code&gt;kms:GenerateDataKey&lt;/code&gt;, and a key policy that names the same principal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Name the &lt;strong&gt;exact role ARN&lt;/strong&gt; in the resource policy; avoid account-root principals unless you truly mean the entire account.&lt;/li&gt;
&lt;li&gt;Scope to prefix (&lt;code&gt;uploads/*&lt;/code&gt;) or specific key aliases.&lt;/li&gt;
&lt;li&gt;Prefer moving the resource closer to the consumer account if daily cross-account access is heavy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verification signal:&lt;/strong&gt; Upload succeeds from the app role; &lt;code&gt;s3:DeleteObject&lt;/code&gt; or access from a different role fails with &lt;code&gt;AccessDenied&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5 — Third-party and vendor access with ExternalId
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use when:&lt;/strong&gt; A SaaS vendor, partner, or integration must assume a role in your account (monitoring, backup, security scanning).&lt;/p&gt;

&lt;p&gt;Always require &lt;code&gt;sts:ExternalId&lt;/code&gt; in the trust policy to mitigate confused deputy problems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F12x1k919aa3u5jmrysv7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F12x1k919aa3u5jmrysv7.png" alt="Third-party vendor assume role flow with ExternalId condition" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::333333333333:root"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&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;"sts:ExternalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-unique-external-id-from-vendor"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Design rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One role per vendor integration; never reuse a vendor role for internal automation.&lt;/li&gt;
&lt;li&gt;Rotate or revoke ExternalId when offboarding the vendor.&lt;/li&gt;
&lt;li&gt;Prefer vendor-specific managed policies over &lt;code&gt;ReadOnlyAccess&lt;/code&gt; if the vendor documents minimum requirements.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  SCPs and permission boundaries: guardrails above IAM
&lt;/h2&gt;

&lt;p&gt;IAM roles express &lt;strong&gt;intent&lt;/strong&gt;. SCPs express &lt;strong&gt;organizational maximums&lt;/strong&gt;. Permission boundaries cap what a role can ultimately grant, even if someone attaches a broader policy (by mistake).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy76t7ubqfkvw2adhmf24.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy76t7ubqfkvw2adhmf24.png" alt="IAM guardrails layered with allow policies, permission boundaries, and SCP deny ceiling" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SCP&lt;/td&gt;
&lt;td&gt;Org-wide deny ceiling&lt;/td&gt;
&lt;td&gt;Deny all actions outside &lt;code&gt;af-south-1&lt;/code&gt; and &lt;code&gt;eu-west-1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permission boundary&lt;/td&gt;
&lt;td&gt;Max permissions for a custom role&lt;/td&gt;
&lt;td&gt;Allow EC2 and ECS, deny IAM and Organizations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session policy&lt;/td&gt;
&lt;td&gt;One-time session scoping&lt;/td&gt;
&lt;td&gt;CLI assume-role with a tighter inline session policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A common SCP pattern for workload accounts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyIAMUserAndAccessKeys"&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;"Deny"&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;"iam:CreateUser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"iam:CreateAccessKey"&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;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;SCPs do not grant access; instead, they filter it. IAM roles still need explicit allow policies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix: which pattern when?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Primary mechanism&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engineer console/CLI access&lt;/td&gt;
&lt;td&gt;Identity Center&lt;/td&gt;
&lt;td&gt;Permission sets + account assignments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipeline deploys to prod&lt;/td&gt;
&lt;td&gt;CI/CD deployment role&lt;/td&gt;
&lt;td&gt;Cross-account &lt;code&gt;AssumeRole&lt;/code&gt; + OIDC in tooling account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security team visibility&lt;/td&gt;
&lt;td&gt;Audit read role&lt;/td&gt;
&lt;td&gt;Cross-account read + delegated security services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App writes to shared bucket&lt;/td&gt;
&lt;td&gt;Resource + identity policy&lt;/td&gt;
&lt;td&gt;S3/KMS policies naming specific role ARNs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor integration&lt;/td&gt;
&lt;td&gt;Vendor assume-role&lt;/td&gt;
&lt;td&gt;Trust policy + &lt;code&gt;sts:ExternalId&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prevent IAM users org-wide&lt;/td&gt;
&lt;td&gt;SCP guardrail&lt;/td&gt;
&lt;td&gt;Deny &lt;code&gt;iam:CreateUser&lt;/code&gt; / &lt;code&gt;CreateAccessKey&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How to verify your design
&lt;/h2&gt;

&lt;p&gt;Run this review before calling the model "done":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No long-lived IAM user keys&lt;/strong&gt; in workload or tooling accounts for automation.&lt;/li&gt;
&lt;li&gt;Every cross-account trust policy names a &lt;strong&gt;specific principal ARN&lt;/strong&gt; (or Identity Center pattern), not &lt;code&gt;*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; (or &lt;code&gt;aws:PrincipalAccount&lt;/code&gt;) appears on cross-account trust where possible.&lt;/li&gt;
&lt;li&gt;CloudTrail logs show &lt;strong&gt;who assumed which role&lt;/strong&gt; and which API calls followed.&lt;/li&gt;
&lt;li&gt;A negative test fails as expected (for example, staging deploy role cannot assume production role).&lt;/li&gt;
&lt;li&gt;SCP deny rules block the risk you fear most (root usage, unapproved regions, public S3 ACLs).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Quick CLI negative test (deployment roles)
&lt;/h3&gt;

&lt;p&gt;From the tooling role session, attempt to assume a role that you should not reach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts assume-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-arn&lt;/span&gt; arn:aws:iam::PROD_ACCOUNT_ID:role/deployment-role &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role-session-name&lt;/span&gt; deploy-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repeat with the &lt;strong&gt;allowed&lt;/strong&gt; workload role and confirm success. Then run a single deploy action and confirm that CloudTrail captures the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this breaks down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Account vending without role standards:&lt;/strong&gt; New accounts get bespoke admin users because there is no template for trust policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Role chaining depth:&lt;/strong&gt; Chaining multiple &lt;code&gt;AssumeRole&lt;/code&gt; calls hits session limits and makes debugging painful; keep chains shallow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overloaded deployment roles:&lt;/strong&gt; One role deploys EC2, edits IAM, and reads Secrets Manager; incident response becomes guesswork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCP vs IAM confusion:&lt;/strong&gt; Teams think an IAM &lt;code&gt;Allow&lt;/code&gt; fixes an SCP &lt;code&gt;Deny&lt;/code&gt;; it does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing break-glass:&lt;/strong&gt; Identity Center power access with no emergency process leads to shared root usage in outages.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I use IAM users at all in member accounts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For human access, no, use IAM Identity Center. Limited exceptions exist for break-glass automation accounts with strict controls, but that is not the default pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is &lt;code&gt;AdministratorAccess&lt;/code&gt; ever okay for deployment roles?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rarely in mature environments. Start tight; broaden only with recorded justification. Admin deploy roles turn pipeline compromise into organization compromise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Do I need a separate tooling account?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not on day one, but CI/CD roles benefit from isolation. Many teams start with a &lt;code&gt;shared-services&lt;/code&gt; account and split later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How does this relate to AWS Control Tower?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Control Tower encodes opinionated OU and account factory patterns. The role designs here still apply; Control Tower helps enforce account baseline and guardrails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned designing roles across accounts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trust policies are the real contract&lt;/strong&gt; between accounts; review them like production code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditions are cheap insurance&lt;/strong&gt;; &lt;code&gt;aws:PrincipalOrgID&lt;/code&gt; stops a surprising amount of cross-account trust mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Readable role names&lt;/strong&gt; (&lt;code&gt;deployment-role-prod&lt;/code&gt;, &lt;code&gt;security-read-member&lt;/code&gt;) beat generic &lt;code&gt;cross-account-role&lt;/code&gt; when auditors and on-call engineers ask questions at 2 a.m.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html" rel="noopener noreferrer"&gt;IAM Identity Center&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html" rel="noopener noreferrer"&gt;IAM roles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies-cross-account-resource-access.html" rel="noopener noreferrer"&gt;Cross-account resource access in IAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html" rel="noopener noreferrer"&gt;AWS Organizations SCPs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html" rel="noopener noreferrer"&gt;IAM permission boundaries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html" rel="noopener noreferrer"&gt;Confused deputy problem (ExternalId)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html" rel="noopener noreferrer"&gt;Security best practices in IAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html" rel="noopener noreferrer"&gt;AWS Well-Architected Framework — Security pillar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>iam</category>
      <category>aws</category>
      <category>security</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Terraform EC2 and S3 Upload Access: A Simple IAM Role Setup Without Access Keys</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Sun, 21 Jun 2026 07:05:28 +0000</pubDate>
      <link>https://dev.to/rufilboss/terraform-ec2-and-s3-upload-access-a-simple-iam-role-setup-without-access-keys-4k99</link>
      <guid>https://dev.to/rufilboss/terraform-ec2-and-s3-upload-access-a-simple-iam-role-setup-without-access-keys-4k99</guid>
      <description>&lt;p&gt;You need a small virtual machine that can upload files to object storage. The fast-but-wrong path is to create an Identity and Access Management (IAM) access key, SSH into the box, and paste credentials into &lt;code&gt;~/.aws/credentials&lt;/code&gt;. That works once, then those keys live on disk, get copied into scripts, and eventually leak.&lt;/p&gt;

&lt;p&gt;In this article, you will use Terraform to launch an Elastic Compute Cloud (EC2) instance and attach an IAM role so the instance can upload to one Simple Storage Service (S3) bucket without storing AWS access keys on the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Developers and DevOps engineers learning Terraform on AWS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you will build:&lt;/strong&gt; One EC2 instance, one private S3 bucket, one IAM role with least-privilege upload permissions, and a working &lt;code&gt;aws s3 cp&lt;/code&gt; test from the instance.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;An AWS account with permissions to create EC2, S3, IAM, and security groups&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.hashicorp.com/terraform/install" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; 1.5+ installed locally&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;AWS CLI&lt;/a&gt; configured locally (&lt;code&gt;aws configure&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;An EC2 key pair already created in your target AWS Region (for SSH access)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Versions used in this tutorial:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform &lt;code&gt;1.5+&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AWS provider &lt;code&gt;~&amp;gt; 5.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Amazon Linux 2023 AMI&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Do not put IAM access keys on EC2 for S3 uploads; use an IAM instance profile instead.&lt;/li&gt;
&lt;li&gt;Create the S3 bucket first, then attach a role policy scoped to that bucket and an &lt;code&gt;uploads/&lt;/code&gt; prefix.&lt;/li&gt;
&lt;li&gt;Launch EC2 in your default virtual private cloud (VPC) with a security group that allows SSH only from your IP.&lt;/li&gt;
&lt;li&gt;Verify access from the instance with &lt;code&gt;aws s3 cp&lt;/code&gt; credentials that come from instance metadata automatically.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform destroy&lt;/code&gt; when finished to avoid ongoing EC2 charges.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why putting access keys on EC2 fails
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Why teams use it&lt;/th&gt;
&lt;th&gt;Why it breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IAM user access keys on the server&lt;/td&gt;
&lt;td&gt;Fast to test uploads&lt;/td&gt;
&lt;td&gt;Keys sit on disk, are hard to rotate, and expand blast radius if the instance is compromised&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overly broad IAM policy (&lt;code&gt;s3:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Fewer permission errors during setup&lt;/td&gt;
&lt;td&gt;One compromised instance can read, delete, or overwrite unrelated buckets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual console clicks only&lt;/td&gt;
&lt;td&gt;No infrastructure as code (IaC) history&lt;/td&gt;
&lt;td&gt;Drift, no repeatable setup, hard to audit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The EC2 instance should assume an IAM role at boot. AWS injects short-lived credentials through the instance metadata service. Your Terraform code documents exactly what the instance can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are building
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your laptop (Terraform)
        |
        v
   AWS API calls
        |
        +--&amp;gt; S3 bucket (uploads/ prefix)
        |
        +--&amp;gt; IAM role + instance profile
        |
        +--&amp;gt; EC2 instance (assumes role, uploads via AWS CLI)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F838etnc5xvlg68uh9thy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F838etnc5xvlg68uh9thy.png" alt="Architecture flow: Terraform provisions EC2 with IAM role for S3 uploads" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create the project layout
&lt;/h2&gt;

&lt;p&gt;Create a new folder and four 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="nb"&gt;mkdir &lt;/span&gt;terraform-ec2-s3-upload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;terraform-ec2-s3-upload
&lt;span class="nb"&gt;touch &lt;/span&gt;versions.tf variables.tf main.tf outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your layout should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform-ec2-s3-upload/
  versions.tf
  variables.tf
  main.tf
  outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Pin provider versions
&lt;/h2&gt;

&lt;p&gt;Add provider constraints in &lt;code&gt;versions.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;= 1.5.0"&lt;/span&gt;

  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;aws&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/aws"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"aws"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Define input variables
&lt;/h2&gt;

&lt;p&gt;Add tunable values in &lt;code&gt;variables.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"aws_region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS region to deploy resources"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"project_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Prefix for resource names"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tf-ec2-s3-demo"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_type"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"EC2 instance type"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t3.micro"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"key_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Existing EC2 key pair name in this region"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"ssh_cidr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Your public IP in CIDR format for SSH access (example: 203.0.113.10/32)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;terraform.tfvars&lt;/code&gt; file with your values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="nx"&gt;key_name&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-key-pair-name"&lt;/span&gt;
&lt;span class="nx"&gt;ssh_cidr&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_PUBLIC_IP/32"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To find your public IP:&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;-s&lt;/span&gt; https://checkip.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Append &lt;code&gt;/32&lt;/code&gt; to that IP for &lt;code&gt;ssh_cidr&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe85ld0yj6n62k77vqs7s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe85ld0yj6n62k77vqs7s.png" alt="public_ip" width="798" height="54"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Create the S3 bucket
&lt;/h2&gt;

&lt;p&gt;Add the bucket and hardening settings in &lt;code&gt;main.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"uploads"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-${data.aws_caller_identity.current.account_id}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_public_access_block"&lt;/span&gt; &lt;span class="s2"&gt;"uploads"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;block_public_acls&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;block_public_policy&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;ignore_public_acls&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;restrict_public_buckets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"current"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bucket name includes your account ID to reduce naming collisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Create IAM role and least-privilege S3 policy
&lt;/h2&gt;

&lt;p&gt;Still in &lt;code&gt;main.tf&lt;/code&gt;, define what the EC2 instance is allowed to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_assume_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nx"&gt;principals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Service"&lt;/span&gt;
      &lt;span class="nx"&gt;identifiers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ec2.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_upload_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-ec2-role"&lt;/span&gt;
  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_assume_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy_document"&lt;/span&gt; &lt;span class="s2"&gt;"s3_upload_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ListBucketPrefix"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:ListBucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;test&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StringLike"&lt;/span&gt;
      &lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3:prefix"&lt;/span&gt;
      &lt;span class="nx"&gt;values&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"uploads/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;statement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"UploadObjectsOnly"&lt;/span&gt;
    &lt;span class="nx"&gt;effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"${aws_s3_bucket.uploads.arn}/uploads/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_policy"&lt;/span&gt; &lt;span class="s2"&gt;"s3_upload_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-s3-upload"&lt;/span&gt;
  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_iam_policy_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_upload_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"attach_upload_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_upload_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_upload_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_instance_profile"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_profile"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-instance-profile"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_upload_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This role can upload only to &lt;code&gt;uploads/*&lt;/code&gt; inside one bucket. It cannot delete objects or read unrelated buckets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Use default VPC networking and security group
&lt;/h2&gt;

&lt;p&gt;For a beginner-friendly setup, use the default VPC and one public subnet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnets"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc-id"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_sg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow SSH from your IP only"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSH from admin IP"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssh_cidr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="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;h2&gt;
  
  
  Step 7: Launch the EC2 instance
&lt;/h2&gt;

&lt;p&gt;Add the Amazon Linux 2023 AMI lookup and EC2 instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ami"&lt;/span&gt; &lt;span class="s2"&gt;"amazon_linux_2023"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;most_recent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;owners&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"amazon"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"al2023-ami-*-kernel-*-x86_64"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"virtualization-type"&lt;/span&gt;
    &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hvm"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ami&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amazon_linux_2023&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_subnets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_sg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key_name&lt;/span&gt;
  &lt;span class="nx"&gt;iam_instance_profile&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_instance_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
              #!/bin/bash
              dnf update -y
              dnf install -y awscli
&lt;/span&gt;&lt;span class="no"&gt;              EOF

&lt;/span&gt;  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project_name}-ec2"&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;Add outputs in &lt;code&gt;outputs.tf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"ec2_public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Public IP for SSH"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_ip&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"s3_bucket_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Target bucket for uploads"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uploads&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"ssh_command"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSH command example"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssh -i ~/.ssh/${var.key_name}.pem ec2-user@${aws_instance.app.public_ip}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 8: Initialize and apply Terraform
&lt;/h2&gt;

&lt;p&gt;Run Terraform from the project folder:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;terraform init&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdj51k9szn2n1q05im1m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftdj51k9szn2n1q05im1m.png" alt="Terraform init" width="799" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;terraform plan&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnhrr7brh749ctwxmhil.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnhrr7brh749ctwxmhil.png" alt="Terraform Plan" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;terraform apply; Type &lt;code&gt;yes&lt;/code&gt; when prompted.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayl5ll7kj23zzyooddwk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayl5ll7kj23zzyooddwk.png" alt="Terraform apply" width="800" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F40no3iwfgs9rcidb88zv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F40no3iwfgs9rcidb88zv.png" alt="Terraform Applied" width="800" height="575"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instance running:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdwgf3uomw76849cyifs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdwgf3uomw76849cyifs.png" alt="Success!" width="797" height="107"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 9: SSH into the instance
&lt;/h2&gt;

&lt;p&gt;Use the output SSH command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/your-key-pair-name.pem ec2-user@&amp;lt;EC2_PUBLIC_IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the connection fails, confirm:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ssh_cidr&lt;/code&gt; matches your current public IP.&lt;/li&gt;
&lt;li&gt;Your key file permissions are &lt;code&gt;chmod 400&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The instance status is &lt;code&gt;running&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ip6abfq5qp6h0hkdv2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ip6abfq5qp6h0hkdv2q.png" alt="successful SSH session" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Verify S3 upload from EC2
&lt;/h2&gt;

&lt;p&gt;On the EC2 instance, confirm AWS CLI identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the role ARN for &lt;code&gt;${project_name}-ec2-role&lt;/code&gt;, not an IAM user.&lt;/p&gt;

&lt;p&gt;Upload a test file:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"upload test &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/upload-test.txt
aws s3 &lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/upload-test.txt s3://&amp;lt;BUCKET_NAME&amp;gt;/uploads/upload-test.txt
aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://&amp;lt;BUCKET_NAME&amp;gt;/uploads/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;BUCKET_NAME&amp;gt;&lt;/code&gt; with the &lt;code&gt;s3_bucket_name&lt;/code&gt; Terraform output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc75lni4ap4iacjjwuyxl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc75lni4ap4iacjjwuyxl.png" alt="s3-upload-success" width="800" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Filhpmr27a3y0pvh6c2zk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Filhpmr27a3y0pvh6c2zk.png" alt="08-s3-console-object" width="799" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify this works
&lt;/h2&gt;

&lt;p&gt;Run this quick checklist:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;terraform output&lt;/code&gt; shows &lt;code&gt;ec2_public_ip&lt;/code&gt; and &lt;code&gt;s3_bucket_name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws sts get-caller-identity&lt;/code&gt; on EC2 returns the instance role ARN.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws s3 cp&lt;/code&gt; to &lt;code&gt;uploads/&lt;/code&gt; succeeds from EC2.&lt;/li&gt;
&lt;li&gt;Upload to a different prefix (for example, &lt;code&gt;secrets/&lt;/code&gt;) fails with &lt;code&gt;AccessDenied&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;No access keys exist in &lt;code&gt;/home/ec2-user/.aws/credentials&lt;/code&gt; on the instance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last check confirms you are using role-based access, not static keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean up to avoid charges
&lt;/h2&gt;

&lt;p&gt;When you finish testing:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhf69y2nvf7o6uj4u2i32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhf69y2nvf7o6uj4u2i32.png" alt="Destroying resources" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Type &lt;code&gt;yes&lt;/code&gt; to remove EC2, IAM, and S3 resources created by this tutorial.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fks2ehftolssuvh5vkhix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fks2ehftolssuvh5vkhix.png" alt="Destroy complete!" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When this breaks down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Default VPC missing:&lt;/strong&gt; Some AWS accounts disable the default VPC. You will need to create a VPC and public subnet first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH blocked by IP change:&lt;/strong&gt; If your home IP changes, update &lt;code&gt;ssh_cidr&lt;/code&gt; and run &lt;code&gt;terraform apply&lt;/code&gt; again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Too open security groups:&lt;/strong&gt; Never set SSH to &lt;code&gt;0.0.0.0/0&lt;/code&gt; in real environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy too broad:&lt;/strong&gt; &lt;code&gt;s3:*&lt;/code&gt; on &lt;code&gt;*&lt;/code&gt; is convenient for demos but unsafe for production workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No remote state backend:&lt;/strong&gt; This tutorial uses local Terraform state. For team use, move state to S3 with locking (DynamoDB).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Why not attach an IAM user access key to EC2?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Access keys are long-lived secrets. IAM roles provide temporary credentials that rotate automatically and are tied to the instance lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can this instance read objects from S3?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not with the policy in this tutorial. It can upload to &lt;code&gt;uploads/*&lt;/code&gt; only. Add explicit &lt;code&gt;s3:GetObject&lt;/code&gt; permissions only if your workload needs reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is this production-ready?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is a strong learning baseline, not a production baseline. Production setups usually add private subnets, Systems Manager (SSM) Session Manager instead of public SSH, encryption, logging, and tighter network controls.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;IAM instance profiles are the default pattern for EC2-to-AWS service access.&lt;/li&gt;
&lt;li&gt;Prefix-scoped S3 policies reduce damage if an instance is compromised.&lt;/li&gt;
&lt;li&gt;Terraform makes role and bucket permissions reviewable in pull requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project on GitHub
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/rufilboss/terraform-ec2-s3-iam-role" rel="noopener noreferrer"&gt;Terraform project on GitHub&lt;/a&gt; with EC2, IAM role, and S3 upload policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs" rel="noopener noreferrer"&gt;Terraform AWS Provider documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html" rel="noopener noreferrer"&gt;IAM roles for Amazon EC2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_examples_s3.html" rel="noopener noreferrer"&gt;Granting permissions to access Amazon S3 resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/security-best-practices.html" rel="noopener noreferrer"&gt;Amazon S3 security best practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html" rel="noopener noreferrer"&gt;Instance metadata and IAM roles&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
      <category>ec2</category>
      <category>s3</category>
    </item>
    <item>
      <title>Secret Scanning in CI: What Pre-Commit, Pull Request, and Main Branch Each Actually Catch</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Wed, 17 Jun 2026 11:32:18 +0000</pubDate>
      <link>https://dev.to/rufilboss/secret-scanning-in-ci-what-pre-commit-pull-request-and-main-branch-each-actually-catch-3c55</link>
      <guid>https://dev.to/rufilboss/secret-scanning-in-ci-what-pre-commit-pull-request-and-main-branch-each-actually-catch-3c55</guid>
      <description>&lt;p&gt;A teammate pastes an AWS access key into a PR comment to "debug quickly." Another commits &lt;code&gt;.env.production&lt;/code&gt; because &lt;code&gt;.gitignore&lt;/code&gt; was wrong on a new microservice. A third rotates nothing after a contractor laptop compromise because "we never committed secrets, probably fine."&lt;/p&gt;

&lt;p&gt;Secret scanners are not interchangeable at every stage of the software development lifecycle (SDLC). Running only on &lt;code&gt;main&lt;/code&gt; means the secret already lived in git history. Running only locally means it never ran on the machine that mattered. &lt;strong&gt;Layers&lt;/strong&gt; matter, and each layer should have a different job.&lt;/p&gt;

&lt;p&gt;In this article, you will implement a practical three-layer secret-scanning model with Gitleaks and GitHub Actions, then verify each layer and handle real incidents without creating scanner fatigue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; Engineers owning GitHub Actions security checks for application repos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt; Pre-commit hook, PR scan, and post-merge history scan with shared allowlist discipline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Git, GitHub repo, optional Gitleaks binary locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use three layers with different goals: pre-commit for fast local feedback, pull request as a merge gate, and scheduled default-branch scans for hygiene.&lt;/li&gt;
&lt;li&gt;Block merges on PR findings; treat default-branch findings as incidents plus historical cleanup.&lt;/li&gt;
&lt;li&gt;Share one &lt;code&gt;.gitleaks.toml&lt;/code&gt; across local and CI runs, and allowlist only specific safe paths.&lt;/li&gt;
&lt;li&gt;Rotation and revocation matter more than history rewrite; deleting git history without credential rotation does not contain compromise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why one scanner in one place fails
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;th&gt;Misses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-commit&lt;/td&gt;
&lt;td&gt;Keys before they enter any remote&lt;/td&gt;
&lt;td&gt;Skipped hooks (&lt;code&gt;--no-verify&lt;/code&gt;), new clones without hooks installed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pull request&lt;/td&gt;
&lt;td&gt;Keys introduced in the diff; blocks merge&lt;/td&gt;
&lt;td&gt;Secrets only in comments, wiki, or release assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default branch/history&lt;/td&gt;
&lt;td&gt;Deep scan, scheduled; finds old leaks&lt;/td&gt;
&lt;td&gt;Still too late for keys already exfiltrated from an open PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Treat findings like production incidents at the PR layer; treat main-branch scans as &lt;strong&gt;hygiene and audit evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1 — Pre-commit (fast feedback)
&lt;/h2&gt;

&lt;p&gt;Start with local feedback so engineers catch leaks before pushing.&lt;br&gt;
Install &lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;Gitleaks&lt;/a&gt; and wire a hook:&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;# .pre-commit-config.yaml&lt;/span&gt;
repos:
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.21.2
    hooks:
      - &lt;span class="nb"&gt;id&lt;/span&gt;: gitleaks
&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;pre-commit &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Developers see failures in under two seconds on staged files. &lt;strong&gt;Allowlist&lt;/strong&gt; test fixtures explicitly, never disable rules globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitleaks.toml (repo root)&lt;/span&gt;
&lt;span class="nn"&gt;[allowlist]&lt;/span&gt;
&lt;span class="py"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;'''^tests/fixtures/'''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;'''^docs/examples/fake-credentials\.json$'''&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; If a path needs a real secret for tests, use a vault-injected env var in CI—not a committed file with "fake" in the name and a real-looking key format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2 — Pull request scan (merge gate)
&lt;/h2&gt;

&lt;p&gt;The PR layer is your enforcement point. Scan &lt;strong&gt;only the PR diff&lt;/strong&gt; so developers are not punished for historical debt on day one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret scan (PR)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitleaks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Gitleaks on PR diff&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;config-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.gitleaks.toml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure branch protection: this check is required before the merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fork PRs:&lt;/strong&gt; &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; from forks is read-only; gitleaks-action still scans the diff but cannot comment with elevated permissions, acceptable for public repos; for private repos, consider org-level secret scanning (GitHub Advanced Security) as a parallel signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3 — Main branch and history (hygiene)
&lt;/h2&gt;

&lt;p&gt;The default-branch layer catches historical debt and supply-chain surprises.&lt;br&gt;
Run a weekly full-history scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Secret scan (history)&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gitleaks-history&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gitleaks/gitleaks-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;config-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.gitleaks.toml&lt;/span&gt;
          &lt;span class="c1"&gt;# no PR context: full repo scan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this fires on &lt;code&gt;main&lt;/code&gt;, assume rotation: revoke the credential, purge from history (&lt;code&gt;git filter-repo&lt;/code&gt; or BFG) only &lt;strong&gt;after&lt;/strong&gt; revocation—scrubbing git without rotating the secret fixes nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to verify this works
&lt;/h2&gt;

&lt;p&gt;Use these checks to confirm each layer is doing the right job:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-commit check:&lt;/strong&gt; add a fake test secret string in a staged file and confirm &lt;code&gt;pre-commit&lt;/code&gt; blocks the commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR gate check:&lt;/strong&gt; Open a test pull request with the same string and confirm the workflow fails before merge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History scan check:&lt;/strong&gt; run &lt;code&gt;workflow_dispatch&lt;/code&gt; for the history workflow and confirm it scans the full repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist check:&lt;/strong&gt; Place the same test token in an allowlisted fixture path and confirm scanner behavior matches your policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use clearly fake values in tests and examples. Never use production-like secrets for scanner testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing false positives without going blind
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist paths, not regexes for "AWS"&lt;/strong&gt; — Broad entropy exceptions hide real keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate example keys:&lt;/strong&gt; Use obviously invalid formats (&lt;code&gt;AKIAFAKE00000000000&lt;/code&gt;) in docs; scanners and humans both win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom rules sparingly:&lt;/strong&gt; Add org-specific patterns (internal API key prefix) via Gitleaks &lt;code&gt;[[rules]]&lt;/code&gt;; do not fork the entire ruleset unless you can maintain it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teach the escape hatch:&lt;/strong&gt; If pre-commit blocks a docs change, fix the example, not &lt;code&gt;SKIP=gitleaks&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When a secret is found anyway
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Revoke&lt;/strong&gt; in the provider (AWS, Stripe, Slack) immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotate&lt;/strong&gt; dependent systems; assume compromise if the repo is public or the PR was open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Purge history&lt;/strong&gt; if the secret touched git; coordinate with legal/comms if customer data access was possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-incident:&lt;/strong&gt; add a rule or allowlist fix so the same mistake is a one-liner next time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pair this scanning pipeline with short-lived cloud credentials (for example, OIDC-based CI federation) so leaked CI identity material has reduced blast radius.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this breaks down
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Teams can bypass local hooks, so pre-commit cannot be your enforcement layer by itself.&lt;/li&gt;
&lt;li&gt;Diff-only PR scans do not catch existing secrets already in history, release assets, or external systems.&lt;/li&gt;
&lt;li&gt;Aggressive allowlists can silently reduce coverage if they are not reviewed during security change management.&lt;/li&gt;
&lt;li&gt;Scanner success can create false confidence if incident response, revocation, and rotation are weak.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use three layers: pre-commit (speed), PR (gate), scheduled main (history).&lt;/li&gt;
&lt;li&gt;Share one &lt;code&gt;.gitleaks.toml&lt;/code&gt;; allowlist paths, not categories of secrets.&lt;/li&gt;
&lt;li&gt;Block merge on PR findings; treat main-branch hits as incident + hygiene work.&lt;/li&gt;
&lt;li&gt;Rotation beats history rewriting; scanning is detection, not prevention alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gitleaks/gitleaks#configuration" rel="noopener noreferrer"&gt;Gitleaks configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning" rel="noopener noreferrer"&gt;GitHub secret scanning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository" rel="noopener noreferrer"&gt;Removing sensitive data from a repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devsecops</category>
      <category>githubactions</category>
      <category>gitleaks</category>
      <category>secrets</category>
    </item>
    <item>
      <title>Amazon S3 Bucket Names Aren’t Always Globally Unique Anymore — Here’s What Changed (and Why I’m Excited)</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Thu, 19 Mar 2026 11:54:35 +0000</pubDate>
      <link>https://dev.to/rufilboss/amazon-s3-bucket-names-arent-always-globally-unique-anymore-heres-what-changed-and-why-im-3a1f</link>
      <guid>https://dev.to/rufilboss/amazon-s3-bucket-names-arent-always-globally-unique-anymore-heres-what-changed-and-why-im-3a1f</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74tcw6c7amqslpu9mfz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F74tcw6c7amqslpu9mfz6.png" alt="Global namespace" width="800" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgj70v2rg5nh1b86zsuyc.png" alt="Account regional namespace" width="800" height="350"&gt;
&lt;/h2&gt;

&lt;p&gt;I still remember the first time S3 humbled me.&lt;/p&gt;

&lt;p&gt;I was following a “hello AWS” tutorial, feeling confident, and then S3 hit me with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Bucket name already exists&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried again. And again. And again.&lt;br&gt;&lt;br&gt;
Different names, different variations, still taken!&lt;/p&gt;

&lt;p&gt;That’s when I learned the “classic rule”:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;S3 bucket names are globally unique&lt;/strong&gt; (within an AWS partition).&lt;br&gt;&lt;br&gt;
Meaning: if someone anywhere already owns that bucket name, you can’t have it.&lt;/p&gt;

&lt;p&gt;But here’s the big update I recently learned, and it’s &lt;em&gt;huge&lt;/em&gt; for builders, platform teams, and security folks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon S3 now supports creating general-purpose buckets inside an “account regional namespace”, which removes the “find a globally unique name” pain.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWS announced this on &lt;strong&gt;Mar 12, 2026&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-s3-account-regional-namespaces/&lt;/code&gt;&lt;/p&gt;



&lt;p&gt;In this article, I’ll break down what changed, how it works, why it matters, and how I’d explain it if I were designing bucket naming at scale.&lt;/p&gt;
&lt;h2&gt;
  
  
  What used to be true (and is still true by default)
&lt;/h2&gt;

&lt;p&gt;By default, &lt;strong&gt;general purpose buckets exist in a global namespace&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;AWS puts it plainly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;“Each bucket name must be unique across all AWS accounts in all the AWS Regions within a partition.”&lt;/strong&gt;
Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;my-company-logs&lt;/code&gt; can only exist once (in that partition).&lt;/li&gt;
&lt;li&gt;If the bucket is deleted, the name becomes available again — and someone else could recreate it later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwy76sxvn8ixkt227rjv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwy76sxvn8ixkt227rjv.png" alt="Bucket already exists" width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What changed: Account regional namespaces (Mar 2026)
&lt;/h2&gt;

&lt;p&gt;AWS introduced a second option:&lt;/p&gt;

&lt;p&gt;You can now create a &lt;strong&gt;general purpose bucket&lt;/strong&gt; in a namespace reserved only for your account:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“You can also choose to create a bucket in your account regional namespace. Your account regional namespace is a subdivision of the global namespace that only your account can create buckets in.”&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AWS also summarizes the “why” in the announcement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;eliminates the need to find globally unique bucket names
&lt;/li&gt;
&lt;li&gt;makes it easier to build workloads like “one bucket per customer/team/dataset”
&lt;/li&gt;
&lt;li&gt;can be enforced using IAM policies/service control policies (SCPs)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Announcement: &lt;code&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-s3-account-regional-namespaces/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The nuance I want to keep clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Global namespace is still the default&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;But now you have a &lt;strong&gt;reserved&lt;/strong&gt; namespace you can opt into&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How the naming looks (the new naming convention)
&lt;/h2&gt;

&lt;p&gt;Buckets in your &lt;strong&gt;account regional namespace&lt;/strong&gt; follow this naming pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bucket-name-prefix-accountId-region-an
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amzn-s3-demo-bucket-111122223333-us-west-2-an
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So instead of fighting to get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my-company-logs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…you can choose a predictable prefix like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my-company-logs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the final name will include your account + region suffix that makes it unique &lt;em&gt;to you&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb18tnn2rnv6102zmjs1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftb18tnn2rnv6102zmjs1.png" alt=" " width="800" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters (in real systems)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) It kills the “I spent 20 minutes naming a bucket” problem
&lt;/h3&gt;

&lt;p&gt;If you’ve ever created buckets for different environments, you know this loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;myapp-prod-logs&lt;/code&gt; (taken)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myapp-prod-logs-1&lt;/code&gt; (taken)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myapp-prod-logs-2026&lt;/code&gt; (taken)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;myapp-prod-logs-&amp;lt;random&amp;gt;&lt;/code&gt; (finally works, but now it’s ugly)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Account regional namespaces support a much more predictable naming scheme.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) It reduces risk after deletion (a security win)
&lt;/h3&gt;

&lt;p&gt;In the shared global namespace, once a bucket is deleted, the name becomes available again. Another account can recreate the name later and potentially receive requests intended for that name.&lt;/p&gt;

&lt;p&gt;Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In contrast, for account regional namespace buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only your account can create buckets in that namespace&lt;/li&gt;
&lt;li&gt;Another account cannot recreate your account-regional bucket name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3) It scales for bucket-per-customer / bucket-per-team designs
&lt;/h3&gt;

&lt;p&gt;AWS explicitly calls out bucket-per-customer/team/dataset workloads in the announcement:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-s3-account-regional-namespaces/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That’s a very modern SaaS/platform pattern, and it’s nice to see S3 making it easier to do it safely and predictably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating an account-regional namespace bucket (AWS CLI)
&lt;/h2&gt;

&lt;p&gt;The official docs show creating a bucket using the AWS CLI like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api create-bucket &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket&lt;/span&gt; amzn-s3-demo-bucket-012345678910-us-west-1-an &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--bucket-namespace&lt;/span&gt; account-regional &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--create-bucket-configuration&lt;/span&gt; &lt;span class="nv"&gt;LocationConstraint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-west-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Before you run the command, make sure your &lt;code&gt;aws cli&lt;/code&gt; command is updated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How I’d enforce this in an organization (IAM/SCP policy)
&lt;/h2&gt;

&lt;p&gt;If I were working on a platform team, I’d want consistent naming and fewer security foot-guns. AWS provides a clean enforcement pattern using the condition key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s3:x-amz-bucket-namespace&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s an example IAM policy from the docs that denies &lt;code&gt;s3:CreateBucket&lt;/code&gt; unless the request is using &lt;code&gt;account-regional&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RequireAccountRegionalBucketCreation"&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;"Deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:CreateBucket"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringNotEquals"&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;"s3:x-amz-bucket-namespace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"account-regional"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bucket naming rules (still important)
&lt;/h2&gt;

&lt;p&gt;Even with namespaces, S3 bucket names still follow the general naming rules, like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;3–63 characters
&lt;/li&gt;
&lt;li&gt;lowercase letters, numbers, periods (&lt;code&gt;.&lt;/code&gt;), and hyphens (&lt;code&gt;-&lt;/code&gt;) only
&lt;/li&gt;
&lt;li&gt;must start and end with a letter or number
&lt;/li&gt;
&lt;li&gt;no two adjacent periods
&lt;/li&gt;
&lt;li&gt;cannot look like an IP address
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official rules: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Also note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-an&lt;/code&gt; is reserved for account regional namespace buckets
&lt;/li&gt;
&lt;li&gt;the account/regional suffix counts toward the 63-character limit
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Official docs: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model I use
&lt;/h2&gt;

&lt;p&gt;This is how I explain it to myself:&lt;/p&gt;

&lt;h3&gt;
  
  
  Global namespace (default)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Like a global username system
&lt;/li&gt;
&lt;li&gt;If someone owns it, you can’t use it
&lt;/li&gt;
&lt;li&gt;If deleted, someone else might grab it later
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Account regional namespace (new)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Like a reserved “username space” under &lt;em&gt;my account + my region&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;I still choose the prefix, but uniqueness is guaranteed by the suffix
&lt;/li&gt;
&lt;li&gt;Another account can’t recreate my account-regional bucket name
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;When I first learned S3 bucket names had to be globally unique, I accepted it as “just an AWS thing.”&lt;/p&gt;

&lt;p&gt;But account regional namespaces feel like AWS responding to how people actually build today: lots of environments, lots of teams, and a need for predictable and secure naming.&lt;/p&gt;

&lt;p&gt;If I’m building something new, I’d strongly consider defaulting new buckets to the account regional namespace — especially if naming conventions matter, or if I want to reduce the risk of name reuse after deletion.&lt;/p&gt;




&lt;h2&gt;
  
  
  References (official)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS announcement (Mar 12, 2026): &lt;code&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/amazon-s3-account-regional-namespaces/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Namespaces for general purpose buckets: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;General purpose bucket naming rules: &lt;code&gt;https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built a zero-cost end-to-end DevOps pipeline (GitHub Actions + Docker + Kubernetes + Docker Hub)</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Wed, 04 Mar 2026 11:27:47 +0000</pubDate>
      <link>https://dev.to/rufilboss/i-built-a-zero-cost-end-to-end-devops-pipeline-github-actions-docker-kubernetes-docker-hub-2g9n</link>
      <guid>https://dev.to/rufilboss/i-built-a-zero-cost-end-to-end-devops-pipeline-github-actions-docker-kubernetes-docker-hub-2g9n</guid>
      <description>&lt;p&gt;I just finished a small but &lt;strong&gt;real&lt;/strong&gt; DevOps project and I want to share it in case you’re trying to build your own portfolio.&lt;/p&gt;

&lt;p&gt;The idea was simple: &lt;strong&gt;take a tiny app and wire the whole path from &lt;code&gt;git push&lt;/code&gt; → CI/CD → container registry → Kubernetes&lt;/strong&gt;, without paying for any cloud resources.&lt;/p&gt;

&lt;p&gt;You can grab the code here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub repo&lt;/strong&gt;: &lt;code&gt;https://github.com/rufilboss/devops-e2e-pipeline&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Hub image&lt;/strong&gt;: &lt;code&gt;docker.io/asruf/demo-app:latest&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I built (high level)
&lt;/h2&gt;

&lt;p&gt;Concretely, the project contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App&lt;/strong&gt;: Tiny Flask API (&lt;code&gt;app/main.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container&lt;/strong&gt;: Dockerfile (&lt;code&gt;app/Dockerfile&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: GitHub Actions workflow that builds and pushes images to &lt;strong&gt;Docker Hub&lt;/strong&gt; (&lt;code&gt;.github/workflows/ci-cd.yaml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt;: &lt;code&gt;Deployment&lt;/code&gt; + &lt;code&gt;Service&lt;/code&gt; (&lt;code&gt;k8s/*.yaml&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform (optional)&lt;/strong&gt;: creates the Kubernetes namespace (&lt;code&gt;terraform/*.tf&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything here runs &lt;strong&gt;for free&lt;/strong&gt; on a local cluster (kind or minikube) and a free Docker Hub + GitHub account.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites I used
&lt;/h2&gt;

&lt;p&gt;To follow exactly what I did, you’ll want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git + GitHub repo&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;One local Kubernetes option:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;kind&lt;/strong&gt; (what I used), or&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;minikube&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Terraform (optional, only for the IaC part)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Docker Hub&lt;/strong&gt; account (I used mine &lt;code&gt;asruf&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Project layout
&lt;/h2&gt;

&lt;p&gt;This is the layout of the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;devops-e2e-pipeline/
├── app
│   ├── Dockerfile
│   ├── main.py
│   └── requirements.txt
├── k8s
│   ├── namespace.yaml
│   └── deployment.yaml
├── terraform
│   ├── main.tf
│   └── k8s.tf
└── .github
    └── workflows
        └── ci-cd.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1) The app I used (simple Flask service)
&lt;/h2&gt;

&lt;p&gt;I deliberately kept the app tiny so the focus is on the &lt;strong&gt;pipeline&lt;/strong&gt;, not the code.&lt;/p&gt;

&lt;p&gt;It exposes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; — info about the service (name, version, env, status)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/health&lt;/code&gt; — liveness&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/ready&lt;/code&gt; — readiness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;app/main.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APP_VERSION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ENV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ENV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dev&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;demo-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VERSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;env&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/requirements.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flask&amp;gt;=3.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app listens on &lt;strong&gt;port 8080&lt;/strong&gt;, which I re-use everywhere (Docker, Kubernetes, port-forward, etc.).&lt;/p&gt;




&lt;h2&gt;
  
  
  2) Containerizing it with Docker
&lt;/h2&gt;

&lt;p&gt;My Dockerfile is intentionally straightforward but shows some basic good practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slim base image&lt;/li&gt;
&lt;li&gt;Non-root user&lt;/li&gt;
&lt;li&gt;Requirements installed in their own layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;app/Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runtime&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;adduser &lt;span class="nt"&gt;--disabled-password&lt;/span&gt; &lt;span class="nt"&gt;--gecos&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; appuser

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.lock

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; main.py .&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8080&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; FLASK_APP=main.py&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "-m", "flask", "run", "--host=0.0.0.0", "--port=8080"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local sanity check
&lt;/h3&gt;

&lt;p&gt;From the repo root:&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;devops-e2e-pipeline

docker build &lt;span class="nt"&gt;-t&lt;/span&gt; demo-app:local ./app
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="nt"&gt;--name&lt;/span&gt; demo-app-test demo-app:local

&lt;span class="c"&gt;# In another terminal:&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/health
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{"status": "healthy"}&lt;/code&gt; from &lt;code&gt;/health&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{"env":"dev","service":"demo-app","status":"ok","version":"1.0.0"}&lt;/code&gt; from &lt;code&gt;/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that worked, I moved on to Kubernetes.&lt;/p&gt;

&lt;p&gt;Here are the screenshots from my terminal while doing this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdz7asuyn8phgm8tz7piu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdz7asuyn8phgm8tz7piu.png" alt="Building the Docker image" width="799" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm6w172zyapvk1uyibduu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm6w172zyapvk1uyibduu.png" alt="Running the container locally" width="799" height="102"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0uuxyouuk346jcxvb9a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0uuxyouuk346jcxvb9a.png" alt="Health endpoint response" width="800" height="26"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx7fl0934mpl26gn41mc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flx7fl0934mpl26gn41mc.png" alt="Root endpoint response" width="800" height="26"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Running it on Kubernetes (kind or minikube)
&lt;/h2&gt;

&lt;p&gt;I wanted a “real” deployment with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dedicated namespace&lt;/li&gt;
&lt;li&gt;2 replicas&lt;/li&gt;
&lt;li&gt;Liveness/readiness probes&lt;/li&gt;
&lt;li&gt;Resource requests/limits&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Starting a local cluster
&lt;/h3&gt;

&lt;p&gt;You can use either tool; I used &lt;strong&gt;kind&lt;/strong&gt;, but here are both options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;minikube:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;kind:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what that looked like for me:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdnjafp0jk4dxf1mmys8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdnjafp0jk4dxf1mmys8.png" alt="kind create cluster output" width="799" height="147"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the image visible to the cluster
&lt;/h3&gt;

&lt;p&gt;Kubernetes can’t automatically see &lt;code&gt;demo-app:local&lt;/code&gt; unless you either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;build inside the cluster’s Docker daemon (minikube), or&lt;/li&gt;
&lt;li&gt;load the image into kind.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Option A: minikube&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;minikube docker-env&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; demo-app:local ./app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B: kind&lt;/strong&gt; (what I used):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; demo-app:local ./app
kind load docker-image demo-app:local &lt;span class="nt"&gt;--name&lt;/span&gt; demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the &lt;code&gt;kind load&lt;/code&gt; output:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdbfv6dlehglwfn6cqo6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpdbfv6dlehglwfn6cqo6.png" alt="kind load docker-image output" width="800" height="26"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes manifests I used
&lt;/h3&gt;

&lt;p&gt;Namespace:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;k8s/namespace.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Namespace&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deployment + Service:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;k8s/deployment.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
          &lt;span class="c1"&gt;# Local image for kind/minikube:&lt;/span&gt;
          &lt;span class="c1"&gt;# image: demo-app:local&lt;/span&gt;
          &lt;span class="c1"&gt;# Docker Hub image (asruf/demo-app) when using CI/CD:&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io/asruf/demo-app:latest&lt;/span&gt;
          &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
          &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENV&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production"&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;APP_VERSION&lt;/span&gt;
              &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0"&lt;/span&gt;
          &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;50m&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;64Mi&lt;/span&gt;
            &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;200m&lt;/span&gt;
              &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;128Mi&lt;/span&gt;
          &lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/health&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
            &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
            &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
          &lt;span class="na"&gt;readinessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/ready&lt;/span&gt;
              &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
            &lt;span class="na"&gt;initialDelaySeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
            &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterIP&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TCP&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo-app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Applying and testing
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/namespace.yaml
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/deployment.yaml

kubectl get pods,svc &lt;span class="nt"&gt;-n&lt;/span&gt; demo-app

kubectl port-forward &lt;span class="nt"&gt;-n&lt;/span&gt; demo-app svc/demo-app 8080:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in another terminal:&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;-s&lt;/span&gt; http://localhost:8080/health
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/ready
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:8080/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the &lt;code&gt;kubectl apply&lt;/code&gt; / &lt;code&gt;kubectl get&lt;/code&gt; snapshot:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkp1it3q5vhwkqt8b42ri.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkp1it3q5vhwkqt8b42ri.png" alt="kubectl apply/get output" width="799" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the port-forward:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F57pxhpz1syi1jw50frhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F57pxhpz1syi1jw50frhy.png" alt="kubectl port-forward output" width="800" height="58"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, I had the app running as &lt;strong&gt;2 replicas in a local cluster&lt;/strong&gt;, fronted by a Service, with working probes.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Pushing to Docker Hub
&lt;/h2&gt;

&lt;p&gt;My Docker Hub username is &lt;strong&gt;&lt;code&gt;asruf&lt;/code&gt;&lt;/strong&gt;. I first pushed manually to make sure everything worked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker tag demo-app:local asruf/demo-app:latest
docker push asruf/demo-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the image was available at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker.io/asruf/demo-app:latest&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s the image the Kubernetes manifest uses by default in this repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  5) CI/CD with GitHub Actions → Docker Hub
&lt;/h2&gt;

&lt;p&gt;I wanted the pipeline to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build the image on every push / PR&lt;/li&gt;
&lt;li&gt;Push to Docker Hub on pushes (not PRs)&lt;/li&gt;
&lt;li&gt;Tag images with:

&lt;ul&gt;
&lt;li&gt;the commit SHA&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;latest&lt;/code&gt; (for the default branch)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The workflow is at &lt;code&gt;./.github/workflows/ci-cd.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Hub secrets
&lt;/h3&gt;

&lt;p&gt;In my GitHub repo I created 2 &lt;strong&gt;Actions secrets&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DOCKERHUB_USERNAME&lt;/code&gt; — &lt;code&gt;asruf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOCKERHUB_TOKEN&lt;/code&gt; — a Docker Hub access token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find these in:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;GitHub repo → Settings → Secrets and variables → Actions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What the workflow does
&lt;/h3&gt;

&lt;p&gt;High level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check out code&lt;/li&gt;
&lt;li&gt;Set up Buildx&lt;/li&gt;
&lt;li&gt;Log in to Docker Hub with &lt;code&gt;DOCKERHUB_USERNAME&lt;/code&gt; + &lt;code&gt;DOCKERHUB_TOKEN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Build the Docker image from &lt;code&gt;./app&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tag it with SHA + &lt;code&gt;latest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Push to &lt;code&gt;docker.io/asruf/demo-app&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So every push to &lt;code&gt;main&lt;/code&gt; automatically gives me a fresh image on Docker Hub, ready for Kubernetes.&lt;/p&gt;




&lt;h2&gt;
  
  
  6) Optional: Terraform for the namespace
&lt;/h2&gt;

&lt;p&gt;I also wanted at least one &lt;strong&gt;Infrastructure as Code&lt;/strong&gt; piece in here, so I used Terraform’s Kubernetes provider to create the namespace.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;terraform/main.tf&lt;/code&gt; (provider + versions) and &lt;code&gt;terraform/k8s.tf&lt;/code&gt; (namespace resource) are already in the repo.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;~/.kube/config&lt;/code&gt; points at a running cluster:&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;terraform

terraform init
terraform plan
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is small on purpose, but it’s enough to say &lt;strong&gt;“I manage part of the Kubernetes infrastructure with Terraform”&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here’s what my &lt;code&gt;terraform init&lt;/code&gt; + &lt;code&gt;terraform plan&lt;/code&gt; looked like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F58cvarz0c17dkp5sahw2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F58cvarz0c17dkp5sahw2.png" alt="Terraform init/plan output" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  7) How can you reuse this
&lt;/h2&gt;

&lt;p&gt;If you want to adapt this project for yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fork the repo or copy the layout&lt;/li&gt;
&lt;li&gt;Change the &lt;strong&gt;Docker Hub&lt;/strong&gt; username and repo name&lt;/li&gt;
&lt;li&gt;Update:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;k8s/deployment.yaml&lt;/code&gt; &lt;code&gt;image:&lt;/code&gt; field&lt;/li&gt;
&lt;li&gt;GitHub Actions secrets (&lt;code&gt;DOCKERHUB_USERNAME&lt;/code&gt;, &lt;code&gt;DOCKERHUB_TOKEN&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Swap the Flask app for your own service if you like&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nice part is that the pattern stays the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;App → Docker → Docker Hub → Kubernetes → (optional) Terraform&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once this pipeline is in your portfolio, you can honestly tell people:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I’ve built and maintained an end-to-end CI/CD pipeline with GitHub Actions, Docker, Kubernetes, Docker Hub, and Terraform. Here’s the repo and here’s the running app.”&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This project is small, but it touches a lot of the buzzwords you see in job posts and freelance gigs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Docker Hub&lt;/li&gt;
&lt;li&gt;Kubernetes&lt;/li&gt;
&lt;li&gt;Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re trying to break into DevOps or just want something concrete to show, feel free to &lt;strong&gt;clone my &lt;a href="https://github.com/rufilboss/devops-e2e-pipeline" rel="noopener noreferrer"&gt;repo&lt;/a&gt;, run it locally, and then customize it&lt;/strong&gt; to match your own style and stack.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>docker</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Are You Planning to Get Into DevOps in 2026? A Practical Guide for the Real World</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Wed, 24 Dec 2025 21:57:09 +0000</pubDate>
      <link>https://dev.to/rufilboss/are-you-planning-to-get-into-devops-in-2026-a-practical-guide-for-the-real-world-3blb</link>
      <guid>https://dev.to/rufilboss/are-you-planning-to-get-into-devops-in-2026-a-practical-guide-for-the-real-world-3blb</guid>
      <description>&lt;p&gt;It’s &lt;strong&gt;December 2025&lt;/strong&gt;, almost January 2026, and if you’re asking yourself &lt;em&gt;“Should I start DevOps now?”&lt;/em&gt; Then this article is for you.&lt;/p&gt;

&lt;p&gt;Not because DevOps is &lt;em&gt;trending&lt;/em&gt; (it is, most companies now practice DevOps workflows and advanced automation standards), but because the &lt;em&gt;demand for real DevOps skill — not surface-level tool knowledge —&lt;/em&gt; is only going to grow faster in the AI-augmented era we’re entering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Now is the Right Time (But Also the Hardest Time)
&lt;/h2&gt;

&lt;p&gt;Over the last five years, DevOps has shifted from pure automation to &lt;strong&gt;intelligent automation&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitOps — treating Git as the &lt;em&gt;single source of truth&lt;/em&gt; for infrastructure and app deployments — is becoming mainstream.&lt;/li&gt;
&lt;li&gt;Cloud-native, serverless, and event-driven designs are reshaping how systems are built.&lt;/li&gt;
&lt;li&gt;AI is now embedded in pipelines, observability, and even predictive deployments, automating decisions that used to require senior engineers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The payoff? Teams deploy faster, with fewer errors, and automation frees engineers from repetitive toil.&lt;br&gt;
The catch? &lt;strong&gt;AI can mask gaps in understanding.&lt;/strong&gt; You can have something running and still not know &lt;em&gt;why&lt;/em&gt; it runs, and that’s where most beginners crash.⁣&lt;/p&gt;

&lt;p&gt;In 2026, &lt;em&gt;DevOps without deep fundamentals&lt;/em&gt; is like driving a car you can’t fix when it breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Works (Based on Real Industry Signals)
&lt;/h2&gt;

&lt;p&gt;Here’s what industry trends say about DevOps evolution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitOps adoption&lt;/strong&gt; is expected to grow sharply, improving reliability and consistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevSecOps&lt;/strong&gt; — embedding security into every pipeline is essential, not optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observers and AI-powered observability tools&lt;/strong&gt; help engineers find problems before they hit users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless pipelines&lt;/strong&gt; are reshaping how we think about infrastructure abstraction and event wiring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren’t buzzwords; they are what you’ll see in job descriptions, production systems, and real-world infrastructure teams.&lt;/p&gt;




&lt;h2&gt;
  
  
  But First — A Reality Check
&lt;/h2&gt;

&lt;p&gt;Before we talk tools: DevOps is &lt;strong&gt;not a checklist&lt;/strong&gt; of technologies. It’s a &lt;em&gt;cultural and engineering mindset&lt;/em&gt; rooted in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Continuous Delivery&lt;/li&gt;
&lt;li&gt;Automated, high-confidence pipelines&lt;/li&gt;
&lt;li&gt;Reproducible infrastructure&lt;/li&gt;
&lt;li&gt;Collaboration between development and operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI will help you generate configs, but it &lt;strong&gt;won’t give you intuition&lt;/strong&gt; around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why a pipeline failed&lt;/li&gt;
&lt;li&gt;How a Kubernetes pod behaves under pressure&lt;/li&gt;
&lt;li&gt;What happens when your Terraform state drifts&lt;/li&gt;
&lt;li&gt;Why observability matters more than logging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That intuition is what separates &lt;em&gt;copy-paste&lt;/em&gt; from &lt;em&gt;craftsmanship&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Practical DevOps Path
&lt;/h2&gt;

&lt;p&gt;Here’s how you should think about your journey if you’re starting &lt;strong&gt;now&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Master Linux and Command Line
&lt;/h3&gt;

&lt;p&gt;Everything in DevOps runs on CLI, shells, permissions, networking, logs, and processes. Your laptop is your first “cluster.”&lt;br&gt;
If you can’t diagnose why SSH fails, nothing else will help.&lt;/p&gt;

&lt;p&gt;In 2026, this fundamental still underpins container workloads, cloud instances, observability agents, and automation scripts.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Scripting &amp;amp; Automation
&lt;/h3&gt;

&lt;p&gt;If you automate only once manually, that’s already DevOps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bash/Python/Go for straightforward automation&lt;/li&gt;
&lt;li&gt;Tools like Make, just to organize tasks&lt;/li&gt;
&lt;li&gt;A mindset of &lt;em&gt;automate early, fix once&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI can &lt;em&gt;suggest scripts&lt;/em&gt; — but always read and refactor them yourself.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Containers &amp;amp; Microservices
&lt;/h3&gt;

&lt;p&gt;Docker, Kubernetes, and sidecar architectures aren’t going anywhere.&lt;br&gt;
These technologies form the backbone of cloud-native systems and service distribution.&lt;/p&gt;

&lt;p&gt;Remember:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Docker makes shipping easier; Kubernetes makes scaling easier —&lt;br&gt;
you still need to know &lt;em&gt;what each component does&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  4. Real Cloud Practice (Practically)
&lt;/h3&gt;

&lt;p&gt;Cloud isn’t just AWS, Azure, or GCP; it’s &lt;em&gt;how production systems run&lt;/em&gt;. AI-native cloud services are replacing old workloads, and multi-cloud/hybrid strategies are increasingly popular.&lt;/p&gt;

&lt;p&gt;Instead of incurring huge bills while you learn, consider tools like &lt;strong&gt;LocalStack&lt;/strong&gt; (&lt;a href="https://github.com/localstack/localstack" rel="noopener noreferrer"&gt;https://github.com/localstack/localstack&lt;/a&gt;) — it lets you &lt;em&gt;practice cloud APIs locally&lt;/em&gt; without risk. That’s real hands-on learning.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. CI/CD and Beyond (From Commit to Deployment)
&lt;/h3&gt;

&lt;p&gt;This is where theory intersects practice.&lt;/p&gt;

&lt;p&gt;You won’t just be writing pipelines, you treat them like &lt;strong&gt;policies&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated testing&lt;/li&gt;
&lt;li&gt;Security scanning&lt;/li&gt;
&lt;li&gt;Version-controlled provisioning&lt;/li&gt;
&lt;li&gt;Fast rollback strategies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In 2026, AI isn’t replacing CI/CD engineers, it’s &lt;em&gt;augmenting&lt;/em&gt; them via smart predictions and autonomous decision loops.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Security &amp;amp; Resilience
&lt;/h3&gt;

&lt;p&gt;DevSecOps is now standard practice:&lt;br&gt;
Security checks must happen &lt;em&gt;before&lt;/em&gt; deployments, not after!&lt;/p&gt;

&lt;p&gt;You need broad fluency with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secrets management (Vault, AWS Secrets Manager)&lt;/li&gt;
&lt;li&gt;Policy as code (OPA, Kyverno)&lt;/li&gt;
&lt;li&gt;Automated vulnerability scanning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Security is not a separate discipline; it’s a microbial process embedded throughout your pipelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  How AI Helps (And When It Hurts)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✔️ Where AI Adds Massive Value
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Generating boilerplate Terraform/CI/CD&lt;/li&gt;
&lt;li&gt;Explaining error logs in clear language&lt;/li&gt;
&lt;li&gt;Offering alternative solutions fast&lt;/li&gt;
&lt;li&gt;Flagging potential configuration problems early&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❌ Where AI Can Mislead
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Blindly copying code without understanding it&lt;/li&gt;
&lt;li&gt;Masking architectural tradeoffs&lt;/li&gt;
&lt;li&gt;Assuming generated configs are optimal&lt;/li&gt;
&lt;li&gt;Trusting AI output without testing it thoroughly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb:&lt;/strong&gt;&lt;br&gt;
If AI helps you &lt;em&gt;understand&lt;/em&gt; a solution — it’s good.&lt;br&gt;
If it only helps you &lt;em&gt;run&lt;/em&gt; a solution — it’s dangerous.&lt;/p&gt;




&lt;h2&gt;
  
  
  These Resources Will Accelerate Your Journey
&lt;/h2&gt;

&lt;p&gt;You might find these extremely useful:&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;100 Days of DevOps&lt;/strong&gt; — a proven, hands-on progression:&lt;br&gt;
&lt;a href="https://github.com/rufilboss/100DaysOfDevOps" rel="noopener noreferrer"&gt;https://github.com/rufilboss/100DaysOfDevOps&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;DevOps Guide (fork &amp;amp; extend it):&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/rufilboss/DevOps-Guide/" rel="noopener noreferrer"&gt;https://github.com/rufilboss/DevOps-Guide/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;DevOps Books curated by the community:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/DevOps-Projects-Ideas/DevOps-Books/" rel="noopener noreferrer"&gt;https://github.com/DevOps-Projects-Ideas/DevOps-Books/&lt;/a&gt;&lt;br&gt;
(500+ stars | 270+ forks — real community validation)&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Cloud infrastructure local practice (Low cost/no risk):&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/localstack/localstack" rel="noopener noreferrer"&gt;https://github.com/localstack/localstack&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are not ads, they are &lt;strong&gt;battle-tested resources&lt;/strong&gt; used by beginners and pros alike.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Takeaway
&lt;/h2&gt;

&lt;p&gt;DevOps in 2026 is not &lt;em&gt;just another career&lt;/em&gt;.&lt;br&gt;
It’s about &lt;strong&gt;thinking in systems&lt;/strong&gt;, not tools.&lt;/p&gt;

&lt;p&gt;AI will let you &lt;em&gt;explore more&lt;/em&gt;, but only fundamentals will let you &lt;em&gt;master more&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You don’t need to learn everything today, but you do need to build &lt;strong&gt;confidence through repetition, testing, failure, and curiosity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’re ready to dive in, you’re already ahead of the crowd.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>beginners</category>
      <category>ai</category>
      <category>cloud</category>
    </item>
    <item>
      <title>A Practical Guide to Organizing an AWS Community Day (From Scratch)</title>
      <dc:creator>Ilyas Rufai</dc:creator>
      <pubDate>Tue, 23 Dec 2025 11:59:50 +0000</pubDate>
      <link>https://dev.to/rufilboss/a-practical-guide-to-organizing-an-aws-community-day-from-scratch-1ao4</link>
      <guid>https://dev.to/rufilboss/a-practical-guide-to-organizing-an-aws-community-day-from-scratch-1ao4</guid>
      <description>&lt;h2&gt;
  
  
  What is AWS Community Day?
&lt;/h2&gt;

&lt;p&gt;AWS Community Day is a &lt;strong&gt;one-day, community-led conference&lt;/strong&gt; organized by AWS communities such as AWS User Groups or AWS Cloud Clubs. It brings cloud practitioners, students, and enthusiasts together to learn, network, and share real-world AWS knowledge.&lt;/p&gt;

&lt;p&gt;Community Days range from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large, multi-country events (e.g., AWS Community Day DACH)&lt;/li&gt;
&lt;li&gt;To smaller, single-community events organized by one User Group or Cloud Club&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide documents &lt;strong&gt;what it takes to plan, organize, and execute an AWS Community Day successfully&lt;/strong&gt;, especially if you’re doing it for the &lt;strong&gt;first time&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 1: Foundations (Before Anything Else)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Define Your Scope
&lt;/h3&gt;

&lt;p&gt;Before tools, sponsors, or speakers, be clear on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Target audience:&lt;/strong&gt; students, professionals, beginners, mixed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event size goal:&lt;/strong&gt; (e.g. 1000–1500 attendees)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event type:&lt;/strong&gt; free or paid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; city, campus, venue type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expected attendees: ___&lt;br&gt;
Target audience: ___&lt;br&gt;
City/Campus: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2. Official AWS Alignment (Very Important)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  AWS Community Day Page
&lt;/h4&gt;

&lt;p&gt;Start here:&lt;br&gt;
👉 &lt;a href="https://aws.amazon.com/developer/community/community-day/" rel="noopener noreferrer"&gt;https://aws.amazon.com/developer/community/community-day/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This page explains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What qualifies as an AWS Community Day&lt;/li&gt;
&lt;li&gt;Branding rules&lt;/li&gt;
&lt;li&gt;FAQs&lt;/li&gt;
&lt;li&gt;Organizer expectations&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  AWS Organizer Slack
&lt;/h4&gt;

&lt;p&gt;Join the &lt;strong&gt;&lt;code&gt;community-day-organizers&lt;/code&gt;&lt;/strong&gt; Slack channel.&lt;br&gt;
This is critical for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoiding date clashes in the same region&lt;/li&gt;
&lt;li&gt;Learning from other organizers&lt;/li&gt;
&lt;li&gt;Funding guidance&lt;/li&gt;
&lt;li&gt;Shared templates and experiences&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Phase 2: Online Presence &amp;amp; Registration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3. Event Website
&lt;/h3&gt;

&lt;p&gt;You will need a simple website containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event details&lt;/li&gt;
&lt;li&gt;Agenda&lt;/li&gt;
&lt;li&gt;Speakers&lt;/li&gt;
&lt;li&gt;Registration link&lt;/li&gt;
&lt;li&gt;Sponsors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build your own&lt;/li&gt;
&lt;li&gt;Use a template (e.g. Hugo-based AWS Community Day templates)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Website URL: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  4. Registration
&lt;/h3&gt;

&lt;p&gt;Common tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eventbrite&lt;/li&gt;
&lt;li&gt;Google Forms&lt;/li&gt;
&lt;li&gt;Konfhub&lt;/li&gt;
&lt;li&gt;Other ticketing platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose one that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handles attendee limits&lt;/li&gt;
&lt;li&gt;Exports attendee data&lt;/li&gt;
&lt;li&gt;Supports QR codes (nice bonus)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Registration platform: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  5. Call for Speakers (CfS)
&lt;/h3&gt;

&lt;p&gt;You’ll need speakers early.&lt;/p&gt;

&lt;p&gt;Tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sessionize&lt;/li&gt;
&lt;li&gt;Google Forms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Collect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Topic title&lt;/li&gt;
&lt;li&gt;Abstract&lt;/li&gt;
&lt;li&gt;Speaker bio&lt;/li&gt;
&lt;li&gt;Preferred time slot&lt;/li&gt;
&lt;li&gt;Session level (Beginner / Intermediate / Advanced)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;CfS link: ___&lt;br&gt;
Submission deadline: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 3: AWS Support &amp;amp; Resources
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6. AWS-Provided Materials
&lt;/h3&gt;

&lt;p&gt;AWS provides downloadable assets that help a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logos&lt;/li&gt;
&lt;li&gt;Fonts&lt;/li&gt;
&lt;li&gt;Slide templates&lt;/li&gt;
&lt;li&gt;Organizer resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UG Toolkit / Community Toolkit&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  7. Funding (Yes, It’s Possible 💵)
&lt;/h3&gt;

&lt;p&gt;AWS may provide &lt;strong&gt;financial or material support&lt;/strong&gt; depending on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event size&lt;/li&gt;
&lt;li&gt;Community maturity&lt;/li&gt;
&lt;li&gt;Region&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Funding requests are usually discussed in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS organizer Slack&lt;/li&gt;
&lt;li&gt;Via AWS Community contacts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Funding requested? Yes / No&lt;br&gt;
Amount / type: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 4: Planning the Actual Event
&lt;/h2&gt;

&lt;h3&gt;
  
  
  8. Attendee Estimation (Be Realistic)
&lt;/h3&gt;

&lt;p&gt;Estimating attendance is tricky.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Size of your community&lt;/li&gt;
&lt;li&gt;Average meetup attendance&lt;/li&gt;
&lt;li&gt;Travel distance&lt;/li&gt;
&lt;li&gt;Marketing reach&lt;/li&gt;
&lt;li&gt;Exam periods / holidays&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rule of thumb:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Expect less → Be pleasantly surprised later&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Also expect &lt;strong&gt;last-minute registrations&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  9. Venue Selection
&lt;/h3&gt;

&lt;p&gt;Choose a venue that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can scale up or down&lt;/li&gt;
&lt;li&gt;Supports multiple rooms&lt;/li&gt;
&lt;li&gt;Has reliable power &amp;amp; internet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Minimum rooms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Main session room(s)&lt;/li&gt;
&lt;li&gt;Speakers’ room&lt;/li&gt;
&lt;li&gt;Storage / quiet room&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Tip:&lt;/em&gt;&lt;br&gt;
Sponsors/expo area should be &lt;strong&gt;where attendees naturally pass&lt;/strong&gt;, not isolated.&lt;/p&gt;




&lt;h3&gt;
  
  
  10. Catering
&lt;/h3&gt;

&lt;p&gt;Keep it simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Snacks&lt;/li&gt;
&lt;li&gt;Lunch&lt;/li&gt;
&lt;li&gt;Drinks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t forget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Speakers’ room refreshments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Catering plan: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  11. Tracks &amp;amp; Agenda Design
&lt;/h3&gt;

&lt;p&gt;Less is more.&lt;/p&gt;

&lt;p&gt;Avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Too many parallel tracks&lt;/li&gt;
&lt;li&gt;Overlapping highly attractive sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recommended format (per session):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;30 min talk&lt;/li&gt;
&lt;li&gt;15 min Q&amp;amp;A&lt;/li&gt;
&lt;li&gt;15 min break / expo&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Sample Agenda
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;08:00 – Registration
09:00 – Opening Remarks
09:15 – Keynote
10:00 – Break / Expo
10:30 – Session Slot 1
11:30 – Session Slot 2
12:30 – Lunch
13:30 – Session Slot 3
14:30 – Session Slot 4
15:30 – Break
16:00 – Final Session
16:30 – Closing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  12. Speakers
&lt;/h3&gt;

&lt;p&gt;Aim for balance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS employees&lt;/li&gt;
&lt;li&gt;Community speakers&lt;/li&gt;
&lt;li&gt;First-time speakers&lt;/li&gt;
&lt;li&gt;Local &amp;amp; external speakers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preferred speaking time&lt;/li&gt;
&lt;li&gt;Travel needs&lt;/li&gt;
&lt;li&gt;Slide sharing permission&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  13. Free vs Paid Event
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Free Event&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Higher no-shows&lt;/li&gt;
&lt;li&gt;More inclusive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Paid Event&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer no-shows&lt;/li&gt;
&lt;li&gt;More admin (tax, accounting)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose what fits your context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 5: Marketing &amp;amp; Sponsors
&lt;/h2&gt;

&lt;h3&gt;
  
  
  14. Marketing (Don’t Underestimate This)
&lt;/h3&gt;

&lt;p&gt;Channels to use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WhatsApp groups&lt;/li&gt;
&lt;li&gt;LinkedIn&lt;/li&gt;
&lt;li&gt;Twitter/X&lt;/li&gt;
&lt;li&gt;Campus clubs&lt;/li&gt;
&lt;li&gt;Word of mouth&lt;/li&gt;
&lt;li&gt;Sponsors’ channels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start early.&lt;/p&gt;




&lt;h3&gt;
  
  
  15. Sponsors
&lt;/h3&gt;

&lt;p&gt;Sponsors fund your event.&lt;/p&gt;

&lt;p&gt;Prepare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear sponsorship packages&lt;/li&gt;
&lt;li&gt;Benefits list (booth, logo, talk, etc.)&lt;/li&gt;
&lt;li&gt;Venue layout&lt;/li&gt;
&lt;li&gt;Simple agreement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some sponsors may qualify for &lt;strong&gt;AWS MDF funding&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;📌 &lt;em&gt;Placeholder:&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sponsor tiers: ___&lt;br&gt;
Confirmed sponsors: ___&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 6: Operations &amp;amp; Logistics
&lt;/h2&gt;

&lt;h3&gt;
  
  
  16. Organizing Team
&lt;/h3&gt;

&lt;p&gt;Small teams can work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2–5 core organizers&lt;/li&gt;
&lt;li&gt;Clear roles (logistics, speakers, sponsors, media)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  17. Volunteers
&lt;/h3&gt;

&lt;p&gt;Volunteers help with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registration&lt;/li&gt;
&lt;li&gt;Directions&lt;/li&gt;
&lt;li&gt;Speaker support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tip:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ask sponsors if they can assign volunteers.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  18. Badges, Lanyards &amp;amp; Printing
&lt;/h3&gt;

&lt;p&gt;Decide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-printed vs on-site printing&lt;/li&gt;
&lt;li&gt;Badge color coding (Organizers, Speakers, Sponsors, Attendees)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reusable materials save money long-term.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase 7: Communication &amp;amp; Experience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  19. Communication Channels
&lt;/h3&gt;

&lt;p&gt;Recommended:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slack (organizers + speakers)&lt;/li&gt;
&lt;li&gt;WhatsApp (organizers + volunteers)&lt;/li&gt;
&lt;li&gt;WhatsApp (real-time event coordination)&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  20. Speaker Slides &amp;amp; Content
&lt;/h3&gt;

&lt;p&gt;Attendees often ask for slides.&lt;/p&gt;

&lt;p&gt;Before the event:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask speakers if slides can be shared
After the event:&lt;/li&gt;
&lt;li&gt;Upload to website or shared drive&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  21. Speaker Dinner (Highly Recommended)
&lt;/h3&gt;

&lt;p&gt;If budget allows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host a speaker dinner&lt;/li&gt;
&lt;li&gt;Builds relationships&lt;/li&gt;
&lt;li&gt;Improves speaker experience&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reality Check 😅
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;Complain&lt;/li&gt;
&lt;li&gt;Ask last-minute questions&lt;/li&gt;
&lt;li&gt;Need help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is normal. Expect it.&lt;/p&gt;




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

&lt;p&gt;Organizing an AWS Community Day is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hard work&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Time-consuming&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extremely rewarding&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From idea to execution, expect &lt;strong&gt;months of planning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you’re thinking about doing it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Yes — do it.&lt;/strong&gt;&lt;br&gt;
The impact on your community is worth it.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>aws</category>
      <category>awscloud</category>
      <category>cloudcomputing</category>
      <category>awscommunityday</category>
    </item>
  </channel>
</rss>
