<?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: Atsushi Suzuki</title>
    <description>The latest articles on DEV Community by Atsushi Suzuki (@suzuki0430).</description>
    <link>https://dev.to/suzuki0430</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F594694%2F43f97445-a02a-471a-8324-6a257b785f04.png</url>
      <title>DEV Community: Atsushi Suzuki</title>
      <link>https://dev.to/suzuki0430</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/suzuki0430"/>
    <language>en</language>
    <item>
      <title>Taking the KCSA Exam on My Washing Machine (And Passing!)</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Thu, 19 Mar 2026 01:47:58 +0000</pubDate>
      <link>https://dev.to/suzuki0430/taking-the-kcsa-exam-on-my-washing-machine-and-passing-4o66</link>
      <guid>https://dev.to/suzuki0430/taking-the-kcsa-exam-on-my-washing-machine-and-passing-4o66</guid>
      <description>&lt;p&gt;After grabbing my CKA on Jan 11, KCNA on Jan 23, and CKAD on Jan 26, I kept the momentum going and took the KCSA on February 6th!&lt;/p&gt;

&lt;p&gt;Briefly, the KCSA (Kubernetes and Cloud Native Security Associate) is a multiple-choice certification that tests your fundamental knowledge of Kubernetes security.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://training.linuxfoundation.org/certification/kubernetes-and-cloud-native-security-associate-kcsa/" rel="noopener noreferrer"&gt;Kubernetes and Cloud Native Security Associate&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Exam Results
&lt;/h2&gt;

&lt;p&gt;I passed with a score of &lt;strong&gt;81%&lt;/strong&gt; (passing score is 75%).&lt;/p&gt;

&lt;p&gt;Usually, I rent a private room in a coworking space to take these exams, but this time I wanted to save some money and decided to take it at home. After desperately searching for a completely empty space in my house that met the proctoring requirements, I ended up putting my MacBook on top of my washing machine!&lt;/p&gt;

&lt;p&gt;Since the KCSA is a multiple-choice exam without practical labs, I figured, "It doesn't matter if the laptop wobbles a bit!"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Date: Feb 6, 2026, 10:00 AM~&lt;/li&gt;
&lt;li&gt;Location: My laundry room&lt;/li&gt;
&lt;li&gt;Device: MacBook Air (2022, M2, 13.6")&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%2Fnnrx5kuvws06emgutc03.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%2Fnnrx5kuvws06emgutc03.png" alt=" " width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Impressions &amp;amp; Difficulty
&lt;/h2&gt;

&lt;p&gt;By the time I finished my first pass through the questions, I had flagged about 40 out of 60 questions for review. Honestly, I thought, "I'm completely doomed..."&lt;/p&gt;

&lt;p&gt;Moreover, since it's the only one of the five core Kubernetes certifications not offered in Japanese, I stumbled over English reading comprehension in a few places. However, there was plenty of time left on the clock, so I read the questions very carefully, tried to visualize the scenarios, and managed to work my way through.&lt;/p&gt;

&lt;p&gt;Looking back, if I had taken the CKS first, I would have had a clearer image of the scenarios and found some questions much easier. I managed to pass this time, but if you want to solidify your knowledge and guarantee a pass, I honestly recommend taking the CKS before the KCSA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Study Materials I Used
&lt;/h2&gt;

&lt;p&gt;I used KodeKloud and a &lt;a href="https://github.com/thiago4go/kubernetes-security-kcsa-mock" rel="noopener noreferrer"&gt;Kubernetes Security KCSA Mock Exam Simulator&lt;/a&gt;.&lt;br&gt;
In total, I spent just under 20 hours studying.&lt;/p&gt;

&lt;h3&gt;
  
  
  KodeKloud
&lt;/h3&gt;

&lt;p&gt;I watched all the video lectures on KodeKloud and looped through their 3 Mock Exams about four times.&lt;/p&gt;

&lt;p&gt;Reflecting on it now, while concepts like STRIDE and the 4C's are specific to KCSA, topics like &lt;code&gt;securityContext&lt;/code&gt;, Admission Controllers, Istio, and Falco heavily overlap with CKS. If I had done hands-on CKS prep beforehand, I could have studied much more efficiently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.kodekloud.com/user/courses/kubernetes-and-cloud-native-security-associate-kcsa" rel="noopener noreferrer"&gt;Kubernetes and Cloud Native Security Associate (KCSA)&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Mock Exams
&lt;/h3&gt;

&lt;p&gt;From reading other people's review articles, I felt that KodeKloud alone wouldn't give me enough practice, so I additionally tackled a web-based mock exam repository containing about 300 questions.&lt;/p&gt;

&lt;p&gt;You can launch an app to practice, but since it lacked a feature to flag questions I couldn't solve, I cloned the repository locally and opened it in my IDE (Cursor). My workflow was simple: I manually deleted the questions I solved correctly, and asked the Cursor AI chat for explanations on the ones I didn't understand.&lt;/p&gt;

&lt;p&gt;I felt these questions were closer to the actual exam content than the KodeKloud mock exams, so I highly recommend doing them!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/thiago4go/kubernetes-security-kcsa-mock" rel="noopener noreferrer"&gt;Kubernetes Security KCSA Mock Exam Simulator&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Now, all that's left is to get my CKS and achieve the ultimate title of Kubestronaut! Wish me luck!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>certification</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>CKAD is Easier Than CKA: How I Passed with 20 Hours of Study</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Tue, 03 Feb 2026 02:40:15 +0000</pubDate>
      <link>https://dev.to/suzuki0430/ckad-is-easier-than-cka-how-i-passed-with-20-hours-of-study-432d</link>
      <guid>https://dev.to/suzuki0430/ckad-is-easier-than-cka-how-i-passed-with-20-hours-of-study-432d</guid>
      <description>&lt;p&gt;In January 2026, I achieved the "Kubernetes Triple Crown" (CKA, KCNA, and CKAD) in a single month. This post is a report of my experience passing the CKAD exam.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://training.linuxfoundation.org/certification/certified-kubernetes-application-developer-ckad/" rel="noopener noreferrer"&gt;Certified Kubernetes Application Developer (CKAD)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Exam Result
&lt;/h2&gt;

&lt;p&gt;I passed with a score of 81% (passing score is 66%).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exam Date: January 28, 2026&lt;/li&gt;
&lt;li&gt;Environment: Coworking space private room&lt;/li&gt;
&lt;li&gt;Kubernetes Version: v1.34&lt;/li&gt;
&lt;li&gt;Device: MacBook Air (2022, M2, 13.6-inch)&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%2Fll65aqt0an68nsi73ypp.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%2Fll65aqt0an68nsi73ypp.png" alt=" " width="800" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts on Difficulty
&lt;/h2&gt;

&lt;p&gt;Overall, the CKAD felt significantly easier than the CKA. I didn't encounter any questions that left me completely stumped. The main reason for this lower difficulty, in my opinion, was the lack of complex troubleshooting or tricky Helm-related questions.&lt;/p&gt;

&lt;p&gt;While I found the actual CKA exam to be more difficult than the Killer Shell simulator, the CKAD was much easier than its Killer Shell counterpart. I never felt rushed for time and even had 30 minutes left to review my answers. Although I solved 10 mock exams as preparation, I feel that if you have already passed the CKA, you might not need to go that far.&lt;/p&gt;

&lt;h2&gt;
  
  
  Study Materials Used
&lt;/h2&gt;

&lt;p&gt;I used KodeKloud and Killer Shell. Total study time was approximately 20 hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  KodeKloud
&lt;/h3&gt;

&lt;p&gt;Since the exam scope overlaps significantly with the CKA, I focused only on the CKAD-specific topics. I ran through the Lightning Labs three times and the Mock Exams once (as they were relatively straightforward).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Observability (Readiness Probes, Liveness Probes, Logging, Monitoring)&lt;/li&gt;
&lt;li&gt;POD Design (Deployment Strategy)&lt;/li&gt;
&lt;li&gt;Lightning Labs (1, 2)&lt;/li&gt;
&lt;li&gt;Mock Exams (1, 2)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://learn.kodekloud.com/user/courses/certified-kubernetes-application-developer-ckad" rel="noopener noreferrer"&gt;Certified Kubernetes Application Developer (CKAD) - KodeKloud&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As the initial course only had two mock exams, which felt insufficient, I tackled an additional 8 mock exams from the series below, repeating any questions I missed three times.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://learn.kodekloud.com/user/courses/ultimate-certified-kubernetes-application-developer-ckad-mock-exam-series" rel="noopener noreferrer"&gt;Ultimate CKAD Mock Exam Series&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Killer Shell
&lt;/h3&gt;

&lt;p&gt;Just like the CKA, scheduling your exam gives you two sessions for the Killer Shell simulator. It is essential to use this because it replicates the actual remote desktop environment.&lt;/p&gt;

&lt;p&gt;Since I was using a MacBook, the keyboard shortcuts for copying and pasting change within the remote desktop. Practicing these in the simulator was vital. Note that each session expires 36 hours after activation. If you want to review after the session expires, I recommend saving the answer pages as PDF or HTML. I only used one of the two sessions this time (saving the other in case I failed).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://killer.sh/" rel="noopener noreferrer"&gt;Killer Shell&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Master the CKAD-specific domains
&lt;/h3&gt;

&lt;p&gt;Make sure to read the documentation for areas not covered in CKA, such as Job and CronJob. You should also be familiar with specific options, such as creating a one-off Job from an existing CronJob:&lt;br&gt;
&lt;code&gt;kubectl create job test-job --from=cronjob/a-cronjob&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Memorize the Documentation URL
&lt;/h3&gt;

&lt;p&gt;The browser doesn't automatically open to the docs when the exam starts. Be ready to quickly navigate to &lt;code&gt;https://kubernetes.io/docs&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Practice Copy-Pasting in Remote Desktop
&lt;/h3&gt;

&lt;p&gt;Your speed in copying and pasting can literally determine whether you pass or fail. Practice these shortcuts in Killer Shell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Copy (Terminal): &lt;code&gt;Ctrl + Shift + C&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Paste (Terminal): &lt;code&gt;Ctrl + Shift + V&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The Linux Foundation was offering a 35% discount coupon, so I’m planning to take the KCSA in February. The CKS is still a bit expensive, so I'll save that for when I have a bit more budget!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>certification</category>
      <category>devops</category>
      <category>learning</category>
    </item>
    <item>
      <title>How I Passed the KCNA in Just 3 Hours (After Getting CKA)</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Mon, 26 Jan 2026 05:00:57 +0000</pubDate>
      <link>https://dev.to/suzuki0430/how-i-passed-the-kcna-in-just-3-hours-after-getting-cka-3bf7</link>
      <guid>https://dev.to/suzuki0430/how-i-passed-the-kcna-in-just-3-hours-after-getting-cka-3bf7</guid>
      <description>&lt;p&gt;After starting my Kubernetes journey in November and earning CKA (Certified Kubernetes Administrator) on January 11, 2026, I decided to keep the momentum going.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/suzuki0430/cka-certified-kubernetes-administrator-exam-report-2026-dont-rely-on-old-guides-mastering-the-534m"&gt;CKA (Certified Kubernetes Administrator) Exam Report 2026: Don’t Rely on Old Guides (Mastering the Post-2025 Revision)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since I had some extra time in January after recovering from a cold, I challenged myself to take the KCNA (Kubernetes and Cloud Native Associate) exam.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://training.linuxfoundation.org/certification/kubernetes-cloud-native-associate/" rel="noopener noreferrer"&gt;Kubernetes and Cloud Native Associate (KCNA)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Exam Results
&lt;/h2&gt;

&lt;p&gt;I passed with a score of 87 % (Passing score: 75 %).&lt;br&gt;
The exam is 90 minutes long, but I finished everything, including a full review, in just 30 minutes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exam Date: Jan 21, 2026&lt;/li&gt;
&lt;li&gt;Location: Private room in a co-working space&lt;/li&gt;
&lt;li&gt;Device: MacBook Air (2022, M2, 13.6")&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%2F72jqaqdg43mqqzut38f3.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%2F72jqaqdg43mqqzut38f3.png" alt=" " width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflections and Difficulty
&lt;/h2&gt;

&lt;p&gt;Coming off a fresh CKA win, I thought this would be a breeze. However, after the first round of 60 questions, I had flagged 27 questions for review. For a moment, I actually felt I might fail!&lt;/p&gt;

&lt;p&gt;During the second round, I carefully switched between English and Japanese to grasp the nuances. Eventually, I narrowed down the "completely unsure" questions to about 9.&lt;/p&gt;

&lt;p&gt;While my CKA knowledge covered the core components perfectly, I struggled with broader CNCF ecosystem questions—such as "What is the official name of this specific CNCF event?" These caught me off guard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparation Materials
&lt;/h2&gt;

&lt;p&gt;I spent a total of about 3 hours studying using these resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. KodeKloud
&lt;/h3&gt;

&lt;p&gt;I completed three Mock Exams (2 rounds each).&lt;br&gt;
The questions were quite straightforward—simpler than the actual exam—so it took less than 2 hours to fly through them.&lt;br&gt;
&lt;a href="https://learn.kodekloud.com/user/courses/kubernetes-and-cloud-native-associate-kcna" rel="noopener noreferrer"&gt;Kubernetes and Cloud-Native Associate (KCNA)&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Kindle Practice Exams
&lt;/h3&gt;

&lt;p&gt;Feeling a bit under-prepared with just KodeKloud, I used a practice book (Japanese) available on Kindle unlimited. This felt closer to the actual exam difficulty, though the questions were still relatively simple.&lt;br&gt;
&lt;a href="https://amzn.asia/d/f482K6o" rel="noopener noreferrer"&gt;2週間で合格！Kubernetes and Cloud Native Associate (KCNA)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Study Tips
&lt;/h2&gt;

&lt;p&gt;Based on my experience, here is what you should focus on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Master Pod Scheduling
&lt;/h3&gt;

&lt;p&gt;There were many questions about NodeSelector, NodeAffinity, PodAffinity, and Taints/Tolerations. Make sure you can clearly distinguish their use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understand the CNCF Ecosystem
&lt;/h3&gt;

&lt;p&gt;Unlike the CKA, the KCNA tests your knowledge of CNCF project names and event details. Don't skip this part!&lt;/p&gt;

&lt;h3&gt;
  
  
  GitOps Concepts
&lt;/h3&gt;

&lt;p&gt;I knew of ArgoCD, but I wasn't familiar with its specific operational details. It’s worth reviewing GitOps workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Monitoring &amp;amp; Observability Tools
&lt;/h3&gt;

&lt;p&gt;Be clear on the boundaries between tools like Prometheus, Fluentd, and Jaeger. Know exactly which tool handles which part of the observability stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I’m riding this wave of momentum and will be taking the CKAD (Certified Kubernetes Application Developer) on January 26. Wish me luck!&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>certification</category>
      <category>devops</category>
      <category>learning</category>
    </item>
    <item>
      <title>CKA (Certified Kubernetes Administrator) Exam Report 2026: Don’t Rely on Old Guides (Mastering the Post-2025 Revision)</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Tue, 13 Jan 2026 04:58:27 +0000</pubDate>
      <link>https://dev.to/suzuki0430/cka-certified-kubernetes-administrator-exam-report-2026-dont-rely-on-old-guides-mastering-the-534m</link>
      <guid>https://dev.to/suzuki0430/cka-certified-kubernetes-administrator-exam-report-2026-dont-rely-on-old-guides-mastering-the-534m</guid>
      <description>&lt;p&gt;Despite being active as an AWS Community Builder (Containers) and a Docker Captain, for some reason, I had never really touched Kubernetes before. So, starting in November 2025, I began studying for 1-2 hours on weekdays outside of work.&lt;/p&gt;

&lt;p&gt;I am the type of person who finds it easier to solidify foundational knowledge when I have a specific goal like a certification, so I decided to aim for the Certified Kubernetes Administrator (CKA) first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://training.linuxfoundation.org/certification/certified-kubernetes-administrator-cka/" rel="noopener noreferrer"&gt;Certified Kubernetes Administrator (CKA)&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I am writing this article
&lt;/h2&gt;

&lt;p&gt;If you search for "CKA Pass" you will find many experience reports. However, there are still not many articles written after the exam scope revision in February 2025. Having actually taken the exam, I felt that the "sense of difficulty" and "essential techniques" mentioned in older reports are no longer entirely applicable. I wrote this article as a reference for those who are planning to take the exam in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exam Results
&lt;/h2&gt;

&lt;p&gt;I passed with a score of 72% (passing score is 66%). It was my first time taking a hands-on practical exam, and compared to multiple-choice exams, I felt constantly rushed—I couldn't stop sweating. I didn't finish everything within the time limit and didn't have time to go back to the troubleshooting questions I had flagged.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exam Date:&lt;/strong&gt; 2026/01/11 13:30~&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; Private room in a co-working space&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes Version:&lt;/strong&gt; v1.34&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device:&lt;/strong&gt; MacBook Air (2022, M2, 13.6-inch)&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%2Fd7s2ddyb1je3z4xgj0ig.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%2Fd7s2ddyb1je3z4xgj0ig.png" alt=" " width="800" height="281"&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%2F0w2g2jvf9g8lbqei2zu1.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%2F0w2g2jvf9g8lbqei2zu1.png" alt=" " width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Impressions of the New Exam Scope (Post-Feb 2025) and Difficulty
&lt;/h2&gt;

&lt;p&gt;To be honest, I felt it was significantly more difficult than the image I had from older experience reports. My personal impression was that it felt about as challenging as the "Killer Shell" simulator (mentioned later).&lt;/p&gt;

&lt;p&gt;Below is the revised exam scope, and I felt that questions related to the newly added content made up about half of the exam.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://training.linuxfoundation.org/certified-kubernetes-administrator-cka-program-changes/" rel="noopener noreferrer"&gt;CKA Program Changes - Feb 2025&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While I cannot provide specific details about the questions, you will likely struggle if your understanding of the following newly added topics is weak:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helm&lt;/li&gt;
&lt;li&gt;Kustomize&lt;/li&gt;
&lt;li&gt;Gateway API (Gateway, HttpRoute, etc.)&lt;/li&gt;
&lt;li&gt;Network Policy&lt;/li&gt;
&lt;li&gt;CRDs (Custom Resource Definitions)&lt;/li&gt;
&lt;li&gt;Extension interfaces (CNI, CSI, CRI, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Learning Resources Used
&lt;/h2&gt;

&lt;p&gt;I used Udemy, KodeKloud, and Killer Shell.&lt;br&gt;
Many reports from before the revision said that Udemy and Killer Shell were enough, but my personal feeling is that I was only able to put up a fight because I went as far as doing the additional labs on KodeKloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Udemy
&lt;/h3&gt;

&lt;p&gt;This is the standard Udemy course recommended in almost every report. I bought it during one of their frequent sales.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.udemy.com/course/certified-kubernetes-administrator-with-practice-tests/" rel="noopener noreferrer"&gt;Certified Kubernetes Administrator (CKA) Practice Exam Tests&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The course consists of video lectures and practice tests (hosted on a separate service called KodeKloud). Since my Kubernetes knowledge was literally zero, I studied each component through the videos before attempting the practice tests.&lt;/p&gt;

&lt;p&gt;I went through the lectures once and repeated the practice questions I didn't understand multiple times (up to 4 times). For my weak areas (Helm, Kustomize, Gateway API, CRD), I re-watched the lectures several times.&lt;/p&gt;

&lt;p&gt;Once I could solve over 80% of the practice questions, I tackled the three Mock Exams in the final section until I could solve them perfectly (up to 4 rounds for some).&lt;/p&gt;

&lt;p&gt;For explanations I didn't quite understand, I asked AI for help.&lt;/p&gt;

&lt;h3&gt;
  
  
  KodeKloud
&lt;/h3&gt;

&lt;p&gt;This is the KodeKloud platform included with the Udemy course:&lt;br&gt;
&lt;a href="https://learn.kodekloud.com/user/courses/cka-certification-course-certified-kubernetes-administrator" rel="noopener noreferrer"&gt;Certified Kubernetes Administrator (CKA)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since having only three Mock Exams felt a bit uncertain, I paid for the following course to solve five additional Mock Exams:&lt;br&gt;
&lt;a href="https://learn.kodekloud.com/user/courses/ultimate-certified-kubernetes-administrator-cka-mock-exam-series" rel="noopener noreferrer"&gt;Ultimate Certified Kubernetes Administrator (CKA) Mock Exam Series&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Killer Shell
&lt;/h3&gt;

&lt;p&gt;When you register for the exam, you get two sessions of the "Killer Shell" exam simulator.&lt;br&gt;
It uses the same Remote Desktop environment as the actual exam, so you should absolutely use it to get used to the interface. In my case, since I took the exam on a MacBook, the shortcut keys for commands change in the Remote Desktop, so I was able to practice copy-pasting in the simulator.&lt;/p&gt;

&lt;p&gt;Note that the simulator access is provided twice, and each session expires after 36 hours. If you want to review after it expires, it’s better to save the answer pages as PDF or HTML.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://killer.sh/" rel="noopener noreferrer"&gt;Killer Shell&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips
&lt;/h2&gt;

&lt;h3&gt;
  
  
  No need to add settings to &lt;code&gt;.bashrc&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Common tips in older reports, such as "aliases for switching namespaces," are no longer necessary. This is because the current exam format involves using &lt;code&gt;ssh&lt;/code&gt; to switch between different environments for each task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memorize the Documentation URL
&lt;/h3&gt;

&lt;p&gt;The browser doesn't automatically open to the documentation page when the exam starts, so make sure you can quickly navigate to &lt;code&gt;https://kubernetes.io/docs&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Learn Copy-Paste Shortcuts for Remote Desktop
&lt;/h3&gt;

&lt;p&gt;Whether or not you can copy-paste quickly can make or break your pass/fail result. Practice the operations on Killer Shell.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy (Terminal):&lt;/strong&gt; &lt;code&gt;Ctrl + Shift + C&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paste (Terminal):&lt;/strong&gt; &lt;code&gt;Ctrl + Shift + V&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prepare for Applied Questions
&lt;/h3&gt;

&lt;p&gt;Don't just memorize each component; imagine how they combine with others. Simply "memorizing Helm commands" might not be enough for some of the harder questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helm + CRD&lt;/li&gt;
&lt;li&gt;Migration from Ingress → Gateway + HttpRoute, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exam Environment
&lt;/h2&gt;

&lt;p&gt;The conditions for the exam environment are strict: "a private space without noise," "no objects on the desk," etc. Unfortunately, I didn't have such an environment at home, so I rented a private room in a co-working space.&lt;/p&gt;

&lt;p&gt;Interaction with the proctor is via chat, so there is no verbal conversation. However, it seems that even slight noise will be pointed out, so choosing a private room is the safe bet.&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%2Fsz31ubr954bd2h2q0cld.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%2Fsz31ubr954bd2h2q0cld.png" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instructions from the proctor come in your native language (Japanese in my case), but they seem to be using machine translation, as some instructions were a bit confusing. I assumed the proctor was non-Japanese, so I replied in English (just basic things like "OK," "thanks," etc.).&lt;/p&gt;

&lt;p&gt;Following the proctor's instructions, I used the webcam to show the room (ceiling, floor, desk, walls), both ears, both wrists, and my powered-off smartphone. It took about 15 minutes to actually start the exam.&lt;/p&gt;

&lt;p&gt;Also, I didn't use the external monitor at the co-working space, but in retrospect, having a larger workspace would have made things easier, so I probably should have used it.&lt;/p&gt;

&lt;p&gt;During the exam, I didn't really feel the presence of the proctor, but when I was stuck on a problem and touched my chin, a chat message arrived saying, "Please refrain from gestures that cover your face."&lt;/p&gt;

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

&lt;p&gt;Obtaining the CKA was quite a challenge, but I plan to take the CKAD while I still have this momentum. The second half of 2025 was quite rough at my company and I felt zero personal growth, so I want to make up for that in 2026.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>certification</category>
      <category>devops</category>
      <category>learning</category>
    </item>
    <item>
      <title>I Automated My Air Conditioner with Kubernetes (kind + CronJob + SwitchBot)</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Fri, 26 Dec 2025 08:06:13 +0000</pubDate>
      <link>https://dev.to/suzuki0430/i-automated-my-air-conditioner-with-kubernetes-kind-cronjob-switchbot-1mpi</link>
      <guid>https://dev.to/suzuki0430/i-automated-my-air-conditioner-with-kubernetes-kind-cronjob-switchbot-1mpi</guid>
      <description>&lt;p&gt;Even though I’m an AWS Community Builder in the Containers category, I had honestly never touched Kubernetes before. I felt a bit guilty about that, so I recently decided to properly start learning it.&lt;/p&gt;

&lt;p&gt;After studying for about a month for the CKA (Certified Kubernetes Administrator) exam, I became familiar with the basic Kubernetes resources and &lt;code&gt;kubectl&lt;/code&gt; commands. At that point, I wanted to actually try using Kubernetes resources end-to-end in a real project.&lt;/p&gt;

&lt;p&gt;Around the same time, I realized that the SwitchBot Hub Mini I had bought during Black Friday might be perfect for a small hands-on experiment, so I decided to play around with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.switch-bot.com/products/switchbot-hub-mini?_pos=1&amp;amp;_psq=mini&amp;amp;_ss=e&amp;amp;_v=1.0" rel="noopener noreferrer"&gt;SwitchBot Hub Mini&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%2Fakegg5zzoqfh6a2iqfeu.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fakegg5zzoqfh6a2iqfeu.jpeg" alt="IMG\_9424 (2).jpg" width="300" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a first step, I built a Kubernetes cluster locally on my Mac using kind, and created a CronJob that turns on the air conditioner in my bedroom every day at 7:00 PM(JST).&lt;/p&gt;

&lt;p&gt;The total implementation time was about 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  SwitchBot Hub Mini Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The SwitchBot app is installed&lt;/li&gt;
&lt;li&gt;A SwitchBot account has been created&lt;/li&gt;
&lt;li&gt;The air conditioner remote has already been registered in the app&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Getting the Token / Secret Key / Device ID
&lt;/h3&gt;

&lt;p&gt;Open Profile &amp;gt; Preferences &amp;gt; About in the SwitchBot app.&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%2Fjgjl34fdd0bn62ej934e.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%2Fjgjl34fdd0bn62ej934e.png" alt="IMG\_9425 (2).png" width="200" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tap App Version repeatedly, and Developer Options will appear.&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%2F1autju44pf4m5yu8yubk.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%2F1autju44pf4m5yu8yubk.png" alt="image.png" width="200" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make a note of your Token and Secret Key.&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%2F4i0q5z4jrlo291zvtl56.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%2F4i0q5z4jrlo291zvtl56.png" alt="IMG\_9427 (2).png" width="200" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using the obtained Token, call the SwitchBot API to retrieve the list of registered devices, and make a note of the Device ID (&lt;code&gt;deviceId&lt;/code&gt;) for your air conditioner.&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="s2"&gt;"https://api.switch-bot.com/v1.0/devices"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nb"&gt;.&lt;/span&gt;

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   350  100   350    0     0   1523      0 &lt;span class="nt"&gt;--&lt;/span&gt;:--:-- &lt;span class="nt"&gt;--&lt;/span&gt;:--:-- &lt;span class="nt"&gt;--&lt;/span&gt;:--:--  1528
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"statusCode"&lt;/span&gt;: 100,
  &lt;span class="s2"&gt;"body"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"deviceList"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"deviceId"&lt;/span&gt;: &lt;span class="s2"&gt;"xxxxxxxxxx"&lt;/span&gt;,
        &lt;span class="s2"&gt;"deviceName"&lt;/span&gt;: &lt;span class="s2"&gt;"Hub Mini E7"&lt;/span&gt;,
        &lt;span class="s2"&gt;"deviceType"&lt;/span&gt;: &lt;span class="s2"&gt;"Hub Mini"&lt;/span&gt;,
        &lt;span class="s2"&gt;"enableCloudService"&lt;/span&gt;: &lt;span class="nb"&gt;true&lt;/span&gt;,
        &lt;span class="s2"&gt;"hubDeviceId"&lt;/span&gt;: &lt;span class="s2"&gt;"000000000000"&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;,
    &lt;span class="s2"&gt;"infraredRemoteList"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"deviceId"&lt;/span&gt;: &lt;span class="s2"&gt;"xx-xxxxxxxx-xxxx"&lt;/span&gt;,
        &lt;span class="s2"&gt;"deviceName"&lt;/span&gt;: &lt;span class="s2"&gt;"Air Conditioner"&lt;/span&gt;,
        &lt;span class="s2"&gt;"remoteType"&lt;/span&gt;: &lt;span class="s2"&gt;"Air Conditioner"&lt;/span&gt;,
        &lt;span class="s2"&gt;"hubDeviceId"&lt;/span&gt;: &lt;span class="s2"&gt;"xxxxxxxxxx"&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;,
  &lt;span class="s2"&gt;"message"&lt;/span&gt;: &lt;span class="s2"&gt;"success"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting Up the Kubernetes Environment (kind)
&lt;/h2&gt;

&lt;p&gt;This time, I used kind to create a local Kubernetes cluster on my Mac.&lt;br&gt;
As long as you have Docker Desktop installed, you’re good to go.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://kind.sigs.k8s.io/" rel="noopener noreferrer"&gt;kind&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cluster can be created with just two commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;kind
kind create cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the node status is &lt;code&gt;Ready&lt;/code&gt;, everything is set.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get nodes

NAME                 STATUS   ROLES           AGE    VERSION
kind-control-plane   Ready    control-plane   3m9s   v1.35.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Application Structure
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Repository
&lt;/h3&gt;

&lt;p&gt;Since the codebase is quite small, it’s probably fastest to just look at the repository directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/suzuki0430/switchbot-k8s-cronjob" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Directory Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;switchbot-k8s-cronjob/
├── README.md
├── Makefile
├── docker/
│   └── Dockerfile
├── app/
│   ├── switchbot.py # Simply calls the SwitchBot API
│   └── requirements.txt
├── k8s/
│   ├── cronjob.yaml
│   ├── secret.example.yaml # Tokens are managed via Kubernetes Secrets
│   └── namespace.yaml
└── .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What I Got Stuck On
&lt;/h3&gt;

&lt;p&gt;At first, the CronJob kept failing with &lt;code&gt;ImagePullBackOff&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In a kind cluster, Docker images built locally cannot be pulled automatically.&lt;br&gt;
Because of this, it was necessary to set &lt;code&gt;imagePullPolicy: Never&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;batch/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;CronJob&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;switchbot-cron&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;switchbot&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;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&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;19&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;*'&lt;/span&gt; &lt;span class="c1"&gt;# every day at 19:00&lt;/span&gt;
  &lt;span class="na"&gt;timeZone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Asia/Tokyo'&lt;/span&gt; &lt;span class="c1"&gt;# JST&lt;/span&gt;
  &lt;span class="na"&gt;jobTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&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;template&lt;/span&gt;&lt;span class="pi"&gt;:&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;switchbot&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;switchbot-job&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;Never&lt;/span&gt; &lt;span class="c1"&gt;# NOTE: use local image&lt;/span&gt;
              &lt;span class="na"&gt;envFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secretRef&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;switchbot-secret&lt;/span&gt;
          &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Even though CronJob itself is outside the CKA exam scope, it was great to see that with just the knowledge gained from CKA study, I could build something that actually works in about 30 minutes.&lt;/p&gt;

&lt;p&gt;There are plenty of other things I could try with this setup, so I’m planning to experiment a bit more during the New Year holidays.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>containers</category>
      <category>iot</category>
      <category>docker</category>
    </item>
    <item>
      <title>How to Re-Encrypt Aurora Snapshots with a CMK for Cross-Account Migration</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Fri, 14 Nov 2025 07:33:15 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-to-re-encrypt-aurora-snapshots-with-a-cmk-for-cross-account-migration-1abm</link>
      <guid>https://dev.to/aws-builders/how-to-re-encrypt-aurora-snapshots-with-a-cmk-for-cross-account-migration-1abm</guid>
      <description>&lt;p&gt;When we rebuilt our AWS Organizations account structure, we needed to migrate the Aurora data from the old account to the new one as-is.&lt;/p&gt;

&lt;p&gt;However, the Aurora cluster in the old account was encrypted using the AWS-managed key (&lt;code&gt;aws/rds&lt;/code&gt;). After some investigation, we learned that &lt;code&gt;aws/rds&lt;/code&gt; cannot be shared across accounts, meaning we couldn’t simply copy the snapshot to the new account...&lt;/p&gt;

&lt;p&gt;To address this, we created customer-managed keys (CMKs) in both the old and new accounts, re-encrypted the snapshot using the CMK in the old account, and then copied/restored it to the new account.&lt;/p&gt;

&lt;p&gt;We ran into several subtle issues with KMS key policies, so here is a complete step-by-step record for future reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall Flow
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Account&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Old&lt;/td&gt;
&lt;td&gt;Create CMK for snapshot encryption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Old&lt;/td&gt;
&lt;td&gt;Create Aurora snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Old&lt;/td&gt;
&lt;td&gt;Copy snapshot with re-encryption using old CMK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Old&lt;/td&gt;
&lt;td&gt;Share CMK-encrypted snapshot with new account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;New&lt;/td&gt;
&lt;td&gt;Create CMK for re-encryption in new account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;New&lt;/td&gt;
&lt;td&gt;Re-encrypt copied snapshot with new CMK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;New&lt;/td&gt;
&lt;td&gt;Restore Aurora from CMK-encrypted snapshot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1jgeutrxsg4gekqnwcp.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%2Fh1jgeutrxsg4gekqnwcp.png" alt=" " width="800" height="157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create a CMK for snapshot encryption in the old account
&lt;/h3&gt;

&lt;p&gt;Create a CMK in the same region as the Aurora cluster.&lt;br&gt;
Here is the key policy required for cross-account snapshot sharing.&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;"AllowOldAccountAdmin"&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::&amp;lt;OLD_ACCOUNT_ID&amp;gt;: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;"kms:*"&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="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;"AllowUseFromOldAccountViaRDS"&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::&amp;lt;OLD_ACCOUNT_ID&amp;gt;: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="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:Encrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:ReEncryptFrom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:ReEncryptTo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:GenerateDataKey*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:DescribeKey"&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;"kms:ViaService"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rds.&amp;lt;REGION&amp;gt;.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="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;"AllowUseFromNewAccountViaRDS"&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::&amp;lt;NEW_ACCOUNT_ID&amp;gt;: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="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:ReEncryptFrom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:CreateGrant"&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;"kms:ViaService"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rds.&amp;lt;REGION&amp;gt;.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;h4&gt;
  
  
  Note 1: Why &lt;code&gt;ReEncryptFrom&lt;/code&gt; / &lt;code&gt;ReEncryptTo&lt;/code&gt; are required
&lt;/h4&gt;

&lt;p&gt;To share Aurora snapshots across accounts, encryption must transition through multiple layers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws/rds&lt;/code&gt; → old CMK → new CMK&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReEncryptFrom&lt;/code&gt; is needed to decrypt the existing key, and is used during.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AllowUseFromOldAccountViaRDS&lt;/code&gt; → decrypting snapshot encrypted with &lt;code&gt;aws/rds&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AllowUseFromNewAccountViaRDS&lt;/code&gt; → decrypting snapshot encrypted with old CMK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ReEncryptTo&lt;/code&gt; is needed to encrypt data with the new key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used during Step 3 to re-encrypt with old CMK&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Note 2: Creating a CMK via the AWS Console
&lt;/h4&gt;

&lt;p&gt;Refer to the following screenshots.&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%2F95rqiry6u2mx89qf2cw7.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%2F95rqiry6u2mx89qf2cw7.png" alt=" " width="800" height="416"&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%2Fzwjl1ah7junrkdmwmb9o.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%2Fzwjl1ah7junrkdmwmb9o.png" alt=" " width="800" height="351"&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%2Fsow5z4tmhe7vmg2ni08i.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%2Fsow5z4tmhe7vmg2ni08i.png" alt=" " width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Create an Aurora snapshot
&lt;/h3&gt;

&lt;p&gt;From console or CLI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds create-db-cluster-snapshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-cluster-snapshot-identifier&lt;/span&gt; &amp;lt;snapshot&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-cluster-identifier&lt;/span&gt; &amp;lt;cluster&amp;gt;
&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.amazonaws.com%2Fuploads%2Farticles%2Fwyy1rw2zk1jb4koy0th4.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%2Fwyy1rw2zk1jb4koy0th4.png" alt=" " width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Copy and re-encrypt snapshot using the old account’s CMK
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds copy-db-cluster-snapshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-db-cluster-snapshot-identifier&lt;/span&gt; arn:aws:rds:REGION:OLD_ACCOUNT:cluster-snapshot:my-snap &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-db-cluster-snapshot-identifier&lt;/span&gt; my-snap-cmk &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kms-key-id&lt;/span&gt; arn:aws:kms:REGION:OLD_ACCOUNT:key/&amp;lt;CMK-old&amp;gt;
&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.amazonaws.com%2Fuploads%2Farticles%2F4hkzs09b6sbvdhmsxocj.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%2F4hkzs09b6sbvdhmsxocj.png" alt=" " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Share the CMK-encrypted snapshot with the new account
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds modify-db-cluster-snapshot-attribute &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-cluster-snapshot-identifier&lt;/span&gt; my-snap-cmk &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attribute-name&lt;/span&gt; restore &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--values-to-add&lt;/span&gt; NEW_ACCOUNT_ID
&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.amazonaws.com%2Fuploads%2Farticles%2Fss3s7xsl6apvfstsg84s.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%2Fss3s7xsl6apvfstsg84s.png" alt=" " width="800" height="98"&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%2Fzdyfcq69pbutp0jzhity.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%2Fzdyfcq69pbutp0jzhity.png" alt=" " width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once shared successfully, the new account should see the 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%2F2t58orffkw2nl7ctbhz2.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%2F2t58orffkw2nl7ctbhz2.png" alt=" " width="800" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Create a new CMK in the new account
&lt;/h3&gt;

&lt;p&gt;This CMK will be used to re-encrypt the snapshot again.&lt;br&gt;
To re-encrypt the snapshot using the new CMK, you need to grant the ReEncryptTo permission in the key policy.&lt;/p&gt;

&lt;p&gt;Key policy example.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"Enable IAM User Permissions"&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::&amp;lt;NEW_ACCOUNT_ID&amp;gt;: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;"kms:*"&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="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;"Allow rds.amazonaws.com to use the key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rds.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:ReEncryptTo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:GenerateDataKey*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:Encrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kms:CreateGrant"&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;h3&gt;
  
  
  6. Copy and re-encrypt the shared snapshot using the new CMK
&lt;/h3&gt;

&lt;p&gt;You then copy the snapshot again using the new CMK.&lt;br&gt;
Internally, the encrypted data is extracted using the old CMK’s ReEncryptFrom permission, and then re-encrypted using the new CMK’s ReEncryptTo permission.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds copy-db-cluster-snapshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-db-cluster-snapshot-identifier&lt;/span&gt; arn:aws:rds:REGION:OLD_ACCOUNT:cluster-snapshot:my-snap-cmk &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-db-cluster-snapshot-identifier&lt;/span&gt; my-snap-new &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kms-key-id&lt;/span&gt; arn:aws:kms:REGION:NEW_ACCOUNT:key/&amp;lt;CMK-new&amp;gt;
&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.amazonaws.com%2Fuploads%2Farticles%2Fd7c9v7ufvi9fuqk7qvk0.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%2Fd7c9v7ufvi9fuqk7qvk0.png" alt=" " width="800" height="204"&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%2Fa70qm5l84exrtxt1nkx0.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%2Fa70qm5l84exrtxt1nkx0.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Restore Aurora from the re-encrypted snapshot in the new account
&lt;/h3&gt;

&lt;p&gt;After that, restore the Aurora cluster from the snapshot you created.&lt;/p&gt;

&lt;p&gt;If you are restoring it using Terraform, the configuration will look like this.&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_rds_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_identifier&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"example-cluster"&lt;/span&gt;
  &lt;span class="nx"&gt;snapshot_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:rds:REGION:NEW_ACCOUNT:cluster-snapshot:my-snap-new"&lt;/span&gt;
  &lt;span class="nx"&gt;kms_key_id&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aurora-mysql"&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_rds_cluster_instance"&lt;/span&gt; &lt;span class="s2"&gt;"example_instances"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="nx"&gt;identifier&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"example-cluster-${count.index}"&lt;/span&gt;
  &lt;span class="nx"&gt;cluster_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_rds_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&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_class&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.r8g.large"&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_rds_cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&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.amazonaws.com%2Fuploads%2Farticles%2F520hbp7k0qktokh1kmxh.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%2F520hbp7k0qktokh1kmxh.png" alt=" " width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;If you foresee migrating from a single-account setup to a multi-account structure, it’s much easier to use a CMK from the beginning. While it does cost more, it can save a huge amount of migration and operational effort later — making it well worth the investment.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>security</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Practical Terraform Tips for Secure and Reliable AWS Environments</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Fri, 10 Oct 2025 13:12:12 +0000</pubDate>
      <link>https://dev.to/aws-builders/practical-terraform-tips-for-secure-and-reliable-aws-environments-19n0</link>
      <guid>https://dev.to/aws-builders/practical-terraform-tips-for-secure-and-reliable-aws-environments-19n0</guid>
      <description>&lt;p&gt;Recently, I had the opportunity to migrate our AWS setup from a single account to a multi-account architecture, which led me to completely redesign my Terraform repository from scratch.&lt;/p&gt;

&lt;p&gt;Although both HashiCorp and AWS now officially provide MCP-based Terraform integrations, I wanted to document some practical tips and lessons I’ve picked up through the redesign process. Before implementing them, I studied this excellent book:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.oreilly.co.jp/books/9784814400133/" rel="noopener noreferrer"&gt;https://www.oreilly.co.jp/books/9784814400133/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I initially thought I already knew most of the content, but as I flipped through it (well, “swiped” through on Kindle), I realized how many important things I had overlooked.&lt;br&gt;
It was honestly worth every yen, and I highly recommend it to anyone who’s been using Terraform “somewhat casually.”&lt;/p&gt;

&lt;p&gt;In this post, I’ll introduce several beginner-friendly tips that I actually adopted in my AWS environment and found helpful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Automatically Generate Module READMEs with &lt;code&gt;terraform-docs&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When I was managing the infrastructure alone, I didn’t really think about documentation for other team members. But as the team grew, I realized how bad tribal knowledge can be.&lt;/p&gt;

&lt;p&gt;To address this, I started using &lt;code&gt;terraform-docs&lt;/code&gt; to automatically generate documentation for each module’s Inputs, Outputs, and Resources.&lt;/p&gt;

&lt;p&gt;Here’s my &lt;code&gt;.terraform-docs.yml&lt;/code&gt; configuration:&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;formatter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;markdown&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;table"&lt;/span&gt;
&lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;show&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;header&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;requirements&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;providers&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;modules&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;resources&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;inputs&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;outputs&lt;/span&gt;
&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;README.md"&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inject&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;!-- BEGIN_TF_DOCS --&amp;gt;&lt;/span&gt;
    &lt;span class="s"&gt;{{ .Content }}&lt;/span&gt;
    &lt;span class="s"&gt;&amp;lt;!-- END_TF_DOCS --&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;name&lt;/span&gt;
&lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hide-empty&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;sensitive&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;anchor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I created a helper script to generate docs for all modules:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; terraform-docs &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ERROR: terraform-docs not found. Please install it."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BASH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&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;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;CONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ROOT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.terraform-docs.yml"&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ROOT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/modules"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;module_dir &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;/ &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$module_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"📝 Generating docs for &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;module_dir&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;pushd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$module_dir&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"README.md"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; README.md &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
# Module
&amp;lt;!-- BEGIN_TF_DOCS --&amp;gt;
&amp;lt;!-- END_TF_DOCS --&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;  &lt;span class="k"&gt;fi

  &lt;/span&gt;terraform-docs &lt;span class="nt"&gt;--config&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONFIG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
  &lt;span class="nb"&gt;popd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, CI automatically updates each module’s README when changes are made — preventing missing documentation in pull requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 Backend Locking with &lt;code&gt;use_lockfile&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When configuring the S3 backend in &lt;code&gt;versions.tf&lt;/code&gt;, I added the &lt;code&gt;use_lockfile = true&lt;/code&gt; option to prevent state inconsistencies during simultaneous &lt;code&gt;terraform plan&lt;/code&gt; or &lt;code&gt;apply&lt;/code&gt; executions.&lt;/p&gt;

&lt;p&gt;This feature became available starting with Terraform v1.11 — previously, you needed a separate DynamoDB table for state locking.&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.13.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; 6.11.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-state"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"environments/dev/terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-northeast-1"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&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;use_lockfile&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Enable Versioning on the S3 State Bucket
&lt;/h2&gt;

&lt;p&gt;Always enable versioning on your state bucket.&lt;br&gt;
I learned this the hard way — in a previous setup, we forgot to do this, and I shudder to think what could have happened if someone deleted the state file 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.amazonaws.com%2Fuploads%2Farticles%2F7i9z19z0d3elz2nz1dbz.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%2F7i9z19z0d3elz2nz1dbz.png" alt=" " width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Simplify Subnet Calculation with &lt;code&gt;cidrsubnet&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I used to manually calculate subnet ranges from the VPC CIDR and hardcode them, but &lt;code&gt;cidrsubnet()&lt;/code&gt; makes this much simpler:&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;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_cidr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"192.168.0.0/20"&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_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"a"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cidrsubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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="c1"&gt;# 192.168.0.0/24&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_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"b"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cidrsubnet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# 192.168.1.0/24&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small but very handy function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use &lt;code&gt;default_tags&lt;/code&gt; to Avoid Tag Omissions
&lt;/h2&gt;

&lt;p&gt;Previously, I manually added common tags like &lt;code&gt;Project&lt;/code&gt; and &lt;code&gt;Environment&lt;/code&gt; to each resource. Naturally, some were missing.&lt;br&gt;
With provider-level &lt;code&gt;default_tags&lt;/code&gt;, all resources now include them automatically.&lt;/p&gt;

&lt;p&gt;I also added &lt;code&gt;ManagedBy = "terraform"&lt;/code&gt; for quick visual identification in the AWS Console.&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;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="s2"&gt;"ap-northeast-1"&lt;/span&gt;
  &lt;span class="nx"&gt;default_tags&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;Project&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"MyApp"&lt;/span&gt;
      &lt;span class="nx"&gt;Environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"prod"&lt;/span&gt;
      &lt;span class="nx"&gt;ManagedBy&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform"&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;
  
  
  Protect Critical Resources with &lt;code&gt;prevent_destroy&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;I once accidentally deleted an ElastiCache (Valkey) cluster because I didn’t review the plan carefully enough before applying.&lt;br&gt;
To avoid repeating that mistake, I now set &lt;code&gt;prevent_destroy = true&lt;/code&gt; for critical resources like databases and KMS keys.&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_rds_cluster"&lt;/span&gt; &lt;span class="s2"&gt;"aurora_cluster"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;prevent_destroy&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also use &lt;code&gt;ignore_changes&lt;/code&gt; to skip diff detection for specific attributes — useful when Blue/Green deployments cause dynamic changes like target group switches.&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_lb_listener_rule"&lt;/span&gt; &lt;span class="s2"&gt;"rules"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&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;listener_rules&lt;/span&gt;

  &lt;span class="nx"&gt;listener_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_listener&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tostring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&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;listener_port&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&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;priority&lt;/span&gt;

  &lt;span class="nx"&gt;action&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;each&lt;/span&gt;&lt;span class="p"&gt;.&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;action_type&lt;/span&gt;
    &lt;span class="nx"&gt;target_group_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&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;target_group&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;host_header&lt;/span&gt; &lt;span class="p"&gt;{&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;each&lt;/span&gt;&lt;span class="p"&gt;.&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;host&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;dynamic&lt;/span&gt; &lt;span class="s2"&gt;"condition"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&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;paths&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&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;paths&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;path_pattern&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;values&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;value&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;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ignore_changes&lt;/span&gt; &lt;span class="p"&gt;=&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;target_group_arn&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;
  
  
  Import Existing Resources with the &lt;code&gt;import&lt;/code&gt; Block
&lt;/h2&gt;

&lt;p&gt;For manually created resources (e.g., S3, Amplify, AppConfig), I used to run &lt;code&gt;terraform import&lt;/code&gt; via CLI.&lt;br&gt;
Since Terraform 1.5.0, the &lt;code&gt;import&lt;/code&gt; block allows for a more declarative and readable approach.&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;"example"&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;"my-bucket"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;to&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;example&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-bucket"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run &lt;code&gt;terraform plan&lt;/code&gt;, Terraform will display the differences between the existing resources and your code.&lt;br&gt;
Update your configuration to match the actual state, and then run &lt;code&gt;terraform apply&lt;/code&gt; — the resources will be imported into your state file.&lt;/p&gt;

&lt;p&gt;You can also auto-generate configurations using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform plan &lt;span class="nt"&gt;-generate-config-out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;generated.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rename Resources Without Recreating Them via &lt;code&gt;terraform state mv&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you mistakenly name a resource (e.g., &lt;code&gt;aws_s3_bucket.exampl&lt;/code&gt;), renaming it directly in code can trigger a destroy/create.&lt;br&gt;
Instead, use &lt;code&gt;terraform state mv&lt;/code&gt; to safely rename without recreating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform state &lt;span class="nb"&gt;mv &lt;/span&gt;aws_s3_bucket.exampl aws_s3_bucket.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always back up your state before doing this — versioning helps!&lt;/p&gt;

&lt;h2&gt;
  
  
  Granularity of Environment File Splitting
&lt;/h2&gt;

&lt;p&gt;The part I struggled with the most during the repository redesign was how to structure the files under &lt;code&gt;environments/{env}/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At first, I dumped everything into a single &lt;code&gt;main.tf&lt;/code&gt; file.&lt;br&gt;
But as the number of resources grew, the file quickly became bloated — and scrolling through it turned into pure hell.&lt;br&gt;
So I decided to split the configuration by functional domains.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;applications.tf      # Cognito, Amplify, SES
compute.tf           # ECS, ECR, ALB, Auto Scaling
db.tf                # RDS, ElastiCache
monitoring.tf        # CloudWatch Logs, SNS, Chatbot
networking.tf        # VPC, Subnet, Route Table
security.tf          # IAM, Security Group, KMS
s3.tf
lambda.tf
locals.tf
providers.tf
versions.tf
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When deciding how to split files, my guiding principle was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“When I need to modify this resource, which file do I want to open?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example, when adding a new ALB listener rule, I usually need to check the ECS service and target groups at the same time — so I grouped them together in &lt;code&gt;compute.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="c1"&gt;# compute.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"ecs"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"ecr"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"alb"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"ecs_autoscaling_visitor"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"ecs_autoscaling_company"&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;On the other hand, for networking, I often need to see the VPC, subnets, and route tables as a set, so I put them all together in &lt;code&gt;networking.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="c1"&gt;# networking.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"subnet"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"route_table"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"internet_gateway"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"nat_gateway"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"vpc_endpoint"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"network_associations"&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;However, &lt;code&gt;security.tf&lt;/code&gt; has now grown to over 1,000 lines…&lt;br&gt;
That’s what happens when you pack IAM roles, policies, security groups, and KMS keys all in one file.&lt;/p&gt;

&lt;p&gt;In hindsight, I probably should have split it up like this (and plan to refactor soon):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;iam.tf&lt;/code&gt; – IAM roles and policies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;security_groups.tf&lt;/code&gt; – Security groups&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kms.tf&lt;/code&gt; – KMS keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the flip side, grouping ElastiCache and Aurora together in &lt;code&gt;db.tf&lt;/code&gt; turned out to be a great decision.&lt;br&gt;
Database-related changes often involve both cache and Aurora, so this granularity feels just right.&lt;/p&gt;

&lt;p&gt;I also really like &lt;code&gt;applications.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="c1"&gt;# applications.tf&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"cognito"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"amplify"&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;module&lt;/span&gt; &lt;span class="s2"&gt;"ses"&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;This file collects application-layer services such as authentication, frontend hosting, and email delivery.&lt;br&gt;
Whenever I add new user-facing functionality, this is usually the file I open first.&lt;/p&gt;

&lt;p&gt;In the end, there’s no single “correct” answer for file granularity.&lt;br&gt;
The right structure depends on your team’s development style and the project’s scale.&lt;br&gt;
In my case, I settled on a simple rule of thumb:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Group resources that are closely related, and separate those with different update frequencies.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That balance has worked very well for us.&lt;/p&gt;

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

&lt;p&gt;The reference book above contains far more useful knowledge beyond these tips.&lt;br&gt;
If you’re using Terraform somewhat intuitively, I strongly recommend revisiting your setup with these practices in mind.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developer.hashicorp.com/terraform/tutorials/aws/aws-default-tags" rel="noopener noreferrer"&gt;Configure default tags for AWS resources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How I Combined Strands Agents, Bedrock AgentCore Runtime, and AgentCore Browser to Automate AWS Docs</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Mon, 15 Sep 2025 02:20:51 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-i-combined-strands-agents-bedrock-agentcore-runtime-and-agentcore-browser-to-automate-aws-docs-50nd</link>
      <guid>https://dev.to/aws-builders/how-i-combined-strands-agents-bedrock-agentcore-runtime-and-agentcore-browser-to-automate-aws-docs-50nd</guid>
      <description>&lt;p&gt;This summer, I barely had any time or energy to explore new technologies due to internal troubles at work and a family member being hospitalized. But by mid-September, I finally got some breathing room—so I decided to dive into Amazon Bedrock AgentCore, which had been generating buzz since July.&lt;/p&gt;

&lt;p&gt;AgentCore is a suite of services designed for production-grade AI agent operations. Its core execution platform, AgentCore Runtime, quickly caught attention for being extremely easy to deploy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/aws/introducing-amazon-bedrock-agentcore-securely-deploy-and-operate-ai-agents-at-any-scale/" rel="noopener noreferrer"&gt;Introducing Amazon Bedrock AgentCore: Securely deploy and operate AI agents at any scale (preview)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since I had the chance, I also tried combining the AgentCore Browser, a managed browser for agents, to experiment with browser automation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://catalog.us-east-1.prod.workshops.aws/workshops/015a2de4-9522-4532-b2eb-639280dc31d8/en-US/60-agentcore-tools/62-browser-tool" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Browser Tool&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I created an agent with Strands Agents, deployed it on AgentCore Runtime, and connected it to the AgentCore Browser through Browser Use.&lt;/p&gt;

&lt;p&gt;The goal: when asked something like&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How can I create a lifecycle policy for an S3 bucket?”,&lt;br&gt;
the agent should navigate through official Amazon S3 documentation pages, extract key information, and generate an answer.&lt;/p&gt;
&lt;/blockquote&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%2Fu6bkvqikknkpay97nfpo.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%2Fu6bkvqikknkpay97nfpo.png" alt="image.png" width="800" height="353"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.11+&lt;/li&gt;
&lt;li&gt;AWS account with configured credentials&lt;/li&gt;
&lt;li&gt;Region: &lt;code&gt;us-west-2&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Unfortunately, AgentCore is not yet available in &lt;code&gt;ap-northeast-1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Access to the Claude Sonnet 4 base model

&lt;ul&gt;
&lt;li&gt;I used &lt;code&gt;us.anthropic.claude-sonnet-4-20250514-v1:0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Code Overview
&lt;/h2&gt;

&lt;p&gt;Here are the main points of the implementation.&lt;br&gt;
The full source code is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/suzuki0430/strands-agentcore-browseruse" rel="noopener noreferrer"&gt;strands-agentcore-browseruse&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Project Structure
&lt;/h3&gt;

&lt;p&gt;Since there’s only one browser tool, I kept it simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── agents/
│   └── main_agent.py      # Main agent
├── runtime/
│   └── agentcore_app.py   # Integration with AgentCore Runtime
└── tools/
    └── browser_tool.py    # Browser automation logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Implementing the Main Agent
&lt;/h3&gt;

&lt;p&gt;The agent is built using Strands Agents.&lt;br&gt;
It uses Claude Sonnet 4 as the model and the &lt;code&gt;browse_url_tool&lt;/code&gt; for browser interaction.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create AWS Docs search agent&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-sonnet-4-20250514-v1: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;params&lt;/span&gt;&lt;span class="o"&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;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top_p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;us-west-2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;read_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&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;aws_docs_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;browse_url_tool&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connecting to AgentCore Runtime
&lt;/h3&gt;

&lt;p&gt;Integrating with AgentCore Runtime is surprisingly simple.&lt;br&gt;
Just use the &lt;code&gt;@app.entrypoint&lt;/code&gt; decorator to expose an endpoint that accepts requests.&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="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&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;prompt&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="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&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;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;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Browser Automation Logic
&lt;/h3&gt;

&lt;p&gt;This is the key part.&lt;br&gt;
It launches AgentCore Browser and controls it from Browser Use via Chrome DevTools Protocol (CDP).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start an AgentCore Browser session&lt;/li&gt;
&lt;li&gt;Get the CDP WebSocket URL&lt;/li&gt;
&lt;li&gt;Pass it to Browser Use&lt;/li&gt;
&lt;li&gt;Provide proper authentication headers
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute_browser_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;starting_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://docs.aws.amazon.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BrowserClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="n"&gt;ws_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_ws_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

          &lt;span class="n"&gt;browser_profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BrowserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;180000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="n"&gt;browser_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BrowserSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="n"&gt;cdp_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ws_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;browser_profile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;browser_profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;keep_alive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

          &lt;span class="n"&gt;browser_use_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BrowserUseAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bedrock_chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="n"&gt;browser_session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;browser_session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser_use_agent&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

      &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;browser_session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;&lt;br&gt;
Without pinning the version of &lt;code&gt;browser-use&lt;/code&gt;, I ran into WebSocket 403 errors.&lt;br&gt;
Be sure to specify it in your &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser-use&amp;lt;0.3.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Deploying to AgentCore Runtime
&lt;/h2&gt;

&lt;p&gt;Create a virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the initial setup (this creates IAM roles, an Amazon Elastic Container Registry repo, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore configure &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; src/runtime/agentcore_app.py &lt;span class="nt"&gt;--region&lt;/span&gt; us-west-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll need to manually attach this policy to the &lt;code&gt;AmazonBedrockAgentCoreSDKRuntime-us-west-2-~&lt;/code&gt; IAM role created by &lt;code&gt;agentcore configure&lt;/code&gt; so it can use AgentCore Browser:&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;"BrowserAccess"&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;"bedrock-agentcore:StartBrowserSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:StopBrowserSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:ConnectBrowserAutomationStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:ConnectBrowserLiveViewStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:ListBrowserSessions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:GetBrowserSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:ListBrowsers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:GetBrowser"&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;&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%2Fyn1ctscqejr2uzgboibp.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%2Fyn1ctscqejr2uzgboibp.png" alt="screenshot.png" width="800" height="634"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore launch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore launch &lt;span class="nt"&gt;--auto-update-on-conflict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Execution Result
&lt;/h2&gt;

&lt;p&gt;After deployment, I tested it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="s1"&gt;'{"prompt": "How can I create a lifecycle policy for an S3 bucket?"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It took about 10 minutes (long), but the response was a fully detailed explanation of how to set up Amazon S3 lifecycle policies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Response:
{"result": "Based on my research of the official AWS documentation, here's a comprehensive guide on how to create an S3 bucket lifecycle policy:\n\n## How to Create an S3 Bucket
Lifecycle Policy\n\nS3 lifecycle policies help you automatically manage your objects throughout their lifecycle, transitioning them between different storage classes and eventually
expiring them to optimize costs. Here are the different methods to create them:\n\n### **1. Using AWS CLI**\n\nThe most direct programmatic approach:\n\n**Basic
Command:**\n```

bash\naws s3api put-bucket-lifecycle-configuration --bucket YOUR-BUCKET-NAME --lifecycle-configuration file://lifecycle.json\n

```\n\n**Example JSON Configuration
File (lifecycle.json):**\n```

json\n{\n  \"Rules\": [\n    {\n      \"ID\": \"ExampleRule\",\n      \"Status\": \"Enabled\",\n      \"Filter\": {\n        \"Prefix\":
\"documents/\"\n      },\n      \"Transitions\": [\n        {\n          \"Days\": 30,\n          \"StorageClass\": \"STANDARD_IA\"\n        },\n        {\n          \"Days\":
90,\n          \"StorageClass\": \"GLACIER\"\n        }\n      ],\n      \"Expiration\": {\n        \"Days\": 365\n      }\n    }\n  ]\n}\n

```\n\n**Additional CLI Commands:**\n-
**View existing policy:** `aws s3api get-bucket-lifecycle-configuration --bucket YOUR-BUCKET-NAME`\n- **Delete policy:** `aws s3api delete-bucket-lifecycle --bucket
YOUR-BUCKET-NAME`\n\n### **2. Using AWS Management Console**\n1. Navigate to the S3 console\n2. Select your bucket\n3. Go to the \"Management\" tab\n4. Click \"Create lifecycle
rule\"\n5. Configure your rule settings (filters, transitions, expiration)\n6. Review and create the rule\n\n### **3. Using REST API**\nThe API operations available:\n-
**PutBucketLifecycleConfiguration**\n- **GetBucketLifecycleConfiguration** \n- **DeleteBucketLifecycle**\n\n### **4. Using AWS SDKs**\nAvailable for Java, .NET, Ruby, Python, and
other languages with complete lifecycle management capabilities.\n\n## **Key Configuration Elements**\n\n### **Basic XML Structure:**\n```

xml\n&amp;lt;LifecycleConfiguration&amp;gt;\n  &amp;lt;Rule&amp;gt;\n
&amp;lt;ID&amp;gt;Unique-Rule-Name&amp;lt;/ID&amp;gt;\n    &amp;lt;Filter&amp;gt;\n      &amp;lt;Prefix&amp;gt;folder/&amp;lt;/Prefix&amp;gt;\n    &amp;lt;/Filter&amp;gt;\n    &amp;lt;Status&amp;gt;Enabled&amp;lt;/Status&amp;gt;\n    &amp;lt;Transition&amp;gt;\n      &amp;lt;Days&amp;gt;365&amp;lt;/Days&amp;gt;\n
&amp;lt;StorageClass&amp;gt;GLACIER&amp;lt;/StorageClass&amp;gt;\n    &amp;lt;/Transition&amp;gt;\n    &amp;lt;Expiration&amp;gt;\n      &amp;lt;Days&amp;gt;3650&amp;lt;/Days&amp;gt;\n    &amp;lt;/Expiration&amp;gt;\n  &amp;lt;/Rule&amp;gt;\n&amp;lt;/LifecycleConfiguration&amp;gt;\n

```\n\n### **Available
Storage Classes (in transition order):**\n- **STANDARD** → **STANDARD_IA** → **GLACIER** → **DEEP_ARCHIVE**\n\n### **Filter Options:**\n- **Prefix**: Target objects with specific
key prefixes\n- **Tags**: Apply rules based on object tags\n- **Size**: Filter by object size (ObjectSizeGreaterThan/LessThan)\n- **Combinations**: Use `&amp;lt;And&amp;gt;` to combine multiple
filters\n\n## **Important Best Practices**\n\n1. **Configuration Replacement**: S3 replaces the entire lifecycle configuration when you add a new one. To update, retrieve existing
config first, modify it, then reapply.\n\n2. **Propagation Delay**: Expect a few minutes delay before new configurations take full effect.\n\n3. **Small Objects**: Objects smaller
than 128KB are prevented from transitioning by default (use size filters to override).\n\n4. **Testing**: Always verify your configuration after applying by checking rule counts
and testing with sample objects.\n\n5. **Cost Optimization**: Design rules to tier down storage classes over object lifetime to minimize costs.\n\n6. **Versioning Support**: Use
`NoncurrentVersionTransition` and `NoncurrentVersionExpiration` for versioning-enabled buckets.\n\n## **Common Use Cases**\n\n- **Log Archival**: Move application logs to cheaper
storage after 30 days, archive after 90 days\n- **Backup Management**: Transition backups to Glacier, expire after retention period\n- **Document Management**: Move old documents
through storage tiers based on access patterns\n- **Cleanup**: Remove incomplete multipart uploads and expired delete markers\n\nFor detailed examples, configuration samples, and
troubleshooting guidance, refer to the AWS S3 User Guide on lifecycle configuration.\n\n**Documentation Links:**\n- [S3 Lifecycle Configuration
Examples](https://docs.aws.amazon.com/AmazonS3/latest/userguide/lifecycle-configuration-examples.html)\n- [Setting S3 Lifecycle
Configuration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-set-lifecycle-configuration-intro.html)\n", "status": "success"}
&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.amazonaws.com%2Fuploads%2Farticles%2F7t2c0tj9b6ffjppljg86.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%2F7t2c0tj9b6ffjppljg86.png" alt=" " width="800" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I could also see the Live View of browser operations in the AgentCore Browser console:&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%2Fo7faurayj2clmdkg4aph.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%2Fo7faurayj2clmdkg4aph.png" alt="スクリーンショット 2025-09-13 15.00.24.png" width="800" height="457"&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%2Fscwyij90ktof39qtfpv5.gif" 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%2Fscwyij90ktof39qtfpv5.gif" alt="Videotogif (1).gif" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/awslabs/amazon-bedrock-agentcore-samples/tree/main/01-tutorials/05-AgentCore-tools/02-Agent-Core-browser-tool" rel="noopener noreferrer"&gt;amazon-bedrock-agentcore-samples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://strandsagents.com/latest/" rel="noopener noreferrer"&gt;Strands Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/startup/5min-ai-agent-hosting/" rel="noopener noreferrer"&gt;5分で AI エージェントをデプロイ・ホスティングする – Amazon Bedrock AgentCore Runtime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.classmethod.jp/articles/amazon-bedrock-agentcore-agentcore-browser-sample/" rel="noopener noreferrer"&gt;[Amazon Bedrock AgentCore] AgentCore Browser をAIエージェントで操作してみた&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Fixing the “Invalid Parameter” Error When Registering an SNS Topic for SES Feedback Notifications</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Thu, 11 Sep 2025 01:11:44 +0000</pubDate>
      <link>https://dev.to/aws-builders/fixing-the-invalid-parameter-error-when-registering-an-sns-topic-for-ses-feedback-notifications-264e</link>
      <guid>https://dev.to/aws-builders/fixing-the-invalid-parameter-error-when-registering-an-sns-topic-for-ses-feedback-notifications-264e</guid>
      <description>&lt;p&gt;While migrating an existing service to a new AWS account, I ran into a strange error when trying to set up an SNS topic for SES feedback notifications (Bounce, Complaint, Delivery):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An invalid or out-of-range value was supplied for the input parameter.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had created the SNS topic as a "Standard" type, in the same region as SES, and configured the access policy to allow &lt;code&gt;ses.amazonaws.com&lt;/code&gt; with &lt;code&gt;sns:Publish&lt;/code&gt;. Everything seemed correct, so I couldn’t figure out what was wrong.&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%2Fchu3vdr28bconn5ti8g8.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%2Fchu3vdr28bconn5ti8g8.png" alt="Screenshot 2025-09-11 9.44.56.png" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;The problem turned out to be insufficient KMS key policy permissions on the encryption key used for the SNS topic.&lt;/p&gt;

&lt;p&gt;When publishing to an encrypted SNS topic, the publishing service (in this case, SES) needs permissions for both &lt;code&gt;kms:GenerateDataKey&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt;. The actual encryption/decryption is handled by SNS, but SES must be able to trigger the KMS API calls required for that process.&lt;/p&gt;

&lt;p&gt;However, in this case I had used the AWS managed key &lt;code&gt;alias/aws/sns&lt;/code&gt; for SNS topic encryption—which cannot be edited to adjust the key policy.&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%2Fcc5yjhe1bpdg31ci67np.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%2Fcc5yjhe1bpdg31ci67np.png" alt="Screenshot 2025-09-11 9.47.44.png" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;The workaround was to create a customer managed key (CMK) named &lt;code&gt;sns-ses-dev-1&lt;/code&gt;, attach the following key policy, and configure it for the SNS topic:&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;"AllowSESToUseKMSKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ses.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kms:GenerateDataKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&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;&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%2Ffei86lgprljgd2h6s5sj.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%2Ffei86lgprljgd2h6s5sj.png" alt="Screenshot 2025-09-11 9.49.16.png" width="800" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After applying this, I was finally able to configure the SNS topic for SES feedback notifications successfully.&lt;/p&gt;

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

&lt;p&gt;Since CMKs incur additional cost, it might not be worth enabling SNS topic encryption in development environments at all. Using encryption only in production could be a more balanced approach.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/configure-sns-notifications.html" rel="noopener noreferrer"&gt;Configuring Amazon SNS notifications for Amazon SES&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
    <item>
      <title>AWS Control Tower Landing Zone Setup: Troubleshooting Account Limits and KMS Policies</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Tue, 02 Sep 2025 00:41:04 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-control-tower-landing-zone-setup-troubleshooting-account-limits-and-kms-policies-45me</link>
      <guid>https://dev.to/aws-builders/aws-control-tower-landing-zone-setup-troubleshooting-account-limits-and-kms-policies-45me</guid>
      <description>&lt;p&gt;While setting up a multi-account environment and trying to enable AWS Control Tower, I encountered two mysterious errors. Here's how I resolved them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Account limit reached" error (even though I hadn't reached the limit)&lt;/li&gt;
&lt;li&gt;CloudFormation deployment failure (KMS key policy related)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Account Limit Error
&lt;/h2&gt;

&lt;p&gt;When enabling Control Tower, it creates a Log Archive account and an Audit account. However, only the Audit account was created, and I got an error saying "Account limit reached" for the Log Archive account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Control Tower failed to set up your landing zone completely: AWS Control Tower cannot create an account because you have reached the limit on the number of accounts in your organization.
&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.amazonaws.com%2Fuploads%2Farticles%2Frrr8tpui9mk5r1tkm68i.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%2Frrr8tpui9mk5r1tkm68i.png" alt="Screenshot 2025-08-21 16.51.09.png" width="800" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I checked Service Quotas, the &lt;code&gt;Default maximum number of accounts&lt;/code&gt; was set to 10, but my actual account count was clearly below that (only 3 including the management account).&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%2Fywsew6xvxm4pkq4l6i3h.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%2Fywsew6xvxm4pkq4l6i3h.png" alt="Screenshot 2025-08-22 15.27.02.png" width="800" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I couldn't figure out the root cause (which is frustrating), but I resolved it by increasing the Service Quota to 15. Perhaps Control Tower internally reserves some account slots...?&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%2Fp3lishho6erbv7iessyf.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%2Fp3lishho6erbv7iessyf.png" alt="Screenshot 2025-09-02 8.51.08.png" width="800" height="543"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CloudFormation Deployment Failure (KMS Key Policy Related)
&lt;/h2&gt;

&lt;p&gt;Just when I thought I'd resolved the account limit error, I ran into a CloudFormation deployment error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Control Tower failed to set up your landing zone completely: AWS Control Tower failed to deploy stack(s): arn:aws:cloudformation:ap-northeast-1:XXXXXXXX:stack/AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER/4787d990-8144-11f0-ad99-0625194a54fd. To continue, review the failed stack(s) and try again.
&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.amazonaws.com%2Fuploads%2Farticles%2Fu7btdrlg583lc7dnuco0.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%2Fu7btdrlg583lc7dnuco0.png" alt="Screenshot 2025-09-02 8.56.29.png" width="800" height="70"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I opened the CloudFormation console, I saw 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%2F8uo83ndatufn3jk3mde1.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%2F8uo83ndatufn3jk3mde1.png" alt="Screenshot 2025-09-02 9.00.28.png" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had configured CloudTrail and Config to use KMS for encryption, but apparently these services lacked the necessary permissions to access the KMS key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Required Permissions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Config: &lt;code&gt;kms:Decrypt&lt;/code&gt;, &lt;code&gt;kms:GenerateDataKey&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CloudTrail: &lt;code&gt;kms:GenerateDataKey*&lt;/code&gt;, &lt;code&gt;kms:Decrypt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I added the following to my KMS policy:&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;"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;"AllowConfigToUseKms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"config.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:GenerateDataKey"&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:kms:ap-northeast-1:xxxxxxxxxxxx:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&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;"AllowCloudTrailToUseKms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudtrail.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kms:GenerateDataKey*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&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:kms:ap-northeast-1:xxxxxxxxxxxx:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"&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:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:cloudtrail:ap-northeast-1:xxxxxxxxxxxx:trail/aws-controltower-BaselineCloudTrail"&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;"StringLike"&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;"kms:EncryptionContext:aws:cloudtrail:arn"&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:cloudtrail:*:xxxxxxxxxxxx:trail/*"&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;After that, I deleted the failed CloudFormation stack &lt;code&gt;AWSControlTowerBP-BASELINE-CLOUDTRAIL-MASTER&lt;/code&gt; and retried the Control Tower setup. All errors were resolved!&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%2Fzaxn40269fw2p8clozg6.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%2Fzaxn40269fw2p8clozg6.png" alt="Screenshot 2025-08-25 15.01.27.png" width="800" height="746"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I'm still puzzled about the root cause of the account limit error. If anyone knows what might have caused it, please let me know!&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://dev.classmethod.jp/articles/aws-control-tower-setup-failed-kms/" rel="noopener noreferrer"&gt;https://dev.classmethod.jp/articles/aws-control-tower-setup-failed-kms/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>tutorial</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How I Reduced ELB Access Log Analysis Time by 80% Using AWS Data Processing MCP Server</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Sat, 09 Aug 2025 06:08:12 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-i-reduced-elb-access-log-analysis-time-by-80-using-aws-data-processing-mcp-server-43co</link>
      <guid>https://dev.to/aws-builders/how-i-reduced-elb-access-log-analysis-time-by-80-using-aws-data-processing-mcp-server-43co</guid>
      <description>&lt;p&gt;The other day, we had a sudden spike in requests to one of our services, which triggered a flood of ELB latency alerts.&lt;/p&gt;

&lt;p&gt;Fortunately, I had already built &lt;a href="https://dev.to/suzuki0430/analyzing-elb-access-logs-with-athena-configuration-and-query-examples-id6"&gt;a foundation for analyzing ELB access logs via Athena&lt;/a&gt;.&lt;br&gt;
The problem? In an incident scenario, running SQL-based analysis is slow. Writing a query → fixing it → waiting for aggregation → fixing again… it all takes time.&lt;/p&gt;

&lt;p&gt;I eventually tracked down a suspicious IP hammering us with requests and blocked it, but I was left thinking "That could have gone faster."&lt;/p&gt;

&lt;p&gt;While browsing AWS’s official MCP servers again, I noticed one I’d previously ignored the &lt;a href="https://awslabs.github.io/mcp/servers/aws-dataprocessing-mcp-server/" rel="noopener noreferrer"&gt;AWS Data Processing MCP Server&lt;/a&gt;.&lt;br&gt;
The name made me think it wasn’t relevant, so I had skipped it before—but it turns out it lets you query Athena using natural language.&lt;/p&gt;

&lt;p&gt;This post is a quick walkthrough of how I set it up, the snags I hit along the way, and how it worked in practice.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;I connected it from Claude Desktop by adding the following to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;work-mcp&lt;/code&gt; is an &lt;code&gt;AWS_PROFILE&lt;/code&gt; I created just for this purpose.&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;"mcpServers"&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.dp-mcp"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uvx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"awslabs.aws-dataprocessing-mcp-server@latest"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"FASTMCP_LOG_LEVEL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERROR"&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_PROFILE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"work-mcp"&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_REGION"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ap-northeast-1"&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;"autoApprove"&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;"disabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;
  
  
  IAM Policy (Where I Got Stuck)
&lt;/h3&gt;

&lt;p&gt;The permissions listed in the official docs weren’t enough for my environment—I ran into missing permissions like &lt;code&gt;athena:StartQueryExecution&lt;/code&gt;.&lt;br&gt;
Here’s the policy I ended up using. You’ll need to replace the S3 bucket names with your own.&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;"AthenaQueryAccess"&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;"athena:StartQueryExecution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"athena:GetQueryExecution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
                &lt;/span&gt;&lt;span class="s2"&gt;"athena:GetQueryResults"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"athena:StopQueryExecution"&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="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;"AthenaMetadataAccess"&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;"athena:GetDatabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"athena:GetTableMetadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"athena:ListDatabases"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"athena:ListTableMetadata"&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="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;"GlueDataCatalogAccess"&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;"glue:GetDatabase"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetDatabases"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"glue:GetTables"&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="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;"S3LogsAccess"&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:GetObject"&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:ListBucket"&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="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::your-alb-logs-bucket"&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:::your-alb-logs-bucket/*"&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="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;"AthenaResultsS3Access"&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:GetBucketLocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
                &lt;/span&gt;&lt;span class="s2"&gt;"s3:ListBucket"&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:DeleteObject"&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="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::your-athena-results-bucket"&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:::your-athena-results-bucket/*"&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;h2&gt;
  
  
  Example Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Question 1
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I want to analyze `alb_access_logs` in Athena.
Between 2025-08-07 00:00:00 and 23:59:59 JST, are there any bot-like requests?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Answer 1 (Sensitive data masked)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Bot Analysis of ALB Access Logs (Aug 7, 2025 JST / Some details masked)

Analyzed ALB access logs in Athena to extract confirmed and suspected bot traffic.
* External IPs are anonymized as `[IP-X]`
* Internal API paths are parameterized
* Counts are shown in approximate ranges

## 🤖 Bots Detected via User-Agent

1. **VirusTotalBot** — ~160 requests
   - Representative IPs: `[IP-V1]`, `[IP-V2]`, `[IP-V3]`

2. **Googlebot** — a few requests
   - Representative IP: `[IP-G1]`

3. **Google-Safety Bot** — a few dozen requests
   - Representative IPs: `[IP-GS1]`, `[IP-GS2]`

4. **Plesk screenshot bot** — ~50 requests
   - Representative IP: `[IP-P1]`

5. **Others**:
   - **AmazonProductDiscovery bot** (few)
   - **Brainfeedbot** (few)
   - **meta-externalads** (few, Facebook-related)
   - **MicroAdBot** (single instance)
   - **curl** (few)

6. **User-Agent not set (“-”)** — ~1.1k requests (most from `[IP-U1]`)

## 🚨 High-Volume Suspicious Activity (Top Examples)

1. **[IP-A]** — ~280k requests (most for the day)
   - Main UA: Generic desktop Chrome
   - Unique URLs: ~2.0k
   - Pattern: Very frequent polling for a single session (peak ~3k–5k/hour)

2. **[IP-B]** — ~180k requests
   - 2 distinct UAs
   - Unique URLs: ~18k

3. **[IP-C]** — ~120k requests
   - 3 distinct UAs
   - Unique URLs: ~10k

4. **[IP-D]** — ~56k requests
   - Concentrated over ~3 hours (peak ~4k/hour)

## 🔍 Notable Patterns (Excerpt)

**[IP-A]** strongly suspected to be a bot or misconfigured client due to:
- Continuous high-frequency polling of similar URLs
- Targeting endpoints like `/v1/log/{sessionId}` for session monitoring

## 📊 Daily Totals

- **Total requests**: ~15.2M
- **Clear bots**: ~1.4k
- **High-volume sources (≥1k/day)**: ~20 IPs totaling ~2.0M requests

**Conclusion:**  
`[IP-A]` is likely a bot or a misconfigured client, given the excessively short polling interval.  
Recommend rate-limiting, access control, or switching to a push/caching approach.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Question 2
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If we enable AWS WAF Bot Control, will the block count be high?
And would `[IP-A]` or `[IP-B]` be caught?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Answer 2
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# AWS WAF Bot Control Assessment (Details masked)

## 🛡️ Bots Likely to be Blocked (Clearly identifiable by UA)

1. **VirusTotalBot**: ~160 requests
2. **Googlebot**: Few (usually whitelisted)
3. **Google-Safety Bot**: Few dozen (usually whitelisted)
4. **MicroAdBot**: ~10
5. **Brainfeedbot**: ~10
6. **Plesk screenshot bot**: Few–dozen
7. **curl**: Few

**Total:** ~230–290 requests/day

## User-Agent Not Set (“-”): ~0.5k–1.1k/day
- Some would be detected by Bot Control
- Could include legitimate proxy traffic

### 📊 Block Volume Prediction
- Clear bots: ~300–400 requests/day
- Share of total: ~0.002–0.003% (on 15–18M daily requests)

## 🎯 Would `[IP-A]` and `[IP-B]` Be Blocked?

### `[IP-A]` (Global IP)
- Likely bypassed
- UA: Generic desktop Chrome (Bot Control focuses on UA-based detection)
- Could be caught only via behavioral analysis (e.g., consistent ~25k/day mechanical access)

### `[IP-B]` (Corporate network)
- Almost certainly bypassed
- UA: Common browser
- Many legitimate authenticated API requests
- Access patterns suggest possible misconfiguration rather than malicious intent

## 🔧 Suggested WAF Enhancements

1. **Rate Limiting Rules**
   - Per IP: ~100 req/min, ~1,000 req/hour

2. **Endpoint-Specific Limits**
   - `/v1/companies/*`: ~10 req/min per IP
   - `/v1/log/*`: ~5 req/min per IP

3. **Behavioral Rules**
   - Burst access to same URL
   - Unusual high-frequency overnight traffic
   - Evenly timed polling

4. **Whitelist Exceptions**
   - Fixed IP ranges for trusted companies
   - Certain UAs for internal monitoring tools

### Effectiveness Forecast
- Bot Control alone: ~300–500 blocked requests/day
- Problematic `[IP-A]/[IP-B]` traffic remains
- Combining with rate-limiting significantly improves impact with minimal false positives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How This Influenced WAF Design
&lt;/h2&gt;

&lt;p&gt;Manually writing SQL for “How many bot-like requests?” is tedious.&lt;br&gt;
This tool delivered a detailed breakdown instantly—helping me confirm that Bot Control wouldn’t address our main offenders.&lt;br&gt;
That was valuable not only for effectiveness but also for cost analysis, since Bot Control is billed separately from other WAF rules.&lt;/p&gt;

&lt;p&gt;The MCP server doesn’t make SQL obsolete, but it dramatically shortens the time from alert to first hypothesis.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Run Multi-Agent AI in the Cloud Without a Local GPU Using Docker Offload and Compose</title>
      <dc:creator>Atsushi Suzuki</dc:creator>
      <pubDate>Mon, 21 Jul 2025 04:45:38 +0000</pubDate>
      <link>https://dev.to/suzuki0430/run-multi-agent-ai-in-the-cloud-without-a-local-gpu-using-docker-offload-and-compose-57db</link>
      <guid>https://dev.to/suzuki0430/run-multi-agent-ai-in-the-cloud-without-a-local-gpu-using-docker-offload-and-compose-57db</guid>
      <description>&lt;p&gt;With the release of &lt;a href="https://docs.docker.com/desktop/release-notes/" rel="noopener noreferrer"&gt;Docker Desktop 4.43 (July 3, 2025)&lt;/a&gt;, you can now declare AI models, agents, and MCP tools in a single &lt;code&gt;compose.yaml&lt;/code&gt; file and launch them all with a single &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On top of that, the beta release of Docker Offload allows you to run Compose projects directly in the cloud with NVIDIA L4 GPUs. This opens the door to running large-scale models from even a modest laptop.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk through how to use &lt;a href="https://github.com/docker/compose-for-agents" rel="noopener noreferrer"&gt;docker/compose-for-agents&lt;/a&gt;' official A2A Multi‑Agent Fact Checker sample entirely with Compose. I'll also demonstrate how to offload the workload to the cloud using Docker Offload.&lt;/p&gt;

&lt;p&gt;Some images in this post are sourced from the official Docker Offload Content Kit provided to Docker Captains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample Overview
&lt;/h2&gt;

&lt;p&gt;The A2A Multi‑Agent Fact Checker is a multi-agent system built with Google’s ADK (Agent Development Kit) and the A2A protocol. It features three agents—Auditor, Critic, and Reviser—that work together to research, verify, and revise a given claim, then return a final conclusion.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auditor: Breaks down the user’s claim into subtasks and delegates them to Critic and Reviser. Collects the final answer and returns it via the UI.&lt;/li&gt;
&lt;li&gt;Critic: Performs external web searches using the DuckDuckGo MCP tool to gather supporting evidence.&lt;/li&gt;
&lt;li&gt;Reviser: Refines and verifies the output using the evidence gathered by Critic and the initial draft from Auditor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Critic communicates with the outside world via the MCP Gateway, and the inference model (Gemma 3 4B‑Q4) is hosted via Docker Model Runner.&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%2F2y8b4nrr7s7l37h9m4u4.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%2F2y8b4nrr7s7l37h9m4u4.png" alt="screenshot" width="800" height="917"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Highlights of &lt;code&gt;compose.yaml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here’s the full &lt;code&gt;compose.yaml&lt;/code&gt; used to define the multi-agent system:&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Auditor Agent coordinates the entire fact-checking workflow&lt;/span&gt;
  &lt;span class="na"&gt;auditor-agent-a2a&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auditor-agent&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:8080"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CRITIC_AGENT_URL=http://critic-agent-a2a:8001&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;REVISER_AGENT_URL=http://reviser-agent-a2a:8001&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;critic-agent-a2a&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;reviser-agent-a2a&lt;/span&gt;
    &lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;endpoint_var&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MODEL_RUNNER_URL&lt;/span&gt;
         &lt;span class="na"&gt;model_var&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MODEL_RUNNER_MODEL&lt;/span&gt;

  &lt;span class="na"&gt;critic-agent-a2a&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;critic-agent&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MCPGATEWAY_ENDPOINT=http://mcp-gateway:8811/sse&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mcp-gateway&lt;/span&gt;
    &lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;gemma3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="c1"&gt;# specify which environment variables to inject into the container&lt;/span&gt;
         &lt;span class="na"&gt;endpoint_var&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MODEL_RUNNER_URL&lt;/span&gt;
         &lt;span class="na"&gt;model_var&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MODEL_RUNNER_MODEL&lt;/span&gt;

  &lt;span class="na"&gt;reviser-agent-a2a&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;reviser-agent&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MCPGATEWAY_ENDPOINT=http://mcp-gateway:8811/sse&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mcp-gateway&lt;/span&gt;
    &lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;gemma3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;endpoint_var&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MODEL_RUNNER_URL&lt;/span&gt;
         &lt;span class="na"&gt;model_var&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MODEL_RUNNER_MODEL&lt;/span&gt;

  &lt;span class="na"&gt;mcp-gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# mcp-gateway secures your MCP servers&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/mcp-gateway:latest&lt;/span&gt;
    &lt;span class="na"&gt;use_api_socket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--transport=sse&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--servers=duckduckgo&lt;/span&gt;
      &lt;span class="c1"&gt;# add an MCP interceptor to log the responses&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--interceptor&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;after:exec:echo RESPONSE=$(cat) &amp;gt;&amp;amp;2&lt;/span&gt;

&lt;span class="na"&gt;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# declare LLM models to pull and use&lt;/span&gt;
  &lt;span class="na"&gt;gemma3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ai/gemma3:4B-Q4_0&lt;/span&gt;
    &lt;span class="na"&gt;context_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt; &lt;span class="c1"&gt;# 3.5 GB VRAM&lt;/span&gt;
    &lt;span class="c1"&gt;#context_size: 131000 # 7.6 GB VRAM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Top-Level &lt;code&gt;models&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;As of Compose v2.38, you can declare LLM images as OCI Artifacts under the top-level &lt;code&gt;models&lt;/code&gt; field. &lt;a href="https://docs.docker.com/ai/model-runner/" rel="noopener noreferrer"&gt;Docker Model Runner&lt;/a&gt; will automatically pull the image and expose it as an API endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-Service &lt;code&gt;models&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Each service defines which model to use and how to inject its URL and model name via environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MODEL_RUNNER_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://model-runner:12434
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MODEL_RUNNER_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gemma3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means your app can simply read environment variables without hardcoding the model path.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://docs.docker.com/ai/mcp-gateway/" rel="noopener noreferrer"&gt;MCP Gateway&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;docker/mcp-gateway&lt;/code&gt; image acts as a secure relay for MCP servers like DuckDuckGo. It communicates with the Critic agent using Server-Sent Events (SSE). The &lt;code&gt;--interceptor&lt;/code&gt; flag logs the raw responses directly to stderr.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency Management
&lt;/h3&gt;

&lt;p&gt;Like traditional Compose setups, &lt;code&gt;depends_on&lt;/code&gt; is used to manage startup order: MCP Gateway → Critic/Reviser → Auditor. This eliminates the need for retry logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Locally
&lt;/h2&gt;

&lt;p&gt;You can launch the stack locally with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[+] Running 8/9
 ✔ reviser-agent-a2a                  Built       0.0s
 ✔ critic-agent-a2a                   Built       0.0s
 ✔ auditor-agent-a2a                  Built       0.0s
 ⠴ gemma3 Configuring                            76.5s
 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Gemma 3 4B‑Q4 is quantized, it even runs on my MacBook Air M2.&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%2Fl4ijldzlfcbtjrdzymy4.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%2Fl4ijldzlfcbtjrdzymy4.png" alt="screenshot" width="800" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open your browser to &lt;code&gt;http://localhost:8080&lt;/code&gt;, type in a claim like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How far is the Moon from the Earth?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Critic performs a DuckDuckGo search, the Reviser polishes the output, and the Auditor returns the final answer.&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%2Fxprbyo375ihwa53gjqku.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%2Fxprbyo375ihwa53gjqku.png" alt="screenshot" width="800" height="292"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Docker Offload
&lt;/h2&gt;

&lt;p&gt;If you want to use a larger model like Gemma 27B Q4, local GPUs might not cut it. That’s where &lt;a href="https://www.docker.com/products/docker-offload/" rel="noopener noreferrer"&gt;Docker Offload&lt;/a&gt; comes in—just enable the feature and override your model config to run in the cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling Docker Offload
&lt;/h3&gt;

&lt;p&gt;First, sign up for beta access on &lt;a href="https://www.docker.com/products/docker-offload/" rel="noopener noreferrer"&gt;Docker's official site&lt;/a&gt;. (As a Docker Captain, I received early access.)&lt;/p&gt;

&lt;p&gt;Then, go to Settings &amp;gt; Beta Features and enable both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Enable Docker Offload"&lt;/li&gt;
&lt;li&gt;"Enable Docker Offload GPU Support"&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%2Fevkna2eq0f2yr8r53e3d.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%2Fevkna2eq0f2yr8r53e3d.png" alt="screenshot" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Switch the Docker Desktop toggle to the cloud icon to activate Offload (or run &lt;code&gt;docker offload start&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.amazonaws.com%2Fuploads%2Farticles%2Fl73pj8mpk1gp18q2as8r.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%2Fl73pj8mpk1gp18q2as8r.png" alt="screenshot" width="744" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;compose.offload.yaml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Prepare a separate file to override the model definition:&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;models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gemma3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ai/gemma3-qat:27B-Q4_K_M&lt;/span&gt; 
    &lt;span class="na"&gt;context_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt; &lt;span class="c1"&gt;# 18.6 GB VRAM&lt;/span&gt;
    &lt;span class="c1"&gt;# context_size: 80000 # 28.37 GB VRAM&lt;/span&gt;
    &lt;span class="c1"&gt;# context_size: 131000 # 35.5 GB VRAM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running with Offload
&lt;/h3&gt;

&lt;p&gt;To launch in the cloud, combine the two Compose files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; compose.yaml &lt;span class="nt"&gt;-f&lt;/span&gt; compose.offload.yaml up &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This overrides the top-level &lt;code&gt;models&lt;/code&gt; field with the Offload-specific config.&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%2Fm4lev95v9c20d7eaep4c.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%2Fm4lev95v9c20d7eaep4c.png" alt="screenshot" width="800" height="271"&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%2Fu6tx1tyvsxmveck69enn.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%2Fu6tx1tyvsxmveck69enn.png" alt="screenshot" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docker Offload gives you 300 GPU credits for free, and any additional usage is billed at \$0.015 per GPU second. Don’t forget to stop the service afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker offload stop
&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.amazonaws.com%2Fuploads%2Farticles%2Fih4zm6jkwi7fmvp0dhg1.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%2Fih4zm6jkwi7fmvp0dhg1.png" alt="screenshot" width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Trying out Compose and Offload together really shows the power of unified agent, model, and tool orchestration. It’s incredibly convenient to use the same &lt;code&gt;docker compose up&lt;/code&gt; command for both local and cloud environments.&lt;/p&gt;

&lt;p&gt;The agent space is evolving rapidly, so if you have a better workflow or tips, I’d love to hear about them.&lt;/p&gt;

&lt;p&gt;If you’re curious about Docker Offload, start experimenting within the free credit limit—you might be surprised how far you can go.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.docker.com/blog/build-ai-agents-with-docker-compose/" rel="noopener noreferrer"&gt;Docker Brings Compose to the AI Agent Era&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/offload/usage/" rel="noopener noreferrer"&gt;Docker Offload usage and billing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>ai</category>
      <category>webdev</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
