<?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: Sergio D. Rodríguez Inclán</title>
    <description>The latest articles on DEV Community by Sergio D. Rodríguez Inclán (@w4ls3n).</description>
    <link>https://dev.to/w4ls3n</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3190411%2Fbfde5f1a-49c7-406f-9518-d29a47bbe933.jpeg</url>
      <title>DEV Community: Sergio D. Rodríguez Inclán</title>
      <link>https://dev.to/w4ls3n</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/w4ls3n"/>
    <language>en</language>
    <item>
      <title>Devbox: Portable and Isolated Development Environments</title>
      <dc:creator>Sergio D. Rodríguez Inclán</dc:creator>
      <pubDate>Sun, 06 Sep 2026 21:00:03 +0000</pubDate>
      <link>https://dev.to/w4ls3n/devbox-portable-and-isolated-development-environments-ael</link>
      <guid>https://dev.to/w4ls3n/devbox-portable-and-isolated-development-environments-ael</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I am one of those people who need to have everything in its place to be able to do something. This applies, of course, when I work with a local or remote repository. I need to have all the dependencies installed and configured; moreover, the environment must be prepared with all the tools and with signaling showing the current status of the environment.&lt;/p&gt;

&lt;p&gt;By signaling, I mean the console "prompt," which should be as friendly and aesthetic as possible. For example:&lt;/p&gt;

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

&lt;p&gt;The terminal is iTerm, running zsh by default, with the &lt;a href="https://github.com/sorin-ionescu/prezto" rel="noopener noreferrer"&gt;Presto&lt;/a&gt; extension, which allows me to configure plugins that enhance command searches in history, improve interaction with Git, and much more. Anyone interested can check its features by following the link.&lt;/p&gt;

&lt;p&gt;In a development directory, the visual aspect should display the current path, the branch I’m on, and if I’m within a special environment like a virtualenv, etc. For example, the directory where I develop this blog:&lt;/p&gt;

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

&lt;p&gt;The knowledgeable reader will notice that I am using a Nix environment and a terminal multiplexer (Zellij).&lt;/p&gt;

&lt;h2&gt;
  
  
  Nix
&lt;/h2&gt;

&lt;p&gt;Nix is an ecosystem of several open-source projects supported by its community.&lt;/p&gt;

&lt;p&gt;The most relevant ones in my opinion are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NixOS: A complete GNU/Linux distribution.&lt;/li&gt;
&lt;li&gt;nix-shell: An Ad Hoc shell that allows installing existing packages from the Nix repositories. These packages are downloaded to a Nix repo located at a local path.&lt;/li&gt;
&lt;li&gt;Nix Language: A shell programming language of its own that allows manipulating Nix environments at will.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;nix-shell&lt;/code&gt; can be installed on MacOS, WSL (Windows), and GNU/Linux.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nix-shell&lt;/code&gt; works by creating an isolated area in the user’s environment where a local repository manages the Nix packages installed by the user.&lt;/p&gt;

&lt;p&gt;The Nix shell is great, but it has a couple of drawbacks. The repository it creates is global for the user; all installed packages are stored in one place. Therefore, cleaning up dependencies can be somewhat complicated. The second problem is that to manage it efficiently, one has to learn the language it uses, which is quite complex. Not everyone will want to invest time in learning a language just to create their development environments.&lt;/p&gt;

&lt;p&gt;There are several alternatives based on Nix, but the one I find best, at least for the usage I give it, is Devbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Devbox
&lt;/h2&gt;

&lt;p&gt;Devbox is similar to Nix in every way (it uses it behind the scenes), except that it generates repositories by project and its definition is much simpler, using a structured JSON file called &lt;code&gt;devbox.json&lt;/code&gt;, which can be shared in the repository and makes the environment reproducible and automatable.&lt;/p&gt;

&lt;p&gt;We will create a very simple example that can be found in this repository &lt;a href="https://github.com/Walsen/devbox-basics" rel="noopener noreferrer"&gt;https://github.com/Walsen/devbox-basics&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We must assume that we are on a Unix-compatible system with a shell similar to bash in the terminal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 0: Install Devbox
&lt;/h3&gt;

&lt;p&gt;It’s as simple as running a shell command (Linux &amp;amp; MacOS):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.jetify.com/devbox | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this, Devbox will be available for use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create the directory
&lt;/h3&gt;

&lt;p&gt;We create a simple directory with the mkdir command and enter it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;devbox-basics
&lt;span class="nb"&gt;cd &lt;/span&gt;devbox-basics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Generate the 'devbox.json' file and review its content
&lt;/h3&gt;

&lt;p&gt;Devbox can automatically generate its configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devbox init
&lt;span class="nb"&gt;cat &lt;/span&gt;devbox.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;We can identify three main sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$schema&lt;/strong&gt;: The document definition according to the version specified in the URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;packages&lt;/strong&gt;: An array with the list of packages installed for this repository and their dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shell&lt;/strong&gt;: An object that contains other objects that help us automate the environment, either when starting it or on demand.

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;init_hook&lt;/strong&gt;: An array with a list of commands that will be executed when starting the environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;scripts&lt;/strong&gt;: An object that contains key/value pairs where the value is an array that may contain shell commands that will be executed when the key is invoked using the command &lt;code&gt;devbox run [key]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are additional sections: &lt;code&gt;env&lt;/code&gt;, &lt;code&gt;env_from&lt;/code&gt;, and &lt;code&gt;include&lt;/code&gt;. The first two are used to initialize environment variables, either explicitly specified in the &lt;code&gt;devbox.json&lt;/code&gt; or read from a &lt;code&gt;.env&lt;/code&gt; file. The last one is used to include additional Devbox plugins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Adding Packages / Dependencies
&lt;/h3&gt;

&lt;p&gt;Generally, it's easy to locate the packages we want to install just by their common name; for example, we will add a list of packages that I commonly use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devbox add git direnv awscli2 python@3.12 terraform just zellij pipenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the reader may notice, it’s possible to specify versions using the at sign after the package name, followed by the version. To see what versions are available or to search for the correct package, this page is available: &lt;a href="https://search.nixos.org/packages" rel="noopener noreferrer"&gt;https://search.nixos.org/packages&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Nix does not provide binaries for some packages, so it must compile the code; that's what it did with Terraform during the installation.&lt;/p&gt;

&lt;p&gt;Finally, Devbox also provides ready-to-use (out-of-the-box) configurations for languages like Python, for which it offers a functional virtual environment. In my case, I chose Pipenv, so it will take a couple more steps to get it ready.&lt;/p&gt;

&lt;p&gt;So far, our &lt;code&gt;devbox.json&lt;/code&gt; looks like this:&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&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;"git@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"direnv@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"awscli2@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"python@3.12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"terraform@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"just@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"zellij@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"pipenv@latest"&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;"shell"&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;"init_hook"&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;"echo 'Welcome to devbox!' &amp;gt; /dev/null"&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;"scripts"&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;"test"&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;"echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Error: no test specified&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; exit 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="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;
  
  
  Step 4: Start the Shell
&lt;/h3&gt;

&lt;p&gt;To start the Devbox shell, we simply run the following command:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The resulting screen should be similar to this:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0d2tv34c5fz27rwibv0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl0d2tv34c5fz27rwibv0.png" alt="Devbox shell" width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can notice that the "Prompt" has changed, indicating that we are inside the Devbox shell.&lt;/p&gt;

&lt;p&gt;Let's check the dependencies and test the virtualenv:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyyavqjs2cz687e2vvsu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyyavqjs2cz687e2vvsu.png" alt="Devbox pipenv install" width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From here, we can develop in Terraform and/or Python without any issues.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Configure the Environment Using direnv
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;direnv&lt;/code&gt; is a small utility that automates the use of environment variables; we simply declare the variables in a &lt;code&gt;.envrc&lt;/code&gt; file, and &lt;code&gt;direnv&lt;/code&gt; will automatically load them when we enter the folder where the &lt;code&gt;.envrc&lt;/code&gt; resides.&lt;/p&gt;

&lt;p&gt;Devbox comes with support for &lt;code&gt;direnv&lt;/code&gt;. To generate a configuration file, we run the following command from outside the shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devbox generate direnv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;When executing the command, Devbox automatically generates it and runs the shell with the configurations from the &lt;code&gt;.envrc&lt;/code&gt;. An interesting detail is that this way, the "prompt" from my console has been imported into the Devbox shell, which is very convenient.&lt;/p&gt;

&lt;p&gt;Now that &lt;code&gt;direnv&lt;/code&gt; is loaded, we can start exporting variables such as our AWS user's access keys.&lt;/p&gt;

&lt;p&gt;We edit the &lt;code&gt;.envrc&lt;/code&gt; file and at the end, we add the variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Automatically sets up your devbox environment whenever you cd into this&lt;/span&gt;
&lt;span class="c"&gt;# directory via our direnv integration:&lt;/span&gt;

&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;devbox generate direnv &lt;span class="nt"&gt;--print-envrc&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/&lt;/span&gt;
&lt;span class="c"&gt;# for more details&lt;/span&gt;

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ASIA4RRMHHCFXBM0DKRM"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4pqlKkaQNBlueLLHkNiWpgDNzihdNfgTYjy3tYk0w"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We run the command at the same directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;direnv allow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time a change is made to the &lt;code&gt;.envrc&lt;/code&gt;, it’s necessary to authorize the change, which will "reload" the environment:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67xct87g0g06cgpz7gs1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67xct87g0g06cgpz7gs1.png" alt="Devbox export variables" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, we test the access keys by listing our buckets in S3.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚨 &lt;strong&gt;WARNING!!&lt;/strong&gt; 🚨&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NEVER&lt;/strong&gt; send credentials or any sensitive text to the repository. Therefore, a good practice would be to leave the value of the access keys blank, submit the &lt;code&gt;.envrc&lt;/code&gt; to the repo, add it to &lt;code&gt;.gitignore&lt;/code&gt; afterward, and then it wouldn’t be taken into account for future submissions. Other team members could use their personal credentials without fear of submitting them later.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 6: Implement Automation
&lt;/h3&gt;

&lt;p&gt;To test the automation features of Devbox, we will add a simple Python script that shows us the buckets in S3, replicating the command &lt;code&gt;aws s3 ls&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The code would be as follows:&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="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_s3_buckets&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    List all S3 buckets in the AWS account
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Create an S3 client
&lt;/span&gt;        &lt;span class="n"&gt;s3_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Get list of buckets
&lt;/span&gt;        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_buckets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S3 Buckets:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-----------&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Print bucket names
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Buckets&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="nf"&gt;print&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;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;'&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="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&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="nf"&gt;print&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="n"&gt;e&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;list_s3_buckets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we run the code, we will get an error message:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wzr9rtknwkvisr3txch.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wzr9rtknwkvisr3txch.png" alt="Devbox virtualenv error" width="798" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is obviously because we haven’t activated the Pipenv virtualenv.&lt;/p&gt;

&lt;p&gt;To avoid the hassle of doing it each time we enter the environment, we will automate this task by modifying the &lt;code&gt;devbox.json&lt;/code&gt; as follows:&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&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;"git@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"direnv@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"awscli2@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"python@3.12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"terraform@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"just@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"zellij@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"pipenv@latest"&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;"env_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shell"&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;"init_hook"&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;"echo 'Welcome to devbox!'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"pipenv shell"&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;"scripts"&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;"test"&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;"echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Error: no test specified&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &amp;amp;&amp;amp; exit 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="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;
  
  
  Environment Variables in a Virtualenv Shell
&lt;/h4&gt;

&lt;p&gt;We have added a new attribute: &lt;code&gt;env_from&lt;/code&gt;, which reads a &lt;code&gt;.env&lt;/code&gt; file from the specified path and loads the environment variables. The reader may wonder why we are doing this since we already have &lt;code&gt;direnv&lt;/code&gt; exporting variables for our Devbox environment; it is important to note that when the Python virtualenv is activated, a new shell is created with its own environment variables. To export our access keys to the virtualenv, we have to do it using this feature.&lt;/p&gt;

&lt;p&gt;There is also the possibility of adding the variables directly in the &lt;code&gt;devbox.json&lt;/code&gt; using the env block, but we want to add our access keys directly. Therefore, a &lt;code&gt;.env&lt;/code&gt; file can be sent empty, allowing each user to add the variables for their environment.&lt;/p&gt;

&lt;p&gt;So, our &lt;code&gt;.env&lt;/code&gt; file contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ASIA4RRMHHCFXBM0DKRM"&lt;/span&gt;
&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"4pqlKkaQNBlueLLHkNiWpgDNzihdNfgTYjy3tYk0w"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Commands in the init_hook
&lt;/h4&gt;

&lt;p&gt;We have added the command &lt;code&gt;pipenv shell&lt;/code&gt; as the second value in the init_hook list so that when the Devbox shell starts, it also starts the Pipenv virtualenv. The result should be similar to 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fldspc8vhjz2ngl64w90u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fldspc8vhjz2ngl64w90u.png" alt="Devbox init hook" width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if we run our script, we see that it works perfectly.&lt;/p&gt;

&lt;p&gt;One thing I am still working on is updating the prompt, respecting the original shell’s or loading a new one. However, since the virtualenv’s activate script runs a bash shell, there is not much to do unless we patch this file, but unfortunately, that wouldn’t be portable.&lt;/p&gt;

&lt;p&gt;If we don’t mind executing a simple command like &lt;code&gt;pipenv shell&lt;/code&gt; manually, then the prompt will respect the current one, and we avoid this automation, especially since if we are working with Terraform in the same repository, we may not want to be in the virtualenv all the time. In any case, this is just an example of what we can do.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scripts
&lt;/h4&gt;

&lt;p&gt;Devbox allows us to run scripts using the environment &lt;strong&gt;without the need to activate the shell&lt;/strong&gt;; for example, we can run the script that lists buckets by modifying the &lt;code&gt;devbox.json&lt;/code&gt; as follows:&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&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;"git@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"direnv@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"awscli2@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"python@3.12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"terraform@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"just@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"zellij@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"pipenv@latest"&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;"env_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shell"&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;"init_hook"&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;"echo 'Welcome to devbox!'"&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;"scripts"&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;"getbuckets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pipenv run python getbuckets.py"&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;Now we simply need to invoke the key &lt;code&gt;getbuckets&lt;/code&gt; in the &lt;code&gt;devbox run ...&lt;/code&gt; command:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fklpnoezatba4vphfx7x2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fklpnoezatba4vphfx7x2.png" alt="Devbox shell run" width="799" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Devbox will temporarily raise the shell, load the variables, and execute &lt;code&gt;pipenv run&lt;/code&gt;, which in turn will execute python &lt;code&gt;getbuckets.py&lt;/code&gt; in its Python environment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We must emphasize that the &lt;code&gt;devbox run&lt;/code&gt; command is executed when the Devbox shell has not been activated; if we have direnv configured to automatically load the environment, then it will not work. To disable the automatic loading of the Devbox shell, we simply run:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;direnv disallow
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;And the shell will no longer load when entering the directory.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Integration with Tools and Advanced Automation
&lt;/h3&gt;

&lt;p&gt;Devbox provides us with the ability to chain commands when starting the environment or when executing scripts, but we can do more than just run simple commands. We can take advantage of lightweight task execution and workflow tools, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/casey/just" rel="noopener noreferrer"&gt;Just&lt;/a&gt; (which we have installed with Devbox)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/go-task/task" rel="noopener noreferrer"&gt;go-task&lt;/a&gt; command runner&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dagger.io" rel="noopener noreferrer"&gt;Dagger&lt;/a&gt; local CI that uses containers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To demonstrate this point, we will create a set of Terraform scripts to launch an EC2 instance.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;main.tf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&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="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"app_server"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_ami&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amazon_linux_2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_type&lt;/span&gt;

  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&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;vpc_security_group_ids&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_sg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;associate_public_ip_address&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;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_name&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Get latest Amazon Linux 2 AMI&lt;/span&gt;
&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_ami"&lt;/span&gt; &lt;span class="s2"&gt;"amazon_linux_2"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;most_recent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;owners&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"amazon"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

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

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

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

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

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Value of the Name tag for the EC2 instance"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AppServer"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"vpc_cidr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CIDR block for VPC"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/16"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"subnet_cidr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CIDR block for subnet"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.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;&lt;code&gt;network.tf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# VPC&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&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="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_hostnames&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;enable_dns_support&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;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Public Subnet&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&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;cidr_block&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subnet_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;map_public_ip_on_launch&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;availability_zone&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_region&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;a"&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Main"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Internet Gateway&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&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;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Main"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Route Table&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&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;route&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="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Main"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Route Table Association&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&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;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;security.tf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"instance_sg"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"instance_sg"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Security group for EC2 instance"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="c1"&gt;# SSH access&lt;/span&gt;
  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Outbound internet access&lt;/span&gt;
  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"instance_sg"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;outputs.tf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"instance_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ID of the EC2 instance"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"instance_public_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Public IP address of the EC2 instance"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_ip&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"instance_public_dns"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Public DNS name of the EC2 instance"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_dns&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have the Terraform script, we create a small build script called &lt;code&gt;justfile&lt;/code&gt; to execute the Terraform commands. Additionally, we will include a task to run our script to retrieve buckets in Python:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;justfile&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="c1"&gt;# Set the default recipe to list available recipes&lt;/span&gt;
&lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="s"&gt;just --list&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize Terraform&lt;/span&gt;
&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform init&lt;/span&gt;

&lt;span class="c1"&gt;# Show changes required by the current configuration&lt;/span&gt;
&lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform plan -out=tfplan&lt;/span&gt;

&lt;span class="c1"&gt;# Apply the changes required to reach the desired state&lt;/span&gt;
&lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform apply tfplan&lt;/span&gt;

&lt;span class="c1"&gt;# Destroy all remote objects managed by this Terraform configuration&lt;/span&gt;
&lt;span class="na"&gt;destroy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform plan -destroy -out=tfplan&lt;/span&gt;
    &lt;span class="s"&gt;@echo "Review the destruction plan carefully!"&lt;/span&gt;
    &lt;span class="s"&gt;@echo "To proceed with destruction, run&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;just apply"&lt;/span&gt;

&lt;span class="c1"&gt;# Clean up local Terraform files&lt;/span&gt;
&lt;span class="na"&gt;clean&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;rm -rf .terraform/&lt;/span&gt;
    &lt;span class="s"&gt;rm -f tfplan&lt;/span&gt;
    &lt;span class="s"&gt;rm -f .terraform.lock.hcl&lt;/span&gt;

&lt;span class="c1"&gt;# Format Terraform files&lt;/span&gt;
&lt;span class="na"&gt;fmt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform fmt -recursive&lt;/span&gt;

&lt;span class="c1"&gt;# Validate Terraform files&lt;/span&gt;
&lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform validate&lt;/span&gt;

&lt;span class="c1"&gt;# Show current workspace&lt;/span&gt;
&lt;span class="na"&gt;workspace-show&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform workspace show&lt;/span&gt;

&lt;span class="c1"&gt;# Create and switch to a new workspace&lt;/span&gt;
&lt;span class="na"&gt;workspace-new name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform workspace new {{name}}&lt;/span&gt;

&lt;span class="c1"&gt;# Switch to an existing workspace&lt;/span&gt;
&lt;span class="na"&gt;workspace-select name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform workspace select {{name}}&lt;/span&gt;

&lt;span class="c1"&gt;# List all workspaces&lt;/span&gt;
&lt;span class="na"&gt;workspace-list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="s"&gt;terraform workspace list&lt;/span&gt;

&lt;span class="c1"&gt;# Combined recipe to plan and apply in one command (use with caution)&lt;/span&gt;
&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan apply&lt;/span&gt;

&lt;span class="c1"&gt;# Combined recipe to initialize, plan and apply in one command (use with caution)&lt;/span&gt;
&lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;init plan apply&lt;/span&gt;

&lt;span class="c1"&gt;# Combined recipe to destroy and clean up&lt;/span&gt;
&lt;span class="na"&gt;teardown&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;destroy apply clean&lt;/span&gt;

&lt;span class="c1"&gt;# Execute Python Boto3 script to get a formated list of S3 buckets&lt;/span&gt;
&lt;span class="na"&gt;get-buckets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="s"&gt;pipenv run python getbuckets.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advantage of using a task runner is that we can chain processes, add logic, and integrate tools. As seen in the &lt;code&gt;justfile&lt;/code&gt;, we have added numerous capabilities for running sequences with Terraform as well as combining them with other tools, such as our Python script.&lt;/p&gt;

&lt;p&gt;We will modify the &lt;code&gt;devbox.json&lt;/code&gt; to use the tasks that interest us most:&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"packages"&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;"git@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"direnv@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"awscli2@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"python@3.12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"terraform@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"just@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"zellij@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"pipenv@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"starship@latest"&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;"env_from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".env"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shell"&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;"init_hook"&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;"echo 'Welcome to devbox!'"&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;"scripts"&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;"tasklist"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tfinit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just init"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tfplan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just plan"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tfapply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just apply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tfdestroy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just destroy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tfdeploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tfsetup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just setup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tfteardown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just teardown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tfclean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just clean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"getbuckets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"just get-buckets"&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;To start, let’s check the list of available tasks in Just:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvubz3vs8x1dys5uwff0b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvubz3vs8x1dys5uwff0b.png" alt="Just tasks" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let’s try to obtain the list of buckets:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wgdpdq32b2dx3ubq4z5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wgdpdq32b2dx3ubq4z5.png" alt="Pipenv get buckets" width="800" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

&lt;p&gt;All good, let’s execute a deploy (apply):&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8ohem1wtg80y5mn792w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8ohem1wtg80y5mn792w.png" alt="Terraform apply" width="800" height="1101"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The resources were successfully created in AWS:&lt;/p&gt;

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

&lt;p&gt;The instance is accessible:&lt;/p&gt;

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

&lt;p&gt;We have verified that the scripts and automation work; it could easily be run from a CI as well.&lt;/p&gt;

&lt;p&gt;Now, let’s destroy the resources and clean up the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devbox tfteardown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Checking the 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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmyzffj2mhjxspoo5533z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmyzffj2mhjxspoo5533z.png" alt="Terraform apply" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Dependencies can become a nightmare, especially for those working with different technologies at the same time. Devbox allows us to have a clean host, with only the applications that the user truly needs installed permanently.&lt;/p&gt;

&lt;p&gt;It also enables us to share an environment among an entire team, promotes best practices, and can be extended and automated programmatically.&lt;/p&gt;

&lt;p&gt;In my opinion, learning to use these tools should be a fundamental chapter in the education of every computer scientist.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>The Future of Work</title>
      <dc:creator>Sergio D. Rodríguez Inclán</dc:creator>
      <pubDate>Sun, 06 Sep 2026 21:00:02 +0000</pubDate>
      <link>https://dev.to/w4ls3n/the-future-of-work-5665</link>
      <guid>https://dev.to/w4ls3n/the-future-of-work-5665</guid>
      <description>&lt;h1&gt;
  
  
  The Future of Work
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Current Landscape
&lt;/h2&gt;

&lt;p&gt;As I write this article, technology companies find themselves in a transitional phase. With the advent of artificial intelligence, the way software is produced will change, and so will the type of professionals required to build it—if this transformation hasn't already begun.&lt;/p&gt;

&lt;p&gt;In the IT field, the most common professional roles include: architects and developers, quality assurance (QA) specialists, infrastructure engineers, database analysts, and all related variants of these specialties.&lt;/p&gt;

&lt;p&gt;Architects and developers design and implement all product-related code.&lt;/p&gt;

&lt;p&gt;QA specialists ensure that products meet quality requirements.&lt;/p&gt;

&lt;p&gt;Infrastructure engineers manage company infrastructure, including servers, databases, cloud automation, and much more.&lt;/p&gt;

&lt;p&gt;Database analysts handle databases, including schema design, data migration, and related tasks.&lt;/p&gt;

&lt;p&gt;Obviously, there are other roles of a more administrative nature, but we won't focus on them in this analysis.&lt;/p&gt;

&lt;p&gt;The fundamental issue is that AI is redefining how the industry creates its products, with software being the first and most significantly affected sector. Software engineers, who until recently were highly sought after for their ability to produce code and deploy it to production, performed their work in a predominantly artisanal manner. Although good practices and processes have always been followed, most existing software has been designed and programmed by human minds and hands.&lt;/p&gt;

&lt;p&gt;Today, Large Language Models (LLMs) have surpassed previous AI models (such as BERT) and have established themselves as the most powerful tool for the software industry.&lt;/p&gt;

&lt;p&gt;Additionally, generative AI techniques such as vibe coding, agents, and MCP (Model Context Protocol) servers, when combined, could potentially replace a considerable portion of the traditional development work that currently dominates the software industry.&lt;/p&gt;

&lt;p&gt;Consequently, technology companies, especially the largest ones, have begun replacing traditional development engineers with AI-specialized engineers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's the Difference Between Traditional Development and AI-Powered Development?
&lt;/h2&gt;

&lt;p&gt;Simply put, it will no longer be necessary for a developer to create an API, or for a DevOps specialist to deploy a cloud solution—an agent with those specific capabilities can do it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Do AI Agents Work?
&lt;/h3&gt;

&lt;p&gt;AI agents are systems that combine language models (LLMs) with tools and execution capabilities. Unlike a simple chatbot, an agent can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reason&lt;/strong&gt; about what actions to take&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt; tools and commands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observe&lt;/strong&gt; the results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate&lt;/strong&gt; until completing the task&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a small example flow of deployment using an AI agent.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBVIGFzIFVzZXIKICAgIHBhcnRpY2lwYW50IEEgYXMgQWdlbnQKICAgIHBhcnRpY2lwYW50IExMTSBhcyBMTE0gTW9kZWwKICAgIHBhcnRpY2lwYW50IFQgYXMgVG9vbHMKICAgIHBhcnRpY2lwYW50IEUgYXMgRW52aXJvbm1lbnQKCiAgICBVLT4%2BQTogUmVxdWVzdDogIkRlcGxveSB0aGUgYXBwbGljYXRpb24iCiAgICBBLT4%2BTExNOiBDb250ZXh0ICsgQXZhaWxhYmxlIHRvb2xzCgogICAgcmVjdCByZ2IoMjQwLCAyNDgsIDI1NSkKICAgICAgICBOb3RlIG92ZXIgQSxMTE06IFJlYXNvbmluZyBDeWNsZQogICAgICAgIExMTS0%2BPkxMTTogQW5hbHl6ZXMgdGhlIHRhc2sKICAgICAgICBMTE0tPj5MTE06IFBsYW5zIHN0ZXBzCiAgICAgICAgTExNLT4%2BQTogRGVjaXNpb246IFVzZSAiZ2l0IHN0YXR1cyIgdG9vbAogICAgZW5kCgogICAgQS0%2BPlQ6IEV4ZWN1dGU6IGdpdCBzdGF0dXMKICAgIFQtPj5FOiBDb21tYW5kIG9uIHN5c3RlbQogICAgRS0%2BPlQ6IFJlc3VsdDogImJyYW5jaCBtYWluLCAzIGNoYW5nZXMiCiAgICBULT4%2BQTogUmV0dXJucyByZXN1bHQKCiAgICByZWN0IHJnYigyNTUsIDI0OCwgMjQwKQogICAgICAgIE5vdGUgb3ZlciBBLExMTTogSXRlcmF0aW9uCiAgICAgICAgQS0%2BPkxMTTogUmVzdWx0ICsgVXBkYXRlZCBjb250ZXh0CiAgICAgICAgTExNLT4%2BTExNOiBFdmFsdWF0ZXMgbmV4dCBzdGVwCiAgICAgICAgTExNLT4%2BQTogRGVjaXNpb246IFVzZSAiZ2l0IHB1c2giCiAgICBlbmQKCiAgICBBLT4%2BVDogRXhlY3V0ZTogZ2l0IHB1c2ggb3JpZ2luIG1haW4KICAgIFQtPj5FOiBQdXNoIHRvIEdpdEh1YgogICAgRS0%2BPlQ6IFN1Y2Nlc3M6ICJQdXNoZWQgdG8gbWFpbiIKICAgIFQtPj5BOiBSZXR1cm5zIHJlc3VsdAoKICAgIHJlY3QgcmdiKDI0MCwgMjU1LCAyNDApCiAgICAgICAgTm90ZSBvdmVyIEEsTExNOiBWZXJpZmljYXRpb24KICAgICAgICBBLT4%2BTExNOiBUYXNrIGNvbXBsZXRlZD8KICAgICAgICBMTE0tPj5BOiBZZXMsIGRlcGxveW1lbnQgaW5pdGlhdGVkCiAgICBlbmQKCiAgICBBLT4%2BVTog4pyFICJBcHBsaWNhdGlvbiBkZXBsb3llZCBzdWNjZXNzZnVsbHki" 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%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBVIGFzIFVzZXIKICAgIHBhcnRpY2lwYW50IEEgYXMgQWdlbnQKICAgIHBhcnRpY2lwYW50IExMTSBhcyBMTE0gTW9kZWwKICAgIHBhcnRpY2lwYW50IFQgYXMgVG9vbHMKICAgIHBhcnRpY2lwYW50IEUgYXMgRW52aXJvbm1lbnQKCiAgICBVLT4%2BQTogUmVxdWVzdDogIkRlcGxveSB0aGUgYXBwbGljYXRpb24iCiAgICBBLT4%2BTExNOiBDb250ZXh0ICsgQXZhaWxhYmxlIHRvb2xzCgogICAgcmVjdCByZ2IoMjQwLCAyNDgsIDI1NSkKICAgICAgICBOb3RlIG92ZXIgQSxMTE06IFJlYXNvbmluZyBDeWNsZQogICAgICAgIExMTS0%2BPkxMTTogQW5hbHl6ZXMgdGhlIHRhc2sKICAgICAgICBMTE0tPj5MTE06IFBsYW5zIHN0ZXBzCiAgICAgICAgTExNLT4%2BQTogRGVjaXNpb246IFVzZSAiZ2l0IHN0YXR1cyIgdG9vbAogICAgZW5kCgogICAgQS0%2BPlQ6IEV4ZWN1dGU6IGdpdCBzdGF0dXMKICAgIFQtPj5FOiBDb21tYW5kIG9uIHN5c3RlbQogICAgRS0%2BPlQ6IFJlc3VsdDogImJyYW5jaCBtYWluLCAzIGNoYW5nZXMiCiAgICBULT4%2BQTogUmV0dXJucyByZXN1bHQKCiAgICByZWN0IHJnYigyNTUsIDI0OCwgMjQwKQogICAgICAgIE5vdGUgb3ZlciBBLExMTTogSXRlcmF0aW9uCiAgICAgICAgQS0%2BPkxMTTogUmVzdWx0ICsgVXBkYXRlZCBjb250ZXh0CiAgICAgICAgTExNLT4%2BTExNOiBFdmFsdWF0ZXMgbmV4dCBzdGVwCiAgICAgICAgTExNLT4%2BQTogRGVjaXNpb246IFVzZSAiZ2l0IHB1c2giCiAgICBlbmQKCiAgICBBLT4%2BVDogRXhlY3V0ZTogZ2l0IHB1c2ggb3JpZ2luIG1haW4KICAgIFQtPj5FOiBQdXNoIHRvIEdpdEh1YgogICAgRS0%2BPlQ6IFN1Y2Nlc3M6ICJQdXNoZWQgdG8gbWFpbiIKICAgIFQtPj5BOiBSZXR1cm5zIHJlc3VsdAoKICAgIHJlY3QgcmdiKDI0MCwgMjU1LCAyNDApCiAgICAgICAgTm90ZSBvdmVyIEEsTExNOiBWZXJpZmljYXRpb24KICAgICAgICBBLT4%2BTExNOiBUYXNrIGNvbXBsZXRlZD8KICAgICAgICBMTE0tPj5BOiBZZXMsIGRlcGxveW1lbnQgaW5pdGlhdGVkCiAgICBlbmQKCiAgICBBLT4%2BVTog4pyFICJBcHBsaWNhdGlvbiBkZXBsb3llZCBzdWNjZXNzZnVsbHki" alt="Mermaid Diagram" width="1324" height="1358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key System Components
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM (Language Model)&lt;/strong&gt;: The "brain" that reasons and makes decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent&lt;/strong&gt;: Orchestrator that coordinates between the LLM and tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt;: APIs, CLI commands, file access, databases, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment&lt;/strong&gt;: The actual system where actions are executed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User Input&lt;/strong&gt;: Defines the high-level task&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning&lt;/strong&gt;: The LLM analyzes which tools to use and in what order&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution&lt;/strong&gt;: The agent invokes the necessary tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observation&lt;/strong&gt;: Receives the results of each action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration&lt;/strong&gt;: The LLM decides the next step based on results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completion&lt;/strong&gt;: When the task is finished, reports to the user&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This cycle of &lt;strong&gt;Reason → Act → Observe&lt;/strong&gt; repeats until the agent completes the task or determines it cannot continue.&lt;/p&gt;

&lt;p&gt;The image below illustrates what I call the "tower of abstraction." As with any process, there's a natural evolution where components become progressively more specialized and their interactions become more concrete and optimized.&lt;/p&gt;

&lt;p&gt;Consider this example: the human brain doesn't directly "know" how to make the hand grip an object. It simply decides that a situation requires gripping and sends the signal to the specialized organ—in this case, the hand muscles. In the image, we can establish the following analogy: the LLM represents the brain, transport symbolizes the nerves, and each agent corresponds to a specific hand muscle.&lt;/p&gt;

&lt;p&gt;Similarly, in the software world, the process evolved from implementing a single monolithic module to executing specific tasks in microservices orchestrated by a controller. This controller decides which microservice should be invoked in response to an event or condition. Applying this analogy to the image, the LLM corresponds to a Control Plane, transport to networking and routes, and agents to Pods/containers.&lt;/p&gt;

&lt;p&gt;In this evolution, the AI agent determines which tools to use and in what sequence, while the LLM decides the next step based on the obtained results.&lt;/p&gt;

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

&lt;p&gt;The next step in traditional development turned out to be perhaps more unexpected than logical. This evolution in the software industry (and many others) translates into delegating development, implementation, testing, and deployment work to intelligent agents. These can perform such work continuously and with superior efficiency compared to humans, consuming fewer resources and thus leaving behind the artisanal aspect of the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  So What Will Happen to...?
&lt;/h2&gt;

&lt;p&gt;Yes, the crucial question: what fate awaits traditional engineers who, day after day, deliver task after task?&lt;/p&gt;

&lt;p&gt;Two clear alternatives present themselves: either we let events overwhelm us and the "tsunami" buries us under tons of water, or we reinvent and adapt ourselves (once again), learning to navigate the vast sea of changes ahead.&lt;/p&gt;

&lt;p&gt;From now on, the industry will no longer require developers who write code from start to finish, engineers who deploy complete solutions, or analysts who exhaustively review code or data structures. Instead, the industry will demand engineers capable of orchestrating agents offered by other providers, as well as creating their own agents for their business's specific use cases.&lt;/p&gt;

&lt;p&gt;Work previously performed by teams of dozens will now be carried out by just a few, who will nevertheless produce the same amount or even more than those "small armies."&lt;/p&gt;

&lt;p&gt;Since creating products will be much simpler, numerous startups will likely emerge that, despite having few people, will achieve significant impact and establish themselves in the market. Additionally, many new positions will be created for those who learn to interact with and improve agents.&lt;/p&gt;

&lt;p&gt;Education must undoubtedly transform as well, especially higher education, which still trains engineers with a "classical" profile who will enter a market that no longer needs them. Higher education institutions must reform their curricula urgently!&lt;/p&gt;

&lt;p&gt;Similarly, software companies that intend to retain their engineers must encourage them to update their skills and make this transition as smoothly as possible. As engineers, we take pride in our capabilities, but we sometimes show stubbornness and reluctance, or even disbelief, in the face of change, especially when it doesn't seem to favor us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to Begin?
&lt;/h2&gt;

&lt;p&gt;As a leader of an AWS User Group, one of my duties is to guide the community toward this future.&lt;/p&gt;

&lt;p&gt;There's no more powerful tool than education. Therefore, my first recommendation would be to pursue certifications related to this topic, created and designed by the very companies leading this transition. Personally, I believe there's no better incentive to study than a certification, as the knowledge it requires is not only theoretical but also encompasses practical day-to-day mastery; consequently, those without that experience will be compelled to acquire it on their own.&lt;/p&gt;

&lt;p&gt;The first and most important certification to begin this journey would be the "&lt;a href="https://aws.amazon.com/certification/certified-ai-practitioner/" rel="noopener noreferrer"&gt;AWS Certified AI Practitioner&lt;/a&gt;," which covers the fundamentals: what an LLM is, what a vector database is, what an agent is, how to create effective prompts to interact with an AI agent, among other aspects. Since it's an AWS certification, it requires at least basic knowledge of many of its services and their minimum configuration. Therefore, reviewing the material for the "&lt;a href="https://aws.amazon.com/certification/certified-cloud-practitioner/" rel="noopener noreferrer"&gt;AWS Certified Cloud Practitioner&lt;/a&gt;" certification is also very beneficial; and of course, if you're motivated to obtain it, all the better.&lt;/p&gt;

&lt;p&gt;Once you've obtained the first certification, you can begin creating AI agents and thus gradually advance in the transition.&lt;/p&gt;

&lt;p&gt;The first option is a fully managed service called &lt;a href="https://aws.amazon.com/bedrock/" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, which integrates numerous LLMs and offers tools for creating agents, integrating models, etc. &lt;a href="https://aws.amazon.com/blogs/machine-learning/category/artificial-intelligence/amazon-machine-learning/amazon-bedrock/" rel="noopener noreferrer"&gt;Here&lt;/a&gt; you can find an exhaustive list of articles related to Amazon Bedrock.&lt;/p&gt;

&lt;p&gt;AWS also offers an open-source framework called &lt;a href="https://strandsagents.com/latest/" rel="noopener noreferrer"&gt;Strands Agents&lt;/a&gt;, code-oriented (currently in Python) and easy to use. Its &lt;a href="https://github.com/strands-agents" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; repository contains excellent examples to get started, particularly highlighting the &lt;a href="https://github.com/strands-agents/samples/tree/main/02-samples/05-personal-assistant" rel="noopener noreferrer"&gt;personal assistant&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%2F0v7gbvaptwrkhj5btcaf.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%2F0v7gbvaptwrkhj5btcaf.png" alt="Agentic Personal Assistant" width="799" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This example employs several integrated agents, such as those for information search, image search, and video search, among others; as well as a main agent responsible for answering user questions. It constitutes a very didactic example for understanding Strands' capabilities in creating agents.&lt;/p&gt;

&lt;p&gt;Once you've acquired the necessary experience, you could pursue the "&lt;a href="https://aws.amazon.com/certification/certified-generative-ai-developer-professional/" rel="noopener noreferrer"&gt;AWS Certified AI Developer Professional&lt;/a&gt;" certification, which, at the time of writing this article, is in beta phase. Upon obtaining this professional-level certification, you'll be able to demonstrate proficiency in developing applications with and oriented toward AI.&lt;/p&gt;

&lt;p&gt;Although this certification, despite being professional-level, doesn't require any prerequisites for taking it, community sentiment suggests that the following are ideal certifications as preparation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/certification/certified-ai-practitioner/" rel="noopener noreferrer"&gt;AWS Certified AI Practitioner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/certification/certified-solutions-architect-associate/" rel="noopener noreferrer"&gt;AWS Certified Solutions Architect - Associate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/certification/certified-machine-learning-engineer-associate/" rel="noopener noreferrer"&gt;AWS Certified Machine Learning Engineer - Associate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/certification/certified-developer-associate/" rel="noopener noreferrer"&gt;AWS Certified Developer - Associate&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Time will tell if this is the case.&lt;/p&gt;

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

&lt;p&gt;If there's one certainty in this life, it's that everything changes. Therefore, we must all be prepared for the changes that will inevitably come.&lt;/p&gt;

&lt;p&gt;Personally, I've begun my own transition journey and must confess that I'm enjoying it immensely. I've always been a "Developer Wannabe" who hadn't found the time or courage to realize my aspirations. However, now I can do so without needing to invest so much time delving deeply into programming, and with the ability to create functional applications in significantly less time. I know my esteemed developer colleagues criticize vibe coding vehemently, but it's imperative to shed prejudices, relearn, and open our minds, because this change, which has already begun, won't stop, no matter who it displeases.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>A Kiro Ambassador at the AWS Community Day Bolivia 2026</title>
      <dc:creator>Sergio D. Rodríguez Inclán</dc:creator>
      <pubDate>Fri, 04 Sep 2026 21:07:49 +0000</pubDate>
      <link>https://dev.to/w4ls3n/a-kiro-ambassador-at-the-aws-community-day-bolivia-2026-4680</link>
      <guid>https://dev.to/w4ls3n/a-kiro-ambassador-at-the-aws-community-day-bolivia-2026-4680</guid>
      <description>&lt;p&gt;What a weekend!&lt;/p&gt;

&lt;p&gt;This past August 28th, I attended the largest AWS community event in my country: "AWS Community Day Bolivia 2026."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphit2b2z1muboc2pfko7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphit2b2z1muboc2pfko7.webp" alt="Opening ceremony of AWS Community Day Bolivia 2026" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; Ambassador, it is my duty and delight to showcase this wonderful tool to anyone who hasn't yet had the pleasure of working with it. Thanks to &lt;a href="https://www.linkedin.com/in/jverbel/" rel="noopener noreferrer"&gt;José Verbel&lt;/a&gt;, who pioneered the idea in Colombia of having a booth dedicated exclusively to &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, we replicated his idea here in Bolivia. We were very sad that he couldn't be with us that day, but we are hopeful it will happen soon somewhere in Latin America.&lt;/p&gt;

&lt;p&gt;At our booth, my good friend Alejandro and I welcomed countless people, handing out massive amounts of swag to those who were able to log into &lt;a href="https://kiro.dev/web/" rel="noopener noreferrer"&gt;Kiro Web&lt;/a&gt;, and even &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; credits to the most daring ones who managed to beat our challenge of live-coding an application in &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; and showcasing it to the audience. It was amazing to see the initiative and creativity of these folks. One of our winners even managed to get &lt;a href="https://kiro.dev/cli/" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt; running on his tablet! Plus, fortune smiled upon him in the raffle, and he took home a beautiful branded jacket.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmlb0zr57rcjpp38ciu0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvmlb0zr57rcjpp38ciu0.webp" alt="A packed room for the opening keynote" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A packed room for the opening keynote&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1epvg9n6noj2bq55i1ur.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1epvg9n6noj2bq55i1ur.webp" alt="Attendees scanning in at the Kiro booth" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Attendees scanning in at the Kiro booth&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1a1ei0dqwn4xn3l01i8v.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1a1ei0dqwn4xn3l01i8v.webp" alt="Some of the folks who stopped by to take on the challenge" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Some of the folks who stopped by to take on the challenge&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rreejriwde3wj0q1sd9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2rreejriwde3wj0q1sd9.webp" alt="Demoing Kiro CLI live at the booth" width="720" height="1280"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Demoing Kiro CLI live at the booth&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0265mew8y1qi1x7ht77.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp0265mew8y1qi1x7ht77.webp" alt="The main auditorium, packed for one of the talks" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The main auditorium, packed for one of the talks&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5txk129lw90u4l7xkzg.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5txk129lw90u4l7xkzg.webp" alt="Our raffle winner with the branded jacket" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Our raffle winner with the branded jacket&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckq3r4g531vlcdbloc33.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckq3r4g531vlcdbloc33.webp" alt="Couldn't resist the swag horns" width="800" height="1067"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Couldn't resist the swag horns&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And these are some of the apps our visitors built live, from scratch, in a matter of minutes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cdn.walsen.website/kiro-ambassador-community-day-2026/booth-challenge-timer-app.mp4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp5vwpwk1b2zlfkwxy8a6.webp" alt="A countdown timer app, built live at the booth" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A countdown timer app, built live at the booth — click the thumbnail to watch&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cdn.walsen.website/kiro-ambassador-community-day-2026/booth-challenge-wheel-app.mp4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7esstn4do3ukso037wok.webp" alt="A prize wheel, complete with winner history" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A prize wheel, complete with winner history — click the thumbnail to watch&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cdn.walsen.website/kiro-ambassador-community-day-2026/booth-challenge-form-app.mp4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fubgus8a6qlql99u2ojpw.webp" alt="A records intake form, built during the challenge" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A records intake form, built during the challenge — click the thumbnail to watch&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cdn.walsen.website/kiro-ambassador-community-day-2026/booth-challenge-presentation.mp4" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdwms26af5mpckaud1aoc.webp" alt="One of our winners presenting his solution to the audience" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One of our winners presenting his solution to the audience — click the thumbnail to watch&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I want to note that most of the people who approached us asked: why choose &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; over other solutions? The answer is simple: quality and accessibility.&lt;/p&gt;

&lt;p&gt;Every serious developer or development team has an initial planning stage, where they shape their future application according to technical and business criteria; this very thing can be done with &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;. Furthermore, &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; guides you through this process with its innate ability to generate specifications and design using &lt;a href="https://kiro.dev/docs/specs/" rel="noopener noreferrer"&gt;SDD&lt;/a&gt; (Spec-Driven-Development). From there, during implementation, you can combine a set of powerful features that &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; brings to the table to make the user's life easier. To summarize, what surprised our visitors the most was that we weren't just talking about a regular IDE—even one with agentic capabilities—but rather a suite of interconnected products and features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Out-of-the-box planning and requirements engineering capabilities:&lt;/strong&gt; Spec-Driven-Development (&lt;a href="https://kiro.dev/docs/specs/" rel="noopener noreferrer"&gt;SDD&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working on the go, on your way to study or work from your mobile or tablet:&lt;/strong&gt; &lt;a href="https://kiro.dev/docs/cloud-sessions/" rel="noopener noreferrer"&gt;Kiro Cloud&lt;/a&gt;, &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; Mobile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to incorporate &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;'s capabilities into your automation and even recover your previous work by integrating it with &lt;a href="https://kiro.dev/docs/cloud-sessions/" rel="noopener noreferrer"&gt;Kiro Cloud&lt;/a&gt;?:&lt;/strong&gt; &lt;a href="https://kiro.dev/cli/" rel="noopener noreferrer"&gt;Kiro CLI&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to establish a clear architecture and rules that define the scope of assisted development?:&lt;/strong&gt; &lt;a href="https://kiro.dev/docs/steering/" rel="noopener noreferrer"&gt;Steering Documents&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to trigger actions in response to specific events?:&lt;/strong&gt; &lt;a href="https://kiro.dev/docs/hooks/" rel="noopener noreferrer"&gt;Hooks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to extend prompts that are recurring in your workflow?:&lt;/strong&gt; &lt;a href="https://kiro.dev/docs/skills/" rel="noopener noreferrer"&gt;Skills&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to leverage capabilities from other services?:&lt;/strong&gt; &lt;a href="https://kiro.dev/docs/mcp/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; Integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need to combine all the above and package it?:&lt;/strong&gt; &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; &lt;a href="https://kiro.dev/docs/powers/" rel="noopener noreferrer"&gt;Powers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need a platform to run dozens of tasks in parallel using swarms of agents?:&lt;/strong&gt; &lt;a href="https://kiro.dev/crew" rel="noopener noreferrer"&gt;KiroCrew&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this, we clarify that it's not that other products don't offer something similar, but rather that the difference in service quality becomes apparent when you experience the speed, stability, and accuracy of the &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; agent. It uses Amazon Bedrock under the hood (a highly proven service in the market), and on top of that, AWS engineers add optimization layers that other products lack.&lt;/p&gt;

&lt;p&gt;Unfortunately, we couldn't attend to everyone who stopped by in the way we would have liked, but I believe we made an excellent impression on all of them.&lt;/p&gt;

&lt;p&gt;Aside from the booth, as I mentioned earlier, I also gave a Strands Agents workshop, where we gave away more &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt; credits and swag, but that will be a story for another post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbf6zblai4w5zfzgu713z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbf6zblai4w5zfzgu713z.webp" alt="Hands-on time during the Strands Agents workshop" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Hands-on time during the Strands Agents workshop&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdtc6uiht64ylri37sypx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdtc6uiht64ylri37sypx.webp" alt="A full room for the Strands Agents workshop" width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A full room for the Strands Agents workshop&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrxidfvl7488vt09hsyz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrxidfvl7488vt09hsyz.webp" alt="Walking through the agent architecture" width="800" height="1365"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Walking through the agent architecture&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://www.linkedin.com/in/jverbel/" rel="noopener noreferrer"&gt;José&lt;/a&gt; for his idea and continuous collaboration in the planning, to Alejandro for helping me at the booth, and to the event organizers who made it incredible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's talk
&lt;/h2&gt;

&lt;p&gt;If you or your company would like to know more about &lt;a href="https://kiro.dev" rel="noopener noreferrer"&gt;Kiro&lt;/a&gt;, feel free to write to me at &lt;a href="mailto:srinclan@arcamo.org"&gt;srinclan@arcamo.org&lt;/a&gt;, or find me on all my networks: &lt;a href="https://about.me/srinclan/" rel="noopener noreferrer"&gt;about.me/srinclan&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>DevSecOps Fundamentals Project</title>
      <dc:creator>Sergio D. Rodríguez Inclán</dc:creator>
      <pubDate>Fri, 20 Feb 2026 14:42:43 +0000</pubDate>
      <link>https://dev.to/w4ls3n/devsecops-fundamentals-project-19jd</link>
      <guid>https://dev.to/w4ls3n/devsecops-fundamentals-project-19jd</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every engineer has that one project — the one that starts as a simple idea and evolves into a full-blown learning laboratory. For me, that project is this &lt;strong&gt;Publisher Web App&lt;/strong&gt;: a system designed to publish announcements across multiple social media channels simultaneously. The primary use case? The &lt;strong&gt;AWS Certification Announcer&lt;/strong&gt;, a community tool where members submit their AWS certification achievements and the platform automatically publishes them to Facebook, Instagram, WhatsApp, LinkedIn, Email, etc.&lt;/p&gt;

&lt;p&gt;What began as a practical need for the AWS User Group quickly became an exercise in applying every DevSecOps principle I wanted to put into practice, but couldn't until now. This article walks through the fundamentals of the project — the architecture, the security posture, the deployment strategy, and the lessons learned along the way.&lt;/p&gt;

&lt;p&gt;The entire project is open source and available at &lt;a href="https://github.com/Walsen/devsecops-poc" rel="noopener noreferrer"&gt;github.com/Walsen/devsecops-poc&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vision: More Than Just Posting
&lt;/h2&gt;

&lt;p&gt;The goal was never to build "yet another social media scheduler." The vision was to create a platform that could serve as a reference implementation for modern cloud-native development — one that demonstrates how to build software that is secure by design, portable across deployment models, and maintainable over time.&lt;/p&gt;

&lt;p&gt;The platform needed to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publish to several channels&lt;/strong&gt;: Facebook, Instagram, WhatsApp, LinkedIn, Email, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support social authentication&lt;/strong&gt;: Google, GitHub, LinkedIn, or email/password via Amazon Cognito&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedule messages&lt;/strong&gt;: Queue announcements for future delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce role-based access&lt;/strong&gt;: Admin and Community Manager roles with strict boundaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be secure from day one&lt;/strong&gt;: Zero Trust networking, encrypted everything, WAF protection, and a hardened supply chain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run in two deployment modes&lt;/strong&gt;: Containers (ECS Fargate) or Serverless (Lambda + DynamoDB), switchable at deploy time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hexagonal Architecture: The Foundation
&lt;/h2&gt;

&lt;p&gt;If there is one architectural decision that made everything else possible, it is the adoption of &lt;strong&gt;hexagonal architecture&lt;/strong&gt; (also known as Ports &amp;amp; Adapters).&lt;/p&gt;

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

&lt;p&gt;This pattern separates the system into three concentric zones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Zone&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Dependencies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Driving Adapters&lt;/td&gt;
&lt;td&gt;Receive external input (HTTP, Kinesis events, cron)&lt;/td&gt;
&lt;td&gt;Depend on Inbound Ports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Core&lt;/td&gt;
&lt;td&gt;Business logic, use cases, domain model&lt;/td&gt;
&lt;td&gt;Zero external dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Driven Adapters&lt;/td&gt;
&lt;td&gt;Implement outbound integrations (DB, queues, APIs)&lt;/td&gt;
&lt;td&gt;Implement Outbound Ports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The application core — domain entities, use cases, and port interfaces — has absolutely zero knowledge of the outside world. It doesn't know if it's running on ECS or Lambda. It doesn't know if the database is PostgreSQL or DynamoDB. It only knows about abstractions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Domain Model
&lt;/h3&gt;

&lt;p&gt;The domain is built with pure Python dataclasses:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entity / Value Object&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;
&lt;code&gt;Message&lt;/code&gt; (Aggregate Root)&lt;/td&gt;
&lt;td&gt;Tracks content, channels, schedule, status, and per-channel deliveries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Certification&lt;/code&gt; (Entity)&lt;/td&gt;
&lt;td&gt;AWS certification achievement with member info and type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ChannelType&lt;/code&gt; (Value Object)&lt;/td&gt;
&lt;td&gt;Enum: &lt;code&gt;facebook&lt;/code&gt;, &lt;code&gt;instagram&lt;/code&gt;, &lt;code&gt;linkedin&lt;/code&gt;, &lt;code&gt;whatsapp&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;sms&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;MessageStatus&lt;/code&gt; (Value Object)&lt;/td&gt;
&lt;td&gt;Lifecycle: &lt;code&gt;DRAFT&lt;/code&gt; → &lt;code&gt;SCHEDULED&lt;/code&gt; → &lt;code&gt;PROCESSING&lt;/code&gt; → &lt;code&gt;DELIVERED&lt;/code&gt; / &lt;code&gt;FAILED&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DeliveryResult&lt;/code&gt; (Value Object)&lt;/td&gt;
&lt;td&gt;Per-channel outcome with external ID or error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Ports and Adapters in Practice
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;MessageRepository&lt;/code&gt; port is the same interface regardless of deployment mode. Only the adapter changes:&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="c1"&gt;# Port (shared across both modes)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MessageRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@abstractmethod&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;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&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;Message&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&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;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# Container adapter
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostgresMessageRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MessageRepository&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;

&lt;span class="c1"&gt;# Serverless adapter
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DynamoMessageRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MessageRepository&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_name&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the key enabler for the dual-mode deployment strategy. Business logic is completely decoupled from infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Services, One Stream
&lt;/h2&gt;

&lt;p&gt;The platform is composed of three microservices connected by Amazon Kinesis Data Streams:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBBUElbIkFQSSBTZXJ2aWNlIl0KICAgICAgICBGQVNUQVBJWyJGYXN0QVBJPGJyLz5SRVNUICsgQXV0aCJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBRdWV1ZVsiRXZlbnQgU3RyZWFtIl0KICAgICAgICBLSU5FU0lTWyJLaW5lc2lzPGJyLz5EYXRhIFN0cmVhbXMiXQogICAgZW5kCgogICAgc3ViZ3JhcGggV29ya2VyWyJXb3JrZXIgU2VydmljZSJdCiAgICAgICAgQ09OU1VNRVJbIktpbmVzaXMgQ29uc3VtZXI8YnIvPkNoYW5uZWwgRGVsaXZlcnkiXQogICAgZW5kCgogICAgc3ViZ3JhcGggU2NoZWR1bGVyWyJTY2hlZHVsZXIgU2VydmljZSJdCiAgICAgICAgQ1JPTlsiQVBTY2hlZHVsZXI8YnIvPkR1ZSBNZXNzYWdlIFNjYW5uZXIiXQogICAgZW5kCgogICAgc3ViZ3JhcGggREJbIkRhdGEgTGF5ZXIiXQogICAgICAgIFJEU1soIlBvc3RncmVTUUw8YnIvPm9yIER5bmFtb0RCIildCiAgICBlbmQKCiAgICBGQVNUQVBJIC0tPnwiUHVibGlzaCBFdmVudCJ8IEtJTkVTSVMKICAgIEtJTkVTSVMgLS0%2BfCJDb25zdW1lInwgQ09OU1VNRVIKICAgIENST04gLS0%2BfCJQb2xsIGR1ZSBtZXNzYWdlcyJ8IFJEUwogICAgQ1JPTiAtLT58IlB1Ymxpc2ggRXZlbnQifCBLSU5FU0lTCiAgICBGQVNUQVBJIC0tPiBSRFMKCiAgICBzdHlsZSBRdWV1ZSBmaWxsOiNmZmYzZTAKICAgIHN0eWxlIERCIGZpbGw6I2ZjZTRlYw%3D%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBBUElbIkFQSSBTZXJ2aWNlIl0KICAgICAgICBGQVNUQVBJWyJGYXN0QVBJPGJyLz5SRVNUICsgQXV0aCJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBRdWV1ZVsiRXZlbnQgU3RyZWFtIl0KICAgICAgICBLSU5FU0lTWyJLaW5lc2lzPGJyLz5EYXRhIFN0cmVhbXMiXQogICAgZW5kCgogICAgc3ViZ3JhcGggV29ya2VyWyJXb3JrZXIgU2VydmljZSJdCiAgICAgICAgQ09OU1VNRVJbIktpbmVzaXMgQ29uc3VtZXI8YnIvPkNoYW5uZWwgRGVsaXZlcnkiXQogICAgZW5kCgogICAgc3ViZ3JhcGggU2NoZWR1bGVyWyJTY2hlZHVsZXIgU2VydmljZSJdCiAgICAgICAgQ1JPTlsiQVBTY2hlZHVsZXI8YnIvPkR1ZSBNZXNzYWdlIFNjYW5uZXIiXQogICAgZW5kCgogICAgc3ViZ3JhcGggREJbIkRhdGEgTGF5ZXIiXQogICAgICAgIFJEU1soIlBvc3RncmVTUUw8YnIvPm9yIER5bmFtb0RCIildCiAgICBlbmQKCiAgICBGQVNUQVBJIC0tPnwiUHVibGlzaCBFdmVudCJ8IEtJTkVTSVMKICAgIEtJTkVTSVMgLS0%2BfCJDb25zdW1lInwgQ09OU1VNRVIKICAgIENST04gLS0%2BfCJQb2xsIGR1ZSBtZXNzYWdlcyJ8IFJEUwogICAgQ1JPTiAtLT58IlB1Ymxpc2ggRXZlbnQifCBLSU5FU0lTCiAgICBGQVNUQVBJIC0tPiBSRFMKCiAgICBzdHlsZSBRdWV1ZSBmaWxsOiNmZmYzZTAKICAgIHN0eWxlIERCIGZpbGw6I2ZjZTRlYw%3D%3D" alt="Mermaid Diagram" width="1040" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Service&lt;/strong&gt;: Handles HTTP requests, authentication, and message scheduling. Implements the full hexagonal stack with FastAPI routes, middleware, and Cognito JWT validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worker Service&lt;/strong&gt;: Consumes messages from Kinesis and delivers them to the appropriate channels. Supports two publishing strategies — direct API calls or AI-powered content adaptation via Amazon Bedrock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduler Service&lt;/strong&gt;: Polls the database for scheduled messages and publishes them to Kinesis when their delivery time arrives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each service has its own &lt;code&gt;main.py&lt;/code&gt; that serves as the composition root — the only place where concrete implementations are imported and wired together. No other module imports from &lt;code&gt;infrastructure/&lt;/code&gt; directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dual-Mode Deployment: Containers or Serverless
&lt;/h2&gt;

&lt;p&gt;This is where hexagonal architecture pays off in a very tangible way. The platform supports two fully independent deployment modes, switchable at deploy time via a single CI/CD parameter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Containers (&lt;code&gt;infra/&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Serverless (&lt;code&gt;infra-fs/&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;ECS Fargate&lt;/td&gt;
&lt;td&gt;Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL (RDS)&lt;/td&gt;
&lt;td&gt;DynamoDB (Single-Table)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API Gateway&lt;/td&gt;
&lt;td&gt;ALB + CloudFront&lt;/td&gt;
&lt;td&gt;API Gateway + CloudFront&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduler&lt;/td&gt;
&lt;td&gt;ECS Service (APScheduler)&lt;/td&gt;
&lt;td&gt;EventBridge + Lambda&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost (low traffic)&lt;/td&gt;
&lt;td&gt;~$180-200/mo&lt;/td&gt;
&lt;td&gt;~$5-15/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both modes share the same domain and application layers. Only the infrastructure adapters change. The deploy workflow selects the mode via &lt;code&gt;infra_type&lt;/code&gt; input (&lt;code&gt;containers&lt;/code&gt; or &lt;code&gt;serverless&lt;/code&gt;), routing to the corresponding CDK project. Stack names are fully independent, so both can coexist in the same AWS account during migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters
&lt;/h3&gt;

&lt;p&gt;Different stages of a project have different needs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Recommended Mode&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development / Prototyping&lt;/td&gt;
&lt;td&gt;Serverless&lt;/td&gt;
&lt;td&gt;Near-zero cost, instant deploys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Staging / QA&lt;/td&gt;
&lt;td&gt;Either&lt;/td&gt;
&lt;td&gt;Match production or save costs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production (steady traffic)&lt;/td&gt;
&lt;td&gt;Containers&lt;/td&gt;
&lt;td&gt;Predictable latency, no cold starts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production (variable traffic)&lt;/td&gt;
&lt;td&gt;Serverless&lt;/td&gt;
&lt;td&gt;Pay-per-use, auto-scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can start cheap with serverless and migrate to containers when traffic justifies the fixed cost — or vice versa — without rewriting a single line of business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Both Simultaneously
&lt;/h3&gt;

&lt;p&gt;Since stack names are independent, you can run both modes in parallel during a migration window. This allows smoke testing against the new mode before cutting over, gradual traffic shifting using weighted DNS routing (Route 53), and instant rollback by switching DNS back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero Trust Security: Trust Nothing, Verify Everything
&lt;/h2&gt;

&lt;p&gt;Security is not a feature you bolt on at the end. It is a design principle that permeates every layer of the system. The platform implements a Zero Trust architecture based on three pillars:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgICBzdWJncmFwaCBaVFsiWmVybyBUcnVzdCBQcmluY2lwbGVzIl0KICAgICAgICBWRVJJRllbIlZlcmlmeSBFeHBsaWNpdGx5PGJyLz5BdXRob3JpemUgYmFzZWQgb24gY29udGV4dDxici8%2BTXVsdGktZmFjdG9yIGF1dGhlbnRpY2F0aW9uIl0KICAgICAgICBMRUFTVFsiTGVhc3QgUHJpdmlsZWdlPGJyLz5KdXN0LWluLXRpbWUgYWNjZXNzPGJyLz5Sb2xlLWJhc2VkIHBlcm1pc3Npb25zIl0KICAgICAgICBCUkVBQ0hbIkFzc3VtZSBCcmVhY2g8YnIvPk1pbmltaXplIGJsYXN0IHJhZGl1czxici8%2BU2VnbWVudCBhY2Nlc3M8YnIvPkVuZC10by1lbmQgZW5jcnlwdGlvbiJdCiAgICBlbmQ%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgICBzdWJncmFwaCBaVFsiWmVybyBUcnVzdCBQcmluY2lwbGVzIl0KICAgICAgICBWRVJJRllbIlZlcmlmeSBFeHBsaWNpdGx5PGJyLz5BdXRob3JpemUgYmFzZWQgb24gY29udGV4dDxici8%2BTXVsdGktZmFjdG9yIGF1dGhlbnRpY2F0aW9uIl0KICAgICAgICBMRUFTVFsiTGVhc3QgUHJpdmlsZWdlPGJyLz5KdXN0LWluLXRpbWUgYWNjZXNzPGJyLz5Sb2xlLWJhc2VkIHBlcm1pc3Npb25zIl0KICAgICAgICBCUkVBQ0hbIkFzc3VtZSBCcmVhY2g8YnIvPk1pbmltaXplIGJsYXN0IHJhZGl1czxici8%2BU2VnbWVudCBhY2Nlc3M8YnIvPkVuZC10by1lbmQgZW5jcnlwdGlvbiJdCiAgICBlbmQ%3D" alt="Mermaid Diagram" width="345" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Security
&lt;/h3&gt;

&lt;p&gt;All traffic flows through multiple security layers:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBVU0VSWygi8J%2BRpCBVc2VyIildIC0tPnwiVExTIDEuMyJ8IENGWyJDbG91ZEZyb250PGJyLz4rIFNoaWVsZCJdCiAgICBDRiAtLT4gV0FGWyJXQUY8YnIvPk9XQVNQIFJ1bGVzIl0KICAgIFdBRiAtLT58IlRMUyAxLjMifCBBTEJbIkFMQiJdCiAgICBBTEIgLS0%2BfCJUTFMgMS4zInwgRUNTWyJFQ1MgRmFyZ2F0ZSJdCiAgICBFQ1MgLS0%2BfCJUTFMifCBSRFNbKCJSRFM8YnIvPlBvc3RncmVTUUwiKV0KCiAgICBzdHlsZSBDRiBmaWxsOiNlOGY1ZTkKICAgIHN0eWxlIFdBRiBmaWxsOiNlOGY1ZTk%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBVU0VSWygi8J%2BRpCBVc2VyIildIC0tPnwiVExTIDEuMyJ8IENGWyJDbG91ZEZyb250PGJyLz4rIFNoaWVsZCJdCiAgICBDRiAtLT4gV0FGWyJXQUY8YnIvPk9XQVNQIFJ1bGVzIl0KICAgIFdBRiAtLT58IlRMUyAxLjMifCBBTEJbIkFMQiJdCiAgICBBTEIgLS0%2BfCJUTFMgMS4zInwgRUNTWyJFQ1MgRmFyZ2F0ZSJdCiAgICBFQ1MgLS0%2BfCJUTFMifCBSRFNbKCJSRFM8YnIvPlBvc3RncmVTUUwiKV0KCiAgICBzdHlsZSBDRiBmaWxsOiNlOGY1ZTkKICAgIHN0eWxlIFdBRiBmaWxsOiNlOGY1ZTk%3D" alt="Mermaid Diagram" width="1185" height="113"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The VPC is segmented into public, private, and isolated subnets. Security groups enforce micro-segmentation — the ALB can only talk to the API, the API can only talk to the Worker and the database, and the database accepts connections only from authorized services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication Flow
&lt;/h3&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgICBzdWJncmFwaCBQcm92aWRlcnNbIklkZW50aXR5IFByb3ZpZGVycyJdCiAgICAgICAgQ09HTklUT1siQ29nbml0bzxici8%2BRW1haWwvUGFzc3dvcmQiXQogICAgICAgIEdPT0dMRVsiR29vZ2xlPGJyLz5PQXV0aCAyLjAiXQogICAgICAgIEdJVEhVQlsiR2l0SHViPGJyLz5PSURDIl0KICAgICAgICBMSU5LRURJTlsiTGlua2VkSW48YnIvPk9JREMiXQogICAgZW5kCgogICAgc3ViZ3JhcGggUG9vbFsiQ29nbml0byBVc2VyIFBvb2wiXQogICAgICAgIEZFREVSQVRFWyJGZWRlcmF0aW9uIExheWVyIl0KICAgICAgICBVU0VSU1siVXNlciBEaXJlY3RvcnkiXQogICAgICAgIEdST1VQU1siR3JvdXBzPGJyLz5hZG1pbiwgY29tbXVuaXR5LW1hbmFnZXIiXQogICAgZW5kCgogICAgc3ViZ3JhcGggVG9rZW5zWyJUb2tlbiBJc3N1YW5jZSJdCiAgICAgICAgSldUWyJKV1QgVG9rZW5zPGJyLz7igKIgQWNjZXNzIFRva2VuICgxaCk8YnIvPuKAoiBJRCBUb2tlbiAoMWgpPGJyLz7igKIgUmVmcmVzaCBUb2tlbiAoMzBkKSJdCiAgICBlbmQKCiAgICBDT0dOSVRPIC0tPiBGRURFUkFURQogICAgR09PR0xFIC0tPiBGRURFUkFURQogICAgR0lUSFVCIC0tPiBGRURFUkFURQogICAgTElOS0VESU4gLS0%2BIEZFREVSQVRFCiAgICBGRURFUkFURSAtLT4gVVNFUlMKICAgIFVTRVJTIC0tPiBHUk9VUFMKICAgIEdST1VQUyAtLT4gSldU" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgICBzdWJncmFwaCBQcm92aWRlcnNbIklkZW50aXR5IFByb3ZpZGVycyJdCiAgICAgICAgQ09HTklUT1siQ29nbml0bzxici8%2BRW1haWwvUGFzc3dvcmQiXQogICAgICAgIEdPT0dMRVsiR29vZ2xlPGJyLz5PQXV0aCAyLjAiXQogICAgICAgIEdJVEhVQlsiR2l0SHViPGJyLz5PSURDIl0KICAgICAgICBMSU5LRURJTlsiTGlua2VkSW48YnIvPk9JREMiXQogICAgZW5kCgogICAgc3ViZ3JhcGggUG9vbFsiQ29nbml0byBVc2VyIFBvb2wiXQogICAgICAgIEZFREVSQVRFWyJGZWRlcmF0aW9uIExheWVyIl0KICAgICAgICBVU0VSU1siVXNlciBEaXJlY3RvcnkiXQogICAgICAgIEdST1VQU1siR3JvdXBzPGJyLz5hZG1pbiwgY29tbXVuaXR5LW1hbmFnZXIiXQogICAgZW5kCgogICAgc3ViZ3JhcGggVG9rZW5zWyJUb2tlbiBJc3N1YW5jZSJdCiAgICAgICAgSldUWyJKV1QgVG9rZW5zPGJyLz7igKIgQWNjZXNzIFRva2VuICgxaCk8YnIvPuKAoiBJRCBUb2tlbiAoMWgpPGJyLz7igKIgUmVmcmVzaCBUb2tlbiAoMzBkKSJdCiAgICBlbmQKCiAgICBDT0dOSVRPIC0tPiBGRURFUkFURQogICAgR09PR0xFIC0tPiBGRURFUkFURQogICAgR0lUSFVCIC0tPiBGRURFUkFURQogICAgTElOS0VESU4gLS0%2BIEZFREVSQVRFCiAgICBGRURFUkFURSAtLT4gVVNFUlMKICAgIFVTRVJTIC0tPiBHUk9VUFMKICAgIEdST1VQUyAtLT4gSldU" alt="Mermaid Diagram" width="773" height="756"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every request is validated against Cognito JWTs with strict algorithm restriction (RS256 only), audience and issuer validation, and JWKS caching with TTL refresh. OAuth credentials for social providers are stored in AWS Secrets Manager.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Security Middleware
&lt;/h3&gt;

&lt;p&gt;The API service implements a comprehensive security middleware stack:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Implementation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Authentication&lt;/td&gt;
&lt;td&gt;Cognito JWT validation with algorithm restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF Protection&lt;/td&gt;
&lt;td&gt;Double Submit Cookie with HMAC-signed tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate Limiting&lt;/td&gt;
&lt;td&gt;Per-user sliding window (60 req/min)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request Validation&lt;/td&gt;
&lt;td&gt;1MB size limit, input sanitization, HTML escaping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Headers&lt;/td&gt;
&lt;td&gt;CSP, HSTS, X-Frame-Options, Permissions-Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content Filtering&lt;/td&gt;
&lt;td&gt;Prompt injection detection + PII scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotency&lt;/td&gt;
&lt;td&gt;Hash-based dedup on message_id + channels&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Data Protection
&lt;/h3&gt;

&lt;p&gt;Encryption is applied at every layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;In Transit&lt;/strong&gt;: TLS 1.3 everywhere — CloudFront to ALB, ALB to ECS, ECS to RDS, ECS to Kinesis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Rest&lt;/strong&gt;: KMS encryption for S3, RDS, and Kinesis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In Use&lt;/strong&gt;: No PII in logs, field-level encryption for sensitive data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Secure Supply Chain
&lt;/h2&gt;

&lt;p&gt;The build pipeline is hardened against supply chain attacks with multiple security gates:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBTb3VyY2VbIlNvdXJjZSJdCiAgICAgICAgR0lUWyJHaXQ8YnIvPlNpZ25lZCBDb21taXRzIl0KICAgICAgICBQUlsiUFIgUmV2aWV3PGJyLz5SZXF1aXJlZCJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBTY2FuWyJTZWN1cml0eSBTY2FubmluZyJdCiAgICAgICAgREVQWyJEZXBlbmRlbmN5PGJyLz5BdWRpdCJdCiAgICAgICAgU0FTVFsiU3RhdGljPGJyLz5BbmFseXNpcyJdCiAgICAgICAgU0VDUkVUWyJTZWNyZXQ8YnIvPlNjYW5uaW5nIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIEJ1aWxkWyJDb250YWluZXIgQnVpbGQiXQogICAgICAgIEJBU0VbIkRpc3Ryb2xlc3M8YnIvPkJhc2UgSW1hZ2UiXQogICAgICAgIE5PTlJPT1RbIk5vbi1yb290PGJyLz5Vc2VyIl0KICAgICAgICBSRUFET05MWVsiUmVhZC1vbmx5PGJyLz5GaWxlc3lzdGVtIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIFNpZ25bIlNpZ25pbmciXQogICAgICAgIFNJR05bIkltYWdlPGJyLz5TaWduaW5nIl0KICAgICAgICBTQk9NWyJTQk9NPGJyLz5HZW5lcmF0aW9uIl0KICAgIGVuZAoKICAgIFNvdXJjZSAtLT4gU2NhbiAtLT4gQnVpbGQgLS0%2BIFNpZ24%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBTb3VyY2VbIlNvdXJjZSJdCiAgICAgICAgR0lUWyJHaXQ8YnIvPlNpZ25lZCBDb21taXRzIl0KICAgICAgICBQUlsiUFIgUmV2aWV3PGJyLz5SZXF1aXJlZCJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBTY2FuWyJTZWN1cml0eSBTY2FubmluZyJdCiAgICAgICAgREVQWyJEZXBlbmRlbmN5PGJyLz5BdWRpdCJdCiAgICAgICAgU0FTVFsiU3RhdGljPGJyLz5BbmFseXNpcyJdCiAgICAgICAgU0VDUkVUWyJTZWNyZXQ8YnIvPlNjYW5uaW5nIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIEJ1aWxkWyJDb250YWluZXIgQnVpbGQiXQogICAgICAgIEJBU0VbIkRpc3Ryb2xlc3M8YnIvPkJhc2UgSW1hZ2UiXQogICAgICAgIE5PTlJPT1RbIk5vbi1yb290PGJyLz5Vc2VyIl0KICAgICAgICBSRUFET05MWVsiUmVhZC1vbmx5PGJyLz5GaWxlc3lzdGVtIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIFNpZ25bIlNpZ25pbmciXQogICAgICAgIFNJR05bIkltYWdlPGJyLz5TaWduaW5nIl0KICAgICAgICBTQk9NWyJTQk9NPGJyLz5HZW5lcmF0aW9uIl0KICAgIGVuZAoKICAgIFNvdXJjZSAtLT4gU2NhbiAtLT4gQnVpbGQgLS0%2BIFNpZ24%3D" alt="Mermaid Diagram" width="1904" height="152"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Defense in Depth: Multiple Scanners
&lt;/h3&gt;

&lt;p&gt;No single scanner catches everything. The project uses six complementary tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scanner&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Semgrep&lt;/td&gt;
&lt;td&gt;SAST&lt;/td&gt;
&lt;td&gt;OWASP Top 10 patterns, custom rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bandit&lt;/td&gt;
&lt;td&gt;SAST&lt;/td&gt;
&lt;td&gt;Python-specific security issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pip-audit&lt;/td&gt;
&lt;td&gt;SCA&lt;/td&gt;
&lt;td&gt;Python CVE database (PyPI Advisory)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trivy&lt;/td&gt;
&lt;td&gt;SCA&lt;/td&gt;
&lt;td&gt;SBOM vulnerability scanning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gitleaks&lt;/td&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;Hardcoded secrets detection in git history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkov&lt;/td&gt;
&lt;td&gt;IaC&lt;/td&gt;
&lt;td&gt;AWS security misconfigurations in CDK/CloudFormation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each tool covers blind spots the others miss. Semgrep catches SQL injection and XSS patterns. Bandit finds Python-specific issues like unsafe YAML loading. pip-audit and Trivy handle known CVEs. Gitleaks prevents credential leaks. Checkov ensures the infrastructure itself is secure — no public S3 buckets, no missing encryption, no overly permissive IAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  Container Security
&lt;/h3&gt;

&lt;p&gt;Production containers are built with security as a first-class concern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distroless base images&lt;/strong&gt;: No shell, minimal attack surface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-root user&lt;/strong&gt;: Containers run as &lt;code&gt;nonroot&lt;/code&gt; by default&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only filesystem&lt;/strong&gt;: No writes to the root filesystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinned dependencies&lt;/strong&gt;: Lock files with hashes for reproducibility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource limits&lt;/strong&gt;: CPU and memory constraints to prevent abuse&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CI/CD with GitHub Actions OIDC
&lt;/h3&gt;

&lt;p&gt;The project uses GitHub Actions OIDC to assume AWS IAM roles without storing long-lived credentials. A CDK bootstrap stack (&lt;code&gt;GitHubOIDCStack&lt;/code&gt;) creates the OIDC provider and two IAM roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Role&lt;/strong&gt; (&lt;code&gt;github-actions-deploy&lt;/code&gt;): For CDK deployments and ECR pushes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Scan Role&lt;/strong&gt; (&lt;code&gt;github-actions-security-scan&lt;/code&gt;): For Prowler audits with read-only access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No AWS access keys are stored in GitHub Secrets. Every deployment uses short-lived credentials (1 hour) obtained via OIDC federation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Golden Thread: Distributed Security Tracing
&lt;/h2&gt;

&lt;p&gt;One of the most interesting aspects of the project is the &lt;strong&gt;Golden Thread&lt;/strong&gt; — a distributed tracing exercise that demonstrates end-to-end request correlation across all infrastructure layers using a single correlation ID (&lt;code&gt;X-Request-ID&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The correlation ID flows through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Edge Layer&lt;/strong&gt;: WAF logs (CloudFront + ALB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Layer&lt;/strong&gt;: API structured logs (structlog JSON)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async Layer&lt;/strong&gt;: Kinesis event → Worker logs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Layer&lt;/strong&gt;: PostgreSQL audit logs (pgaudit) with SQL comments
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/* correlation_id=golden-thread-test-1739... */ INSERT INTO messages (id, content_text, ...) VALUES (...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All queryable from CloudWatch Logs Insights with a single filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;correlation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;correlation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"YOUR_TRACE_ID"&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;asc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Attack Simulation Exercises
&lt;/h3&gt;

&lt;p&gt;The project includes hands-on exercises that simulate real attacks and trace them across layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQL Injection&lt;/strong&gt;: Send a &lt;code&gt;' OR 1=1--&lt;/code&gt; payload, verify WAF blocks it, confirm the request never reaches the application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS Attempts&lt;/strong&gt;: Send &lt;code&gt;&amp;lt;script&amp;gt;alert(1)&amp;lt;/script&amp;gt;&lt;/code&gt;, trace the WAF block in CloudWatch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brute Force Detection&lt;/strong&gt;: Send 60 rapid requests, observe rate limiting kick in, check CloudWatch alarms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Pipeline Trace&lt;/strong&gt;: Schedule a message with a known correlation ID, trace it from CloudFront WAF → ALB WAF → API → Kinesis → Worker → PostgreSQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The documentation also includes exercises using professional penetration testing tools — nmap, nikto, sqlmap, ffuf, nuclei, and hydra — each with corresponding CloudWatch queries to trace the attack signatures across the infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-Powered Content Adaptation
&lt;/h2&gt;

&lt;p&gt;The Worker service supports two publishing strategies, selectable via configuration:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgICBzdWJncmFwaCBQdWJsaXNoZXJbIlNvY2lhbE1lZGlhUHVibGlzaGVyIFBvcnQiXQogICAgICAgIERJUkVDVFsiRGlyZWN0UHVibGlzaGVyPGJyLz5TYW1lIGNvbnRlbnQsIGFsbCBjaGFubmVscyJdCiAgICAgICAgQUdFTlRbIkFnZW50UHVibGlzaGVyPGJyLz5BSS1hZGFwdGVkIHBlciBwbGF0Zm9ybSJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBUb29sc1siQHRvb2wgRnVuY3Rpb25zIl0KICAgICAgICBGQlsicG9zdF90b19mYWNlYm9vaygpIl0KICAgICAgICBJR1sicG9zdF90b19pbnN0YWdyYW0oKSJdCiAgICAgICAgTElbInBvc3RfdG9fbGlua2VkaW4oKSJdCiAgICAgICAgV0FbInNlbmRfd2hhdHNhcHAoKSJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBHYXRld2F5c1siQ2hhbm5lbCBHYXRld2F5cyJdCiAgICAgICAgRkJfR1dbIkZhY2Vib29rIEdhdGV3YXkiXQogICAgICAgIElHX0dXWyJJbnN0YWdyYW0gR2F0ZXdheSJdCiAgICAgICAgTElfR1dbIkxpbmtlZEluIEdhdGV3YXkiXQogICAgICAgIFdBX0dXWyJXaGF0c0FwcCBHYXRld2F5Il0KICAgIGVuZAoKICAgIERJUkVDVCAtLT4gR2F0ZXdheXMKICAgIEFHRU5UIC0tPiBUb29scyAtLT4gR2F0ZXdheXM%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgICBzdWJncmFwaCBQdWJsaXNoZXJbIlNvY2lhbE1lZGlhUHVibGlzaGVyIFBvcnQiXQogICAgICAgIERJUkVDVFsiRGlyZWN0UHVibGlzaGVyPGJyLz5TYW1lIGNvbnRlbnQsIGFsbCBjaGFubmVscyJdCiAgICAgICAgQUdFTlRbIkFnZW50UHVibGlzaGVyPGJyLz5BSS1hZGFwdGVkIHBlciBwbGF0Zm9ybSJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBUb29sc1siQHRvb2wgRnVuY3Rpb25zIl0KICAgICAgICBGQlsicG9zdF90b19mYWNlYm9vaygpIl0KICAgICAgICBJR1sicG9zdF90b19pbnN0YWdyYW0oKSJdCiAgICAgICAgTElbInBvc3RfdG9fbGlua2VkaW4oKSJdCiAgICAgICAgV0FbInNlbmRfd2hhdHNhcHAoKSJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBHYXRld2F5c1siQ2hhbm5lbCBHYXRld2F5cyJdCiAgICAgICAgRkJfR1dbIkZhY2Vib29rIEdhdGV3YXkiXQogICAgICAgIElHX0dXWyJJbnN0YWdyYW0gR2F0ZXdheSJdCiAgICAgICAgTElfR1dbIkxpbmtlZEluIEdhdGV3YXkiXQogICAgICAgIFdBX0dXWyJXaGF0c0FwcCBHYXRld2F5Il0KICAgIGVuZAoKICAgIERJUkVDVCAtLT4gR2F0ZXdheXMKICAgIEFHRU5UIC0tPiBUb29scyAtLT4gR2F0ZXdheXM%3D" alt="Mermaid Diagram" width="616" height="1116"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;AgentPublisher&lt;/strong&gt; uses the &lt;a href="https://strandsagents.com" rel="noopener noreferrer"&gt;Strands Agents SDK&lt;/a&gt; with Amazon Bedrock (Claude) to intelligently adapt content for each platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Adaptation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Facebook&lt;/td&gt;
&lt;td&gt;Longer posts, emojis, hashtags at end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instagram&lt;/td&gt;
&lt;td&gt;Visual focus, heavy emojis, hashtags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn&lt;/td&gt;
&lt;td&gt;Professional tone, formal language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp&lt;/td&gt;
&lt;td&gt;Short, celebratory, personal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent reasons about which tools to call, executes them, observes results, and continues until the task is complete. Content filtering middleware protects against prompt injection and PII leakage in AI-generated output.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI-Powered Security Testing Agent
&lt;/h2&gt;

&lt;p&gt;Perhaps the most novel addition to the project is the &lt;strong&gt;Security Testing Agent&lt;/strong&gt; — an AI-powered interactive penetration testing assistant built with the Strands Agents SDK and Amazon Bedrock. Instead of manually running security tests and interpreting results, you have a conversation with an agent that can run tests, diagnose failures, query AWS resources, and even auto-fix test code.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBVc2VyCiAgICAgICAgQ0xJWyJUZXJtaW5hbCAvIEludGVyYWN0aXZlIFByb21wdCJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBBZ2VudFsiU3RyYW5kcyBBZ2VudCAoQ2xhdWRlIEhhaWt1IDQuNSkiXQogICAgICAgIExPT1BbIkFnZW50IExvb3A8YnIvPuKAoiBSZWFzb25pbmc8YnIvPuKAoiBUb29sIFNlbGVjdGlvbjxici8%2B4oCiIEV4ZWN1dGlvbiJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBUb29sc1siQHRvb2wgRnVuY3Rpb25zIl0KICAgICAgICBURVNUWyJUZXN0IFJ1bm5lcnM8YnIvPnJ1bl9weXRlc3RfdGVzdDxici8%2BcnVuX2FsbF90ZXN0c19wYXJhbGxlbDxici8%2BcnVuX2Zhc3RfdGVzdHMiXQogICAgICAgIEFXU1siQVdTIFJlYWQtT25seTxici8%2BQ2xvdWRGb3JtYXRpb24gb3V0cHV0czxici8%2BQ2xvdWRXYXRjaCBMb2dzPGJyLz5XQUYgV2ViIEFDTHMiXQogICAgICAgIENPREVbIkNvZGUgVG9vbHM8YnIvPnJlYWRfdGVzdF9maWxlPGJyLz5maXhfdGVzdF9jb2RlIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIEluZnJhWyJBV1MgUmVzb3VyY2VzIChSZWFkLU9ubHkpIl0KICAgICAgICBDRlsiQ2xvdWRGb3JtYXRpb24iXQogICAgICAgIENXWyJDbG91ZFdhdGNoIExvZ3MiXQogICAgICAgIFdBRlsiV0FGIHYyIl0KICAgIGVuZAoKICAgIENMSSAtLT4gTE9PUAogICAgTE9PUCAtLT4gVEVTVAogICAgTE9PUCAtLT4gQVdTCiAgICBMT09QIC0tPiBDT0RFCiAgICBBV1MgLS0%2BIENGCiAgICBBV1MgLS0%2BIENXCiAgICBBV1MgLS0%2BIFdBRgoKICAgIHN0eWxlIEFnZW50IGZpbGw6I2UzZjJmZAogICAgc3R5bGUgVG9vbHMgZmlsbDojZmZmM2UwCiAgICBzdHlsZSBJbmZyYSBmaWxsOiNlOGY1ZTk%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBVc2VyCiAgICAgICAgQ0xJWyJUZXJtaW5hbCAvIEludGVyYWN0aXZlIFByb21wdCJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBBZ2VudFsiU3RyYW5kcyBBZ2VudCAoQ2xhdWRlIEhhaWt1IDQuNSkiXQogICAgICAgIExPT1BbIkFnZW50IExvb3A8YnIvPuKAoiBSZWFzb25pbmc8YnIvPuKAoiBUb29sIFNlbGVjdGlvbjxici8%2B4oCiIEV4ZWN1dGlvbiJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBUb29sc1siQHRvb2wgRnVuY3Rpb25zIl0KICAgICAgICBURVNUWyJUZXN0IFJ1bm5lcnM8YnIvPnJ1bl9weXRlc3RfdGVzdDxici8%2BcnVuX2FsbF90ZXN0c19wYXJhbGxlbDxici8%2BcnVuX2Zhc3RfdGVzdHMiXQogICAgICAgIEFXU1siQVdTIFJlYWQtT25seTxici8%2BQ2xvdWRGb3JtYXRpb24gb3V0cHV0czxici8%2BQ2xvdWRXYXRjaCBMb2dzPGJyLz5XQUYgV2ViIEFDTHMiXQogICAgICAgIENPREVbIkNvZGUgVG9vbHM8YnIvPnJlYWRfdGVzdF9maWxlPGJyLz5maXhfdGVzdF9jb2RlIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIEluZnJhWyJBV1MgUmVzb3VyY2VzIChSZWFkLU9ubHkpIl0KICAgICAgICBDRlsiQ2xvdWRGb3JtYXRpb24iXQogICAgICAgIENXWyJDbG91ZFdhdGNoIExvZ3MiXQogICAgICAgIFdBRlsiV0FGIHYyIl0KICAgIGVuZAoKICAgIENMSSAtLT4gTE9PUAogICAgTE9PUCAtLT4gVEVTVAogICAgTE9PUCAtLT4gQVdTCiAgICBMT09QIC0tPiBDT0RFCiAgICBBV1MgLS0%2BIENGCiAgICBBV1MgLS0%2BIENXCiAgICBBV1MgLS0%2BIFdBRgoKICAgIHN0eWxlIEFnZW50IGZpbGw6I2UzZjJmZAogICAgc3R5bGUgVG9vbHMgZmlsbDojZmZmM2UwCiAgICBzdHlsZSBJbmZyYSBmaWxsOiNlOGY1ZTk%3D" alt="Mermaid Diagram" width="1212" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interaction looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔒 Penetration Testing Agent Ready!

You: Run all fast tests
Agent: I'll run the fast test suite in parallel, skipping slow scans...
       ✅ All tests passed in 8 seconds!

You: The TLS test is failing, can you debug it?
Agent: Let me run the TLS test and check the logs...
       [runs test, queries CloudFormation for endpoints, checks CloudWatch]
       The certificate for api.ugcbba.click expired. Here's what I found...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Strict Guardrails
&lt;/h3&gt;

&lt;p&gt;The agent enforces strict security boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can only run tests from a hardcoded allowlist — no arbitrary command execution&lt;/li&gt;
&lt;li&gt;AWS access is read-only — cannot create, modify, or delete any resources&lt;/li&gt;
&lt;li&gt;File access is sandboxed to the &lt;code&gt;testing/&lt;/code&gt; directory — path traversal is blocked&lt;/li&gt;
&lt;li&gt;Refuses tasks unrelated to penetration testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Test Suite
&lt;/h3&gt;

&lt;p&gt;The agent wraps a comprehensive pytest-based security test suite running inside a Kali Linux container:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Tests&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fast (&amp;lt; 10s each)&lt;/td&gt;
&lt;td&gt;Health, security headers, TLS, CORS, cookies, error disclosure, HTTP methods, origin access, SQLi, XSS&lt;/td&gt;
&lt;td&gt;~10s total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium (15-30s)&lt;/td&gt;
&lt;td&gt;Rate limiting, CSRF end-to-end, IDOR protection&lt;/td&gt;
&lt;td&gt;~1 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow (30s-10min)&lt;/td&gt;
&lt;td&gt;nmap port scan, Nikto web scan, sqlmap automated injection&lt;/td&gt;
&lt;td&gt;~15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent is smart about which tests to run — it prioritizes recently failed tests, skips slow scans unless explicitly requested, and caches successful results for 5 minutes to avoid redundant runs. Parallel execution via pytest-xdist makes the full fast suite complete in about 8 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Penetration Testing Framework
&lt;/h2&gt;

&lt;p&gt;Beyond the AI agent, the project includes a standalone automated penetration testing framework using Dockerized Kali Linux. This is the foundation that the agent builds on, but it can also be used independently in CI/CD pipelines.&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;# Build the Kali container&lt;/span&gt;
docker build &lt;span class="nt"&gt;-f&lt;/span&gt; testing/Dockerfile.kali &lt;span class="nt"&gt;-t&lt;/span&gt; pentest:latest testing/

&lt;span class="c"&gt;# Quick smoke test&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;TARGET_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://your-alb.amazonaws.com pentest smoke

&lt;span class="c"&gt;# Full test suite&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;TARGET_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://your-alb.amazonaws.com pentest all

&lt;span class="c"&gt;# Generate report&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:/tests &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;TARGET_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TARGET_URL&lt;/span&gt; pentest report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework integrates with GitHub Actions for scheduled weekly scans and on-demand testing, with results uploaded as workflow artifacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat Detection and Incident Response
&lt;/h2&gt;

&lt;p&gt;The platform implements automated threat detection and response:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgICBzdWJncmFwaCBEZXRlY3Rpb25bIlRocmVhdCBEZXRlY3Rpb24iXQogICAgICAgIEdEWyJHdWFyZER1dHk8YnIvPk1hbGljaW91cyBJUHMsIHVudXN1YWwgQVBJIGNhbGxzIl0KICAgICAgICBTSFsiU2VjdXJpdHkgSHViPGJyLz5BZ2dyZWdhdGVkIGZpbmRpbmdzLCBjb21wbGlhbmNlIl0KICAgICAgICBDVFsiQ2xvdWRUcmFpbDxici8%2BQVBJIGF1ZGl0IGxvZyJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBSZXNwb25zZVsiQXV0b21hdGVkIFJlc3BvbnNlIl0KICAgICAgICBFQlsiRXZlbnRCcmlkZ2UgUnVsZSJdCiAgICAgICAgTEFNQkRBWyJSZXNwb25zZSBMYW1iZGEiXQogICAgICAgIFdBRlsiV0FGIElQIFNldCBVcGRhdGUiXQogICAgZW5kCgogICAgc3ViZ3JhcGggQWxlcnRbIkFsZXJ0aW5nIl0KICAgICAgICBTTEFDS1siU2xhY2siXQogICAgZW5kCgogICAgR0QgLS0%2BfCJGaW5kaW5nInwgRUIKICAgIEVCIC0tPnwiU2V2ZXJpdHkgPj0gNCJ8IExBTUJEQQogICAgTEFNQkRBIC0tPnwiQmxvY2sgSVAifCBXQUYKICAgIExBTUJEQSAtLT58Ik5vdGlmeSJ8IEFsZXJ0CiAgICBTSCAtLi0%2BIEdECiAgICBDVCAtLi0%2BIFNI" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRCCiAgICBzdWJncmFwaCBEZXRlY3Rpb25bIlRocmVhdCBEZXRlY3Rpb24iXQogICAgICAgIEdEWyJHdWFyZER1dHk8YnIvPk1hbGljaW91cyBJUHMsIHVudXN1YWwgQVBJIGNhbGxzIl0KICAgICAgICBTSFsiU2VjdXJpdHkgSHViPGJyLz5BZ2dyZWdhdGVkIGZpbmRpbmdzLCBjb21wbGlhbmNlIl0KICAgICAgICBDVFsiQ2xvdWRUcmFpbDxici8%2BQVBJIGF1ZGl0IGxvZyJdCiAgICBlbmQKCiAgICBzdWJncmFwaCBSZXNwb25zZVsiQXV0b21hdGVkIFJlc3BvbnNlIl0KICAgICAgICBFQlsiRXZlbnRCcmlkZ2UgUnVsZSJdCiAgICAgICAgTEFNQkRBWyJSZXNwb25zZSBMYW1iZGEiXQogICAgICAgIFdBRlsiV0FGIElQIFNldCBVcGRhdGUiXQogICAgZW5kCgogICAgc3ViZ3JhcGggQWxlcnRbIkFsZXJ0aW5nIl0KICAgICAgICBTTEFDS1siU2xhY2siXQogICAgZW5kCgogICAgR0QgLS0%2BfCJGaW5kaW5nInwgRUIKICAgIEVCIC0tPnwiU2V2ZXJpdHkgPj0gNCJ8IExBTUJEQQogICAgTEFNQkRBIC0tPnwiQmxvY2sgSVAifCBXQUYKICAgIExBTUJEQSAtLT58Ik5vdGlmeSJ8IEFsZXJ0CiAgICBTSCAtLi0%2BIEdECiAgICBDVCAtLi0%2BIFNI" alt="Mermaid Diagram" width="528" height="952"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GuardDuty monitors for threats. When a finding with severity &amp;gt;= 4 is detected, an EventBridge rule triggers a Lambda function that automatically blocks the offending IP in the WAF IP set and sends a detailed alert to Slack. Security Hub aggregates findings from GuardDuty, CloudTrail, and AWS Config rules for a unified compliance view.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Observability
&lt;/h3&gt;

&lt;p&gt;CloudWatch dashboards and alarms provide real-time visibility:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth failures (5min)&lt;/td&gt;
&lt;td&gt;&amp;gt;= 20&lt;/td&gt;
&lt;td&gt;Alarm + SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSRF failures (5min)&lt;/td&gt;
&lt;td&gt;&amp;gt;= 20&lt;/td&gt;
&lt;td&gt;Alarm + SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access denied (5min)&lt;/td&gt;
&lt;td&gt;&amp;gt;= 30&lt;/td&gt;
&lt;td&gt;Alarm + SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limit hits (5min)&lt;/td&gt;
&lt;td&gt;&amp;gt;= 100&lt;/td&gt;
&lt;td&gt;Alarm + SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error rate (5min)&lt;/td&gt;
&lt;td&gt;&amp;gt;= 10&lt;/td&gt;
&lt;td&gt;Alarm + SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Critical errors (1min)&lt;/td&gt;
&lt;td&gt;&amp;gt;= 1&lt;/td&gt;
&lt;td&gt;Alarm + SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API latency p95&lt;/td&gt;
&lt;td&gt;&amp;gt; 1000ms&lt;/td&gt;
&lt;td&gt;Alarm + SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Developer Experience
&lt;/h2&gt;

&lt;p&gt;A good DevSecOps project is not just about security and architecture — it is also about making the developer's life easier. The project uses &lt;a href="https://www.jetpack.io/devbox/" rel="noopener noreferrer"&gt;Devbox&lt;/a&gt; for isolated development environments and &lt;a href="https://github.com/casey/just" rel="noopener noreferrer"&gt;Just&lt;/a&gt; as a task runner.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;justfile&lt;/code&gt; provides a comprehensive set of commands:&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;# Development&lt;/span&gt;
just dev          &lt;span class="c"&gt;# Start local services and tail logs&lt;/span&gt;
just up           &lt;span class="c"&gt;# Start PostgreSQL + LocalStack&lt;/span&gt;
just &lt;span class="nb"&gt;test&lt;/span&gt;         &lt;span class="c"&gt;# Run all tests&lt;/span&gt;
just lint-local   &lt;span class="c"&gt;# Lint all services locally&lt;/span&gt;

&lt;span class="c"&gt;# Security&lt;/span&gt;
just security-scan    &lt;span class="c"&gt;# SAST + SCA + Secrets scan&lt;/span&gt;
just trivy-scan       &lt;span class="c"&gt;# Container vulnerability scan&lt;/span&gt;
just sbom-scan        &lt;span class="c"&gt;# Generate and scan SBOMs&lt;/span&gt;
just iac-scan         &lt;span class="c"&gt;# Infrastructure scan with Checkov&lt;/span&gt;
just pentest-full     &lt;span class="c"&gt;# Run the full penetration test suite&lt;/span&gt;

&lt;span class="c"&gt;# AWS Resource Management (Cost Saving)&lt;/span&gt;
just aws-up       &lt;span class="c"&gt;# Scale up ECS services + start RDS&lt;/span&gt;
just aws-down     &lt;span class="c"&gt;# Scale down to zero (save money)&lt;/span&gt;
just aws-status   &lt;span class="c"&gt;# Check resource status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;aws-up&lt;/code&gt; and &lt;code&gt;aws-down&lt;/code&gt; commands are particularly useful — they allow scaling the entire infrastructure to zero when not in use, saving significant costs during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;

&lt;p&gt;The codebase is organized for clarity and separation of concerns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── api/                    # API service (FastAPI + hexagonal)
│   ├── src/
│   │   ├── domain/         # Business entities, value objects
│   │   ├── application/    # Use cases, ports, DTOs
│   │   ├── infrastructure/ # Adapters (DB, Kinesis, Secrets)
│   │   └── presentation/   # HTTP routes, middleware
│   └── tests/
│
├── worker/                 # Worker service (Kinesis consumer)
│   ├── src/
│   │   ├── domain/         # Ports for channels, publishers
│   │   ├── application/    # Delivery service
│   │   ├── infrastructure/ # Publisher adapters (Direct, AI Agent)
│   │   └── channels/       # Channel gateways (FB, IG, LI, Email, SMS)
│   └── tests/
│
├── scheduler/              # Scheduler service (cron + ECS)
├── api-lambda/             # API Lambda handler (serverless)
├── worker-lambda/          # Worker Lambda handler (serverless)
├── scheduler-lambda/       # Scheduler Lambda handler (serverless)
├── web/                    # Frontend (React + Vite + TypeScript)
│
├── testing/                # Penetration testing framework
│   ├── pentest_agent.py    # AI security testing agent (Strands SDK)
│   ├── test_pentest.py     # Security test cases (pytest)
│   ├── Dockerfile.kali     # Kali Linux container with tools
│   └── justfile            # Pentest task runner
│
├── infra/                  # CDK infrastructure (containers)
│   └── stacks/             # Network, Security, Auth, Data, Compute, Edge, Monitoring
│
├── infra-fs/               # CDK infrastructure (serverless)
│   └── stacks/             # Data, Auth, API, Worker, Scheduler, Security, Frontend
│
├── docs/                   # Comprehensive documentation
├── .github/workflows/      # CI/CD (deploy + security scan)
├── devbox.json             # Development environment
├── docker-compose.yml      # Local services
└── justfile                # Task runner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building this project reinforced several convictions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hexagonal architecture is not academic overhead.&lt;/strong&gt; It is the single decision that enabled dual-mode deployment, clean testing, and a codebase that remains navigable as it grows. The upfront investment in defining ports and adapters pays for itself many times over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security scanning must be layered.&lt;/strong&gt; No single tool catches everything. The combination of SAST, SCA, secrets scanning, IaC scanning, and DAST provides genuine defense in depth. The key is making these scans fast and automatic — if they slow down the developer, they will be bypassed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correlation IDs are non-negotiable.&lt;/strong&gt; The ability to trace a single request from CloudFront through the API, into Kinesis, through the Worker, and into the database is invaluable for debugging, incident response, and security forensics. Implement them from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost management is a feature.&lt;/strong&gt; The &lt;code&gt;aws-up&lt;/code&gt; / &lt;code&gt;aws-down&lt;/code&gt; pattern for scaling resources to zero when not in use is simple but effective. For a project like this, it is the difference between a $200/month bill and a $5/month bill during development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agents need guardrails.&lt;/strong&gt; The Strands Agents integration is powerful, but without content filtering, prompt injection detection, and output validation, it is a liability. The agent can only execute pre-approved operations through well-defined tool interfaces — never raw API calls. The same principle applies to the Security Testing Agent: it operates within a strict allowlist of tests and read-only AWS access, proving that AI-powered automation and security boundaries can coexist.&lt;/p&gt;

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

&lt;p&gt;This project is a living laboratory. It is not finished, and it probably never will be — because the landscape of threats, tools, and best practices is always evolving. But it serves its purpose: a concrete, open-source reference for how to build modern cloud-native applications with security woven into every layer.&lt;/p&gt;

&lt;p&gt;If you are starting a new project and wondering where to begin with DevSecOps, my advice is simple: start with the architecture. Get hexagonal architecture right, define your ports and adapters, and the rest — security, testing, deployment flexibility — becomes dramatically easier.&lt;/p&gt;

&lt;p&gt;The code is open source. Fork it, break it, improve it. That is what it is there for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc" rel="noopener noreferrer"&gt;https://github.com/Walsen/devsecops-poc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hexagonal Architecture Reference&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/hexagonal-architecture.md" rel="noopener noreferrer"&gt;docs/hexagonal-architecture.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Documentation&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/security.md" rel="noopener noreferrer"&gt;docs/security.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden Thread Tracing&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/golden-thread-tracing.md" rel="noopener noreferrer"&gt;docs/golden-thread-tracing.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-Mode Deployment Guide&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/dual-mode-deployment.md" rel="noopener noreferrer"&gt;docs/dual-mode-deployment.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Penetration Testing Guide&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/penetration-testing.md" rel="noopener noreferrer"&gt;docs/penetration-testing.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Testing Agent&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/security-testing-agent.md" rel="noopener noreferrer"&gt;docs/security-testing-agent.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Penetration Testing&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/automated-penetration-testing.md" rel="noopener noreferrer"&gt;docs/automated-penetration-testing.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing Guide&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/testing.md" rel="noopener noreferrer"&gt;docs/testing.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Agents Documentation&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/devsecops-poc/blob/main/docs/ai-agents.md" rel="noopener noreferrer"&gt;docs/ai-agents.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>aws</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>An Incredible Operations Platform - Rundeck</title>
      <dc:creator>Sergio D. Rodríguez Inclán</dc:creator>
      <pubDate>Sun, 01 Feb 2026 19:56:51 +0000</pubDate>
      <link>https://dev.to/w4ls3n/an-incredible-operations-platform-rundeck-1kan</link>
      <guid>https://dev.to/w4ls3n/an-incredible-operations-platform-rundeck-1kan</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;There's a moment every operations engineer knows well: it's 2 AM, something's broken, and you're frantically SSH-ing into servers trying to remember the exact sequence of commands to fix it. You've done this before, but was it &lt;code&gt;systemctl restart&lt;/code&gt; first or the config update? And which servers exactly?&lt;/p&gt;

&lt;p&gt;This is the problem Rundeck solves. It's an open-source runbook automation platform that lets you define, schedule, and execute operational procedures across your entire infrastructure—with proper access control, audit trails, and the peace of mind that comes from knowing the procedure will run exactly the same way every time.&lt;/p&gt;

&lt;p&gt;I've been using Rundeck for years, and recently I decided to contribute back to the ecosystem by creating three tools that solve specific pain points I encountered: a production-ready Docker image, a plugin for copying files between nodes, and a GitHub Action for seamless CI/CD integration. Let me walk you through all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes Rundeck Special
&lt;/h2&gt;

&lt;p&gt;Rundeck occupies a unique space in the DevOps toolchain. It's not a configuration management tool like Ansible or Puppet—though it integrates beautifully with them. It's not a CI/CD platform like Jenkins—though it can trigger and be triggered by pipelines. Rundeck is specifically designed for &lt;strong&gt;operational workflows&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core Capabilities
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Job Definitions&lt;/strong&gt;: Create multi-step workflows with conditionals, error handling, and node targeting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node Management&lt;/strong&gt;: Maintain a centralized inventory of your infrastructure with custom attributes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Control&lt;/strong&gt;: Fine-grained RBAC so developers can restart their services without full SSH access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Storage&lt;/strong&gt;: Secure credential management for SSH keys, passwords, and API tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution History&lt;/strong&gt;: Complete audit trail of who ran what, when, and with what results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduling&lt;/strong&gt;: Cron-like scheduling for recurring maintenance tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST API&lt;/strong&gt;: Full API for integration with CI/CD pipelines, monitoring systems, and AI agents&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where It Shines
&lt;/h3&gt;

&lt;p&gt;In my experience, Rundeck excels at:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Incident Response&lt;/strong&gt;: Pre-built runbooks that on-call engineers can execute confidently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Service Operations&lt;/strong&gt;: Let developers restart services or clear caches without ops tickets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinated Procedures&lt;/strong&gt;: Multi-node operations that need to happen in a specific order&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance Tasks&lt;/strong&gt;: Scheduled security scans, backup verifications, audit reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment Orchestration&lt;/strong&gt;: Coordinate deployments across environments with approval gates&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  REST API: Automation Beyond the UI
&lt;/h2&gt;

&lt;p&gt;One of Rundeck's most powerful features is its comprehensive REST API. Every action you can perform in the web interface is available programmatically, making Rundeck a perfect backend for automated operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Capabilities
&lt;/h3&gt;

&lt;p&gt;The API supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Job execution&lt;/strong&gt;: Trigger jobs with custom parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution monitoring&lt;/strong&gt;: Check status, stream logs, abort running jobs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job management&lt;/strong&gt;: Create, update, delete, import/export job definitions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node inventory&lt;/strong&gt;: Query and manage node sources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key storage&lt;/strong&gt;: Programmatic credential management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System info&lt;/strong&gt;: Health checks, metrics, cluster status&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Rundeck supports multiple authentication methods for API access:&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;# Using API Token (recommended)&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Rundeck-Auth-Token: YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://rundeck.example.com/api/41/projects

&lt;span class="c"&gt;# Using session cookie&lt;/span&gt;
curl &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"j_username=admin&amp;amp;j_password=admin"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://rundeck.example.com/j_security_check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Triggering Jobs Programmatically
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run a job by ID&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Rundeck-Auth-Token: YOUR_TOKEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"options": {"environment": "production", "version": "1.2.3"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://rundeck.example.com/api/41/job/JOB_ID/run

&lt;span class="c"&gt;# Response includes execution ID for monitoring&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: 12345,
  &lt;span class="s2"&gt;"href"&lt;/span&gt;: &lt;span class="s2"&gt;"https://rundeck.example.com/api/41/execution/12345"&lt;/span&gt;,
  &lt;span class="s2"&gt;"status"&lt;/span&gt;: &lt;span class="s2"&gt;"running"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integration Scenarios
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;CI/CD Pipelines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trigger deployment jobs from Jenkins, GitHub Actions, or GitLab CI. I created a dedicated GitHub Action to make this integration even easier: &lt;a href="https://github.com/Walsen/rundeck-github-action" rel="noopener noreferrer"&gt;rundeck-github-action&lt;/a&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="c1"&gt;# Using the Rundeck GitHub Action&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;Deploy via Rundeck&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Walsen/rundeck-github-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;rundeck_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://rundeck.example.com&lt;/span&gt;
    &lt;span class="na"&gt;rundeck_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RUNDECK_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run_job&lt;/span&gt;
    &lt;span class="na"&gt;job_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.DEPLOY_JOB_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;job_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{"version":&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;github.sha&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;"environment":&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"production"}'&lt;/span&gt;
    &lt;span class="na"&gt;wait_for_completion&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;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action supports multiple operations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&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;&lt;code&gt;run_job&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Execute a Rundeck job with options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_job_info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get job details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List jobs in a project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_execution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get execution details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_executions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List executions for a job or project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;abort_execution&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Abort a running execution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can also wait for job completion and get the execution status:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run deployment and wait&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Walsen/rundeck-github-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;rundeck_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://rundeck.example.com&lt;/span&gt;
    &lt;span class="na"&gt;rundeck_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RUNDECK_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run_job&lt;/span&gt;
    &lt;span class="na"&gt;job_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.DEPLOY_JOB_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;wait_for_completion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;Check result&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;echo "Status: ${{ steps.deploy.outputs.execution_status }}"&lt;/span&gt;
    &lt;span class="s"&gt;echo "URL: ${{ steps.deploy.outputs.execution_url }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Monitoring Integration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Have your monitoring system trigger remediation jobs automatically:&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="c1"&gt;# PagerDuty webhook handler example
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alert&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;alert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;high_memory&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RUNDECK_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/41/job/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CLEAR_CACHE_JOB&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/run&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Rundeck-Auth-Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RUNDECK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;json&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;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hostname&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AI Agents and LLM Integration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is where things get interesting. Rundeck's API makes it an ideal execution backend for AI-powered operations. Using the &lt;a href="https://github.com/strands-agents/sdk-python" rel="noopener noreferrer"&gt;Strands Agents SDK&lt;/a&gt; from AWS, you can build intelligent agents that leverage Rundeck as their operational backbone.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICB1c2VyWyJVc2VyL0FsZXJ0Il0gLS0%2BIGFnZW50WyJTdHJhbmRzIEFnZW50Il0KICAgIGFnZW50IC0tPiBhbmFseXplWyJBbmFseXplIFNpdHVhdGlvbiJdCiAgICBhbmFseXplIC0tPiBkZWNpZGVbIlNlbGVjdCBSdW5ib29rIl0KICAgIGRlY2lkZSAtLT4gdG9vbFsiUnVuZGVjayBUb29sIl0KICAgIHRvb2wgLS0%2BIGFwaVsiUnVuZGVjayBBUEkiXQogICAgYXBpIC0tPiBqb2JbIkV4ZWN1dGUgSm9iIl0KICAgIGpvYiAtLT4gcmVzdWx0WyJSZXR1cm4gUmVzdWx0cyJdCiAgICByZXN1bHQgLS0%2BIGFnZW50" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICB1c2VyWyJVc2VyL0FsZXJ0Il0gLS0%2BIGFnZW50WyJTdHJhbmRzIEFnZW50Il0KICAgIGFnZW50IC0tPiBhbmFseXplWyJBbmFseXplIFNpdHVhdGlvbiJdCiAgICBhbmFseXplIC0tPiBkZWNpZGVbIlNlbGVjdCBSdW5ib29rIl0KICAgIGRlY2lkZSAtLT4gdG9vbFsiUnVuZGVjayBUb29sIl0KICAgIHRvb2wgLS0%2BIGFwaVsiUnVuZGVjayBBUEkiXQogICAgYXBpIC0tPiBqb2JbIkV4ZWN1dGUgSm9iIl0KICAgIGpvYiAtLT4gcmVzdWx0WyJSZXR1cm4gUmVzdWx0cyJdCiAgICByZXN1bHQgLS0%2BIGFnZW50" alt="Mermaid Diagram" width="1637" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;strands-agents strands-agents-tools requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a custom Rundeck tool for your agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;RUNDECK_URL&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://rundeck.example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RUNDECK_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-api-token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_rundeck_jobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;List available runbooks in a Rundeck project.

    Args:
        project: The Rundeck project name

    Returns:
        List of available jobs with their IDs and descriptions
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RUNDECK_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/41/project/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/jobs&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Rundeck-Auth-Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RUNDECK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_rundeck_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job_id&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;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Execute a Rundeck job/runbook.

    Args:
        job_id: The UUID of the job to execute
        options: Optional parameters for the job

    Returns:
        Execution details including status and execution ID
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RUNDECK_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/41/job/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/run&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Rundeck-Auth-Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RUNDECK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&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;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}}&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_execution_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Check the status of a Rundeck job execution.

    Args:
        execution_id: The execution ID to check

    Returns:
        Execution status and details
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RUNDECK_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/41/execution/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;execution_id&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="n"&gt;headers&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;X-Rundeck-Auth-Token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RUNDECK_TOKEN&lt;/span&gt;&lt;span class="p"&gt;}&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Create the operations agent
&lt;/span&gt;&lt;span class="n"&gt;ops_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;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are an operations assistant with access to Rundeck runbooks.
    When asked to perform operational tasks:
    1. List available jobs to find the appropriate runbook
    2. Execute the job with the correct parameters
    3. Monitor the execution and report the results
    Always confirm before executing destructive operations.&lt;/span&gt;&lt;span class="sh"&gt;"""&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;list_rundeck_jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_rundeck_job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_execution_status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Example interaction
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ops_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;The app servers are running low on disk space. &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;    Can you clear the log files on the production cluster?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern keeps humans in control—the AI can only execute pre-approved runbooks with proper access controls and audit trails. It's autonomous operations with guardrails. The agent can reason about which runbook to use, execute it, and report back the results, all while respecting Rundeck's RBAC policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low-Code Integration with n8n&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you prefer a visual approach to automation, &lt;a href="https://n8n.io" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; offers native Rundeck integration. You can build workflows that connect GitHub events to Rundeck job executions without writing code.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBnaXRodWJbIkdpdEh1YiBXZWJob29rIl0gLS0%2BIG44blsibjhuIFdvcmtmbG93Il0KICAgIG44biAtLT4gcnVuZGVja1siUnVuZGVjayBOb2RlIl0KICAgIHJ1bmRlY2sgLS0%2BIGpvYlsiRXhlY3V0ZSBKb2IiXQogICAgam9iIC0tPiBub3RpZnlbIlNsYWNrL0VtYWlsIl0%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBnaXRodWJbIkdpdEh1YiBXZWJob29rIl0gLS0%2BIG44blsibjhuIFdvcmtmbG93Il0KICAgIG44biAtLT4gcnVuZGVja1siUnVuZGVjayBOb2RlIl0KICAgIHJ1bmRlY2sgLS0%2BIGpvYlsiRXhlY3V0ZSBKb2IiXQogICAgam9iIC0tPiBub3RpZnlbIlNsYWNrL0VtYWlsIl0%3D" alt="Mermaid Diagram" width="1012" height="70"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://n8n.io/integrations/github/and/rundeck/" rel="noopener noreferrer"&gt;n8n Rundeck node&lt;/a&gt; supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt;: Trigger any Rundeck job with parameters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get Metadata&lt;/strong&gt;: Retrieve job definitions and status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combined with n8n's 400+ integrations, you can create powerful automation chains—for example, triggering a deployment job when a GitHub release is published, then notifying your team on Slack when it completes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licensing: Open Source vs Enterprise
&lt;/h2&gt;

&lt;p&gt;Rundeck follows an open-core model. The community edition is fully open source under the Apache 2.0 license, while PagerDuty (who acquired Rundeck) offers commercial versions with additional features.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rundeck Community (Open Source)
&lt;/h3&gt;

&lt;p&gt;Free forever, includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workflow execution and job definitions&lt;/li&gt;
&lt;li&gt;Node management and key storage&lt;/li&gt;
&lt;li&gt;Access control (ACL-based)&lt;/li&gt;
&lt;li&gt;Scheduling and job activity logs&lt;/li&gt;
&lt;li&gt;Community plugins&lt;/li&gt;
&lt;li&gt;REST API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what my Docker image uses—perfect for small to medium teams and learning environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  PagerDuty Runbook Automation (Commercial)
&lt;/h3&gt;

&lt;p&gt;The enterprise offerings add:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&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;High Availability&lt;/td&gt;
&lt;td&gt;Clustered deployments with auto-takeover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSO Integration&lt;/td&gt;
&lt;td&gt;SAML, LDAP, OAuth support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise Plugins&lt;/td&gt;
&lt;td&gt;ServiceNow, PagerDuty, Datadog, VMware integrations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advanced Scheduling&lt;/td&gt;
&lt;td&gt;Blackout calendars, schedule forecasting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed Job Resume&lt;/td&gt;
&lt;td&gt;Resume from the failed step instead of restarting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise Support&lt;/td&gt;
&lt;td&gt;SLA-backed support and account management&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PagerDuty offers two commercial options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runbook Automation Self-Hosted&lt;/strong&gt;: You manage the infrastructure, they provide the enterprise features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runbook Automation (Cloud)&lt;/strong&gt;: Fully managed SaaS with 99.9% SLA&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Which Should You Choose?
&lt;/h3&gt;

&lt;p&gt;For most use cases, start with the open source version. It's production-ready and covers the core functionality. Consider upgrading when you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High availability for mission-critical operations&lt;/li&gt;
&lt;li&gt;SSO integration with your identity provider&lt;/li&gt;
&lt;li&gt;Enterprise integrations (ServiceNow tickets, PagerDuty incidents)&lt;/li&gt;
&lt;li&gt;Professional support with SLAs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The open source version isn't a "lite" version—it's a complete operations platform that many organizations run successfully in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap: Node-to-Node File Transfers
&lt;/h2&gt;

&lt;p&gt;While building a configuration distribution workflow, I hit a limitation. Rundeck's built-in file copier moves files &lt;strong&gt;from the Rundeck server to target nodes&lt;/strong&gt;. But I needed to copy files &lt;strong&gt;from one node to multiple other nodes&lt;/strong&gt;—specifically, distributing generated configs from a central server to application nodes.&lt;/p&gt;

&lt;p&gt;The workaround was clunky: download to Rundeck, then upload to each destination. For large files or many destinations, this becomes a bottleneck.&lt;/p&gt;

&lt;p&gt;So I built a plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rundeck Node-to-Node Plugin
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/Walsen/rundeck-node-to-node" rel="noopener noreferrer"&gt;rundeck-node-to-node&lt;/a&gt; plugin adds a workflow step that copies files and directories between nodes using SSH/SFTP, fully integrated with Rundeck's node definitions and key storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two Transfer Modes&lt;/strong&gt;: Route through Rundeck (reliable) or direct node-to-node (fast)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel Transfers&lt;/strong&gt;: Copy to multiple destinations simultaneously&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directory Support&lt;/strong&gt;: Recursive copy with preserved permissions and timestamps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Storage Integration&lt;/strong&gt;: Uses Rundeck's secure credential management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error Handling&lt;/strong&gt;: Option to continue on partial failures&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Transfer Modes Explained
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Via-Rundeck (Default)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Files download to Rundeck once, then upload to all destinations. Works in any network topology.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzb3VyY2VbIlNvdXJjZSBOb2RlIl0gLS0%2BIHJ1bmRlY2tbIlJ1bmRlY2sgU2VydmVyIl0KICAgIHJ1bmRlY2sgLS0%2BIGRlc3QxWyJEZXN0aW5hdGlvbiAxIl0KICAgIHJ1bmRlY2sgLS0%2BIGRlc3QyWyJEZXN0aW5hdGlvbiAyIl0KICAgIHJ1bmRlY2sgLS0%2BIGRlc3QzWyJEZXN0aW5hdGlvbiAzIl0%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzb3VyY2VbIlNvdXJjZSBOb2RlIl0gLS0%2BIHJ1bmRlY2tbIlJ1bmRlY2sgU2VydmVyIl0KICAgIHJ1bmRlY2sgLS0%2BIGRlc3QxWyJEZXN0aW5hdGlvbiAxIl0KICAgIHJ1bmRlY2sgLS0%2BIGRlc3QyWyJEZXN0aW5hdGlvbiAyIl0KICAgIHJ1bmRlY2sgLS0%2BIGRlc3QzWyJEZXN0aW5hdGlvbiAzIl0%3D" alt="Mermaid Diagram" width="598" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct Mode&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Source pushes directly to destinations via SCP. Faster, but requires source-to-destination SSH access.&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzb3VyY2VbIlNvdXJjZSBOb2RlIl0gLS0%2BIGRlc3QxWyJEZXN0aW5hdGlvbiAxIl0KICAgIHNvdXJjZSAtLT4gZGVzdDJbIkRlc3RpbmF0aW9uIDIiXQogICAgc291cmNlIC0tPiBkZXN0M1siRGVzdGluYXRpb24gMyJd" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzb3VyY2VbIlNvdXJjZSBOb2RlIl0gLS0%2BIGRlc3QxWyJEZXN0aW5hdGlvbiAxIl0KICAgIHNvdXJjZSAtLT4gZGVzdDJbIkRlc3RpbmF0aW9uIDIiXQogICAgc291cmNlIC0tPiBkZXN0M1siRGVzdGluYXRpb24gMyJd" alt="Mermaid Diagram" width="373" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Node Configuration
&lt;/h3&gt;

&lt;p&gt;The plugin uses Rundeck's standard node attributes:&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;config-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.1.10&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;ssh-key-storage-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keys/project/deploy-key&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;

&lt;span class="na"&gt;app-server-01&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.1.20&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;ssh-key-storage-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keys/project/deploy-key&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app,production&lt;/span&gt;

&lt;span class="na"&gt;app-server-02&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.1.21&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;ssh-key-storage-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;keys/project/deploy-key&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app,production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Plugin Options
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Default&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;Source Node&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Node name where files originate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source Path&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;File or directory path on source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination Nodes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Comma-separated destination node names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Destination Path&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Target path on destinations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursive Copy&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Copy directories recursively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preserve Attributes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Keep timestamps and permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer Mode&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;via-rundeck&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;via-rundeck&lt;/code&gt; or &lt;code&gt;direct&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parallel Transfers&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Transfer to multiple nodes in parallel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continue on Error&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Don't fail if some destinations fail&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Download the JAR from &lt;a href="https://github.com/Walsen/rundeck-node-to-node/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Copy to Rundeck's &lt;code&gt;libext&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;Restart Rundeck (or wait for auto-reload)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The plugin appears as a new workflow step type: "Node to Node File Copy".&lt;/p&gt;

&lt;h2&gt;
  
  
  Production-Ready Docker Image
&lt;/h2&gt;

&lt;p&gt;Installing Rundeck traditionally means dealing with Java, databases, reverse proxies, and process management. To streamline this, I created a Docker image that bundles everything for production use.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/Walsen/rundeck-image" rel="noopener noreferrer"&gt;rundeck-image&lt;/a&gt; provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rundeck 5.18.0&lt;/strong&gt; (configurable version)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nginx&lt;/strong&gt; reverse proxy for proper HTTP handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supervisor&lt;/strong&gt; for process management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; support for production databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node-to-Node plugin&lt;/strong&gt; pre-installed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBjb250YWluZXJbIkRvY2tlciBDb250YWluZXIiXQogICAgICAgIHN1YmdyYXBoIHN1cGVydmlzb3JbIlN1cGVydmlzb3IiXQogICAgICAgICAgICBuZ2lueFsiTmdpbng8YnIvPjo4MDgwIl0KICAgICAgICAgICAgcnVuZGVja1siUnVuZGVjazxici8%2BOjQ0NDAiXQogICAgICAgIGVuZAogICAgZW5kCiAgICAKICAgIGNsaWVudFsiQ2xpZW50Il0gLS0%2BIG5naW54CiAgICBuZ2lueCAtLT4gcnVuZGVjaw%3D%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBzdWJncmFwaCBjb250YWluZXJbIkRvY2tlciBDb250YWluZXIiXQogICAgICAgIHN1YmdyYXBoIHN1cGVydmlzb3JbIlN1cGVydmlzb3IiXQogICAgICAgICAgICBuZ2lueFsiTmdpbng8YnIvPjo4MDgwIl0KICAgICAgICAgICAgcnVuZGVja1siUnVuZGVjazxici8%2BOjQ0NDAiXQogICAgICAgIGVuZAogICAgZW5kCiAgICAKICAgIGNsaWVudFsiQ2xpZW50Il0gLS0%2BIG5naW54CiAgICBuZ2lueCAtLT4gcnVuZGVjaw%3D%3D" alt="Mermaid Diagram" width="541" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RUNDECK_GRAILS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/walsen/rundeck-image:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Access Rundeck at &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Setup with Docker Compose
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rundeck&lt;/span&gt;&lt;span class="pi"&gt;:&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;ghcr.io/walsen/rundeck-image:latest&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="na"&gt;RUNDECK_GRAILS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://rundeck.example.com&lt;/span&gt;
      &lt;span class="na"&gt;RUNDECK_DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdbc:postgresql://db:5432/rundeck&lt;/span&gt;
      &lt;span class="na"&gt;RUNDECK_DATABASE_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rundeck&lt;/span&gt;
      &lt;span class="na"&gt;RUNDECK_DATABASE_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config/realm.properties:/etc/rundeck/realm.properties&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rundeck-data:/var/rundeck&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;db&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&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;postgres:15-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rundeck&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rundeck&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${DB_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres-data:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rundeck-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Default&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;&lt;code&gt;RUNDECK_GRAILS_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http://localhost:8080&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;External URL (must match your setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUNDECK_DATABASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;PostgreSQL JDBC connection string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUNDECK_DATABASE_USERNAME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Database user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RUNDECK_DATABASE_PASSWORD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;Database password&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  User Authentication
&lt;/h3&gt;

&lt;p&gt;Mount a &lt;code&gt;realm.properties&lt;/code&gt; file for basic authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# username:password,role1,role2
&lt;/span&gt;&lt;span class="py"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;admin,user,admin&lt;/span&gt;
&lt;span class="py"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;operator123,user&lt;/span&gt;
&lt;span class="py"&gt;readonly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s"&gt;viewer456,user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For production, integrate with LDAP or SSO—Rundeck supports both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom Port Mapping
&lt;/h3&gt;

&lt;p&gt;The image handles any external port. Just match &lt;code&gt;RUNDECK_GRAILS_URL&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Running on port 4440&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 4440:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;RUNDECK_GRAILS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4440 &lt;span class="se"&gt;\&lt;/span&gt;
  ghcr.io/walsen/rundeck-image:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical Example: Config Distribution Workflow
&lt;/h2&gt;

&lt;p&gt;Let me show how these tools work together in a real scenario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Setup:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 config server generates environment-specific configuration files&lt;/li&gt;
&lt;li&gt;5 application servers need these configs&lt;/li&gt;
&lt;li&gt;Updates should happen without manual intervention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Workflow:&lt;/strong&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBzdWJncmFwaCBzdGVwMVsiU3RlcCAxOiBHZW5lcmF0ZSBDb25maWciXQogICAgICAgIGNvbmZpZ1siY29uZmlnLXNlcnZlciJdCiAgICAgICAgc2NyaXB0WyJnZW5lcmF0ZS1jb25maWcuc2giXQogICAgICAgIGNvbmZpZyAtLT4gc2NyaXB0CiAgICBlbmQKICAgIAogICAgc3ViZ3JhcGggc3RlcDJbIlN0ZXAgMjogRGlzdHJpYnV0ZSAoTm9kZS10by1Ob2RlIFBsdWdpbikiXQogICAgICAgIHNvdXJjZVsiY29uZmlnLXNlcnZlcjxici8%2BL2V0Yy9teWFwcC9jb25maWcueW1sIl0KICAgICAgICBhcHAxWyJhcHAtMDEiXQogICAgICAgIGFwcDJbImFwcC0wMiJdCiAgICAgICAgYXBwM1siYXBwLTAzIl0KICAgICAgICBhcHA0WyJhcHAtMDQiXQogICAgICAgIGFwcDVbImFwcC0wNSJdCiAgICAgICAgc291cmNlIC0tPiBhcHAxCiAgICAgICAgc291cmNlIC0tPiBhcHAyCiAgICAgICAgc291cmNlIC0tPiBhcHAzCiAgICAgICAgc291cmNlIC0tPiBhcHA0CiAgICAgICAgc291cmNlIC0tPiBhcHA1CiAgICBlbmQKICAgIAogICAgc3ViZ3JhcGggc3RlcDNbIlN0ZXAgMzogUm9sbGluZyBSZWxvYWQiXQogICAgICAgIHIxWyJyZWxvYWQgYXBwLTAxIl0gLS0%2BIHIyWyJyZWxvYWQgYXBwLTAyIl0KICAgICAgICByMiAtLT4gcjNbInJlbG9hZCBhcHAtMDMiXQogICAgICAgIHIzIC0tPiByNFsicmVsb2FkIGFwcC0wNCJdCiAgICAgICAgcjQgLS0%2BIHI1WyJyZWxvYWQgYXBwLTA1Il0KICAgIGVuZAogICAgCiAgICBzdGVwMSAtLT4gc3RlcDIKICAgIHN0ZXAyIC0tPiBzdGVwMw%3D%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBzdWJncmFwaCBzdGVwMVsiU3RlcCAxOiBHZW5lcmF0ZSBDb25maWciXQogICAgICAgIGNvbmZpZ1siY29uZmlnLXNlcnZlciJdCiAgICAgICAgc2NyaXB0WyJnZW5lcmF0ZS1jb25maWcuc2giXQogICAgICAgIGNvbmZpZyAtLT4gc2NyaXB0CiAgICBlbmQKICAgIAogICAgc3ViZ3JhcGggc3RlcDJbIlN0ZXAgMjogRGlzdHJpYnV0ZSAoTm9kZS10by1Ob2RlIFBsdWdpbikiXQogICAgICAgIHNvdXJjZVsiY29uZmlnLXNlcnZlcjxici8%2BL2V0Yy9teWFwcC9jb25maWcueW1sIl0KICAgICAgICBhcHAxWyJhcHAtMDEiXQogICAgICAgIGFwcDJbImFwcC0wMiJdCiAgICAgICAgYXBwM1siYXBwLTAzIl0KICAgICAgICBhcHA0WyJhcHAtMDQiXQogICAgICAgIGFwcDVbImFwcC0wNSJdCiAgICAgICAgc291cmNlIC0tPiBhcHAxCiAgICAgICAgc291cmNlIC0tPiBhcHAyCiAgICAgICAgc291cmNlIC0tPiBhcHAzCiAgICAgICAgc291cmNlIC0tPiBhcHA0CiAgICAgICAgc291cmNlIC0tPiBhcHA1CiAgICBlbmQKICAgIAogICAgc3ViZ3JhcGggc3RlcDNbIlN0ZXAgMzogUm9sbGluZyBSZWxvYWQiXQogICAgICAgIHIxWyJyZWxvYWQgYXBwLTAxIl0gLS0%2BIHIyWyJyZWxvYWQgYXBwLTAyIl0KICAgICAgICByMiAtLT4gcjNbInJlbG9hZCBhcHAtMDMiXQogICAgICAgIHIzIC0tPiByNFsicmVsb2FkIGFwcC0wNCJdCiAgICAgICAgcjQgLS0%2BIHI1WyJyZWxvYWQgYXBwLTA1Il0KICAgIGVuZAogICAgCiAgICBzdGVwMSAtLT4gc3RlcDIKICAgIHN0ZXAyIC0tPiBzdGVwMw%3D%3D" alt="Mermaid Diagram" width="1185" height="904"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job Configuration:&lt;/strong&gt;&lt;/p&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;Node(s)&lt;/th&gt;
&lt;th&gt;Action&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;config-server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/opt/scripts/generate-config.sh&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;config-server → app-01..05&lt;/td&gt;
&lt;td&gt;Node-to-Node copy &lt;code&gt;/etc/myapp/config.yml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;app-01..05 (sequential)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemctl reload myapp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-click config updates&lt;/li&gt;
&lt;li&gt;Parallel distribution to all servers&lt;/li&gt;
&lt;li&gt;Rolling reload to avoid downtime&lt;/li&gt;
&lt;li&gt;Complete audit trail&lt;/li&gt;
&lt;li&gt;Anyone with permissions can run it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing the Plugin
&lt;/h2&gt;

&lt;p&gt;The plugin repository includes a Docker-based test environment:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This spins up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Rundeck instance with the plugin&lt;/li&gt;
&lt;li&gt;Multiple test nodes for source/destination testing&lt;/li&gt;
&lt;li&gt;Pre-configured SSH keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;See &lt;code&gt;test/README.md&lt;/code&gt; for detailed testing instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD Integration
&lt;/h2&gt;

&lt;p&gt;Both repositories include GitHub Actions workflows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;rundeck-image:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Builds on every push to main&lt;/li&gt;
&lt;li&gt;Publishes to GitHub Container Registry&lt;/li&gt;
&lt;li&gt;Tags releases with version numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;rundeck-node-to-node:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Builds and tests the plugin&lt;/li&gt;
&lt;li&gt;Creates release artifacts&lt;/li&gt;
&lt;li&gt;Publishes JAR files to GitHub Releases&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deploying Rundeck on AWS
&lt;/h2&gt;

&lt;p&gt;If you're running on AWS, you have several options for deploying Rundeck. Here's my recommendation based on cost and operational effort:&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Option: Amazon ECS on Fargate
&lt;/h3&gt;

&lt;p&gt;For most teams, ECS Fargate hits the sweet spot between cost and operational simplicity:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBhbGJbIkFwcGxpY2F0aW9uPGJyLz5Mb2FkIEJhbGFuY2VyIl0gLS0%2BIGVjc1siRUNTIEZhcmdhdGU8YnIvPihSdW5kZWNrKSJdCiAgICBlY3MgLS0%2BIHJkc1siUkRTPGJyLz5Qb3N0Z3JlU1FMIl0KICAgIGVjcyAtLT4gZWZzWyJFRlM8YnIvPi92YXIvcnVuZGVjayJdCiAgICBlY3MgLS0%2BIHNtWyJTZWNyZXRzPGJyLz5NYW5hZ2VyIl0%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBhbGJbIkFwcGxpY2F0aW9uPGJyLz5Mb2FkIEJhbGFuY2VyIl0gLS0%2BIGVjc1siRUNTIEZhcmdhdGU8YnIvPihSdW5kZWNrKSJdCiAgICBlY3MgLS0%2BIHJkc1siUkRTPGJyLz5Qb3N0Z3JlU1FMIl0KICAgIGVjcyAtLT4gZWZzWyJFRlM8YnIvPi92YXIvcnVuZGVjayJdCiAgICBlY3MgLS0%2BIHNtWyJTZWNyZXRzPGJyLz5NYW5hZ2VyIl0%3D" alt="Mermaid Diagram" width="580" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;ECS Fargate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Operational Effort&lt;/td&gt;
&lt;td&gt;Low - no EC2 instances to manage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;~$30-50/month for small workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling&lt;/td&gt;
&lt;td&gt;Easy horizontal scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Native ALB, Secrets Manager, CloudWatch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;EFS for Rundeck data, RDS for database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Budget Option: ECS with PostgreSQL Sidecar
&lt;/h3&gt;

&lt;p&gt;For cost-conscious deployments, you can run PostgreSQL as a sidecar container instead of using RDS. This cuts costs significantly while maintaining a containerized approach:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBhbGJbIkFwcGxpY2F0aW9uPGJyLz5Mb2FkIEJhbGFuY2VyIl0gLS0%2BIGVjc1siRUNTIEZhcmdhdGUiXQogICAgc3ViZ3JhcGggZWNzWyJFQ1MgVGFzayJdCiAgICAgICAgcnVuZGVja1siUnVuZGVjazxici8%2BQ29udGFpbmVyIl0KICAgICAgICBwb3N0Z3Jlc1siUG9zdGdyZVNRTDxici8%2BQ29udGFpbmVyIl0KICAgICAgICBydW5kZWNrIC0tPiBwb3N0Z3JlcwogICAgZW5kCiAgICBlY3MgLS0%2BIGVmc1siRUZTPGJyLz4oZGF0YSArIHBnZGF0YSkiXQ%3D%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBhbGJbIkFwcGxpY2F0aW9uPGJyLz5Mb2FkIEJhbGFuY2VyIl0gLS0%2BIGVjc1siRUNTIEZhcmdhdGUiXQogICAgc3ViZ3JhcGggZWNzWyJFQ1MgVGFzayJdCiAgICAgICAgcnVuZGVja1siUnVuZGVjazxici8%2BQ29udGFpbmVyIl0KICAgICAgICBwb3N0Z3Jlc1siUG9zdGdyZVNRTDxici8%2BQ29udGFpbmVyIl0KICAgICAgICBydW5kZWNrIC0tPiBwb3N0Z3JlcwogICAgZW5kCiAgICBlY3MgLS0%2BIGVmc1siRUZTPGJyLz4oZGF0YSArIHBnZGF0YSkiXQ%3D%3D" alt="Mermaid Diagram" width="665" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;With RDS&lt;/th&gt;
&lt;th&gt;With PostgreSQL Sidecar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Monthly Cost&lt;/td&gt;
&lt;td&gt;$50-80&lt;/td&gt;
&lt;td&gt;$20-35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database Backups&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Manual (EFS snapshots)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High Availability&lt;/td&gt;
&lt;td&gt;RDS Multi-AZ option&lt;/td&gt;
&lt;td&gt;Single container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;Two services&lt;/td&gt;
&lt;td&gt;Single task&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most Rundeck deployments, the sidecar approach works great—Rundeck isn't a high-transaction workload, and EFS provides durability for your data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparison of AWS Options
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Ops Effort&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2 (t3.small)&lt;/td&gt;
&lt;td&gt;$15-20&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Budget-conscious, 24/7 workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EC2 Spot&lt;/td&gt;
&lt;td&gt;$5-10&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Dev/test, can tolerate interruptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lightsail&lt;/td&gt;
&lt;td&gt;$5-20&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Learning, simple setups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECS Fargate&lt;/td&gt;
&lt;td&gt;$30-50&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Production, low maintenance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EKS&lt;/td&gt;
&lt;td&gt;$100+&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Already running Kubernetes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Why Not the Others?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EC2&lt;/strong&gt;: More operational overhead (patching, monitoring), but cheaper for 24/7 workloads with Reserved Instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EKS&lt;/strong&gt;: Overkill unless you already have a Kubernetes cluster—adds complexity and ~$70/month just for the control plane&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Runner&lt;/strong&gt;: Simpler but limited networking control, harder to reach private infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightsail&lt;/strong&gt;: Cheapest option but limited VPC integration for node access&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My Recommendations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small team/learning&lt;/strong&gt;: EC2 t3.small or Lightsail (~$10-20/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production with low ops effort&lt;/strong&gt;: ECS Fargate + RDS (~$50-80/month)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enterprise/HA&lt;/strong&gt;: ECS Fargate multi-task + RDS Multi-AZ (~$150+/month)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ECS Task Definition Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"family"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rundeck"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"networkMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"awsvpc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requiresCompatibilities"&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;"FARGATE"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cpu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1024"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"memory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"containerDefinitions"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rundeck"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ghcr.io/walsen/rundeck-image: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;"portMappings"&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="nl"&gt;"containerPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"protocol"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tcp"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"environment"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RUNDECK_GRAILS_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://rundeck.example.com"&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;"secrets"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RUNDECK_DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valueFrom"&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:secretsmanager:..."&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RUNDECK_DATABASE_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valueFrom"&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:secretsmanager:..."&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;"mountPoints"&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="nl"&gt;"sourceVolume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rundeck-data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"containerPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/var/rundeck"&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;"logConfiguration"&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;"logDriver"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"awslogs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"options"&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;"awslogs-group"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/ecs/rundeck"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"awslogs-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;"us-east-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"awslogs-stream-prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ecs"&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;"volumes"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rundeck-data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"efsVolumeConfiguration"&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;"fileSystemId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fs-xxxxxxxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"transitEncryption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ENABLED"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup gives you a production-ready Rundeck deployment with persistent storage, secrets management, and centralized logging—all with minimal operational overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Rundeck transforms operational chaos into repeatable, auditable procedures. It's the tool I reach for when I need to bridge the gap between "we should automate this" and "we have time to build proper automation."&lt;/p&gt;

&lt;p&gt;The Node-to-Node plugin fills a specific gap—efficient file distribution between nodes without routing everything through Rundeck. And the Docker image removes the friction of getting a production-ready Rundeck instance running.&lt;/p&gt;

&lt;p&gt;If you're drowning in operational toil, give Rundeck a try. And if you need these specific capabilities, the tools are ready for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rundeck GitHub Action&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/rundeck-github-action" rel="noopener noreferrer"&gt;https://github.com/Walsen/rundeck-github-action&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rundeck Docker Image&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/rundeck-image" rel="noopener noreferrer"&gt;https://github.com/Walsen/rundeck-image&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node-to-Node Plugin&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/rundeck-node-to-node" rel="noopener noreferrer"&gt;https://github.com/Walsen/rundeck-node-to-node&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rundeck Official Docs&lt;/strong&gt;: &lt;a href="https://docs.rundeck.com" rel="noopener noreferrer"&gt;https://docs.rundeck.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My Blog&lt;/strong&gt;: &lt;a href="https://blog.walsen.website" rel="noopener noreferrer"&gt;https://blog.walsen.website&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>opensource</category>
      <category>tooling</category>
    </item>
    <item>
      <title>GitHub Action to Publish Hugo Posts to Dev.to</title>
      <dc:creator>Sergio D. Rodríguez Inclán</dc:creator>
      <pubDate>Mon, 26 Jan 2026 04:01:50 +0000</pubDate>
      <link>https://dev.to/w4ls3n/github-action-to-publish-hugo-posts-to-devto-2nmp</link>
      <guid>https://dev.to/w4ls3n/github-action-to-publish-hugo-posts-to-devto-2nmp</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As developers who maintain technical blogs, we often face a common dilemma: should we publish exclusively on our own site, or should we cross-post to platforms like Dev.to, Medium, or Hashnode to reach a wider audience?&lt;/p&gt;

&lt;p&gt;The answer is usually "both," but that creates a new problem: &lt;strong&gt;manual cross-posting is tedious, error-prone, and time-consuming&lt;/strong&gt;. You write a post in Hugo, publish it to your site, then manually copy-paste the content to Dev.to, adjust the formatting, add tags, set the canonical URL, and hope you didn't miss anything.&lt;/p&gt;

&lt;p&gt;I experienced this friction firsthand with my Hugo-powered blog at &lt;a href="https://blog.walsen.website" rel="noopener noreferrer"&gt;blog.walsen.website&lt;/a&gt;. After publishing several posts and manually cross-posting them to Dev.to, I realized this workflow was unsustainable. There had to be a better way.&lt;/p&gt;

&lt;p&gt;That's when I decided to build &lt;strong&gt;&lt;a href="https://github.com/Walsen/hugo2devto" rel="noopener noreferrer"&gt;hugo2devto&lt;/a&gt;&lt;/strong&gt;: a GitHub Action that automatically publishes Hugo blog posts to Dev.to with full frontmatter support, canonical URLs, and zero manual intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Cross-Posting is Painful
&lt;/h2&gt;

&lt;p&gt;Let me paint a picture of the traditional workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write your post in Hugo with YAML frontmatter&lt;/li&gt;
&lt;li&gt;Build and deploy your Hugo site&lt;/li&gt;
&lt;li&gt;Open Dev.to in your browser&lt;/li&gt;
&lt;li&gt;Copy-paste your markdown content&lt;/li&gt;
&lt;li&gt;Manually configure:

&lt;ul&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Tags (max 4)&lt;/li&gt;
&lt;li&gt;Cover image&lt;/li&gt;
&lt;li&gt;Canonical URL&lt;/li&gt;
&lt;li&gt;Published/draft status&lt;/li&gt;
&lt;li&gt;Series information&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Preview and publish&lt;/li&gt;
&lt;li&gt;Repeat for every post update&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This process has several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Time-consuming&lt;/strong&gt;: 10-15 minutes per post&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error-prone&lt;/strong&gt;: Easy to forget canonical URLs or tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not scalable&lt;/strong&gt;: Discourages cross-posting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No version control&lt;/strong&gt;: Changes aren't tracked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manual synchronization&lt;/strong&gt;: Updates require repeating the entire process&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: Automation Through GitHub Actions
&lt;/h2&gt;

&lt;p&gt;The ideal solution would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect when a Hugo post is created or updated&lt;/li&gt;
&lt;li&gt;Automatically extract frontmatter metadata&lt;/li&gt;
&lt;li&gt;Publish or update the post on Dev.to&lt;/li&gt;
&lt;li&gt;Set the correct canonical URL&lt;/li&gt;
&lt;li&gt;Handle tags, cover images, and series&lt;/li&gt;
&lt;li&gt;Work seamlessly with existing Hugo workflows&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is exactly what &lt;code&gt;hugo2devto&lt;/code&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Action: Technical Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture Overview
&lt;/h3&gt;

&lt;p&gt;The action is built with TypeScript and runs on Node.js 20. Here's the high-level architecture:&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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBzdWJncmFwaCB0cmlnZ2VyWyJHaXRIdWIgV29ya2Zsb3cgVHJpZ2dlciJdCiAgICAgICAgVDFbInB1c2ggdG8gbWFpbiJdCiAgICAgICAgVDJbIndvcmtmbG93X2Rpc3BhdGNoIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIGFjdGlvblsiaHVnbzJkZXZ0byBBY3Rpb24gdjEiXQogICAgICAgIEExWyIxLiBSZWFkIE1hcmtkb3duIEZpbGUiXQogICAgICAgIEEyWyIyLiBQYXJzZSBZQU1MIEZyb250bWF0dGVyIl0KICAgICAgICBBM1siMy4gVHJhbnNmb3JtIEh1Z28g4oaSIERldi50byJdCiAgICAgICAgQTRbIjQuIENhbGwgRGV2LnRvIEFQSSJdCiAgICAgICAgQTVbIjUuIFJldHVybiBPdXRwdXRzIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIG91dHB1dFsiUmVzdWx0Il0KICAgICAgICBPMVsiUHVibGlzaGVkIG9uIERldi50byJdCiAgICBlbmQKCiAgICB0cmlnZ2VyIC0tPiBBMQogICAgQTEgLS0%2BIEEyCiAgICBBMiAtLT4gQTMKICAgIEEzIC0tPiBBNAogICAgQTQgLS0%2BIEE1CiAgICBBNSAtLT4gTzE%3D" 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%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IFRECiAgICBzdWJncmFwaCB0cmlnZ2VyWyJHaXRIdWIgV29ya2Zsb3cgVHJpZ2dlciJdCiAgICAgICAgVDFbInB1c2ggdG8gbWFpbiJdCiAgICAgICAgVDJbIndvcmtmbG93X2Rpc3BhdGNoIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIGFjdGlvblsiaHVnbzJkZXZ0byBBY3Rpb24gdjEiXQogICAgICAgIEExWyIxLiBSZWFkIE1hcmtkb3duIEZpbGUiXQogICAgICAgIEEyWyIyLiBQYXJzZSBZQU1MIEZyb250bWF0dGVyIl0KICAgICAgICBBM1siMy4gVHJhbnNmb3JtIEh1Z28g4oaSIERldi50byJdCiAgICAgICAgQTRbIjQuIENhbGwgRGV2LnRvIEFQSSJdCiAgICAgICAgQTVbIjUuIFJldHVybiBPdXRwdXRzIl0KICAgIGVuZAoKICAgIHN1YmdyYXBoIG91dHB1dFsiUmVzdWx0Il0KICAgICAgICBPMVsiUHVibGlzaGVkIG9uIERldi50byJdCiAgICBlbmQKCiAgICB0cmlnZ2VyIC0tPiBBMQogICAgQTEgLS0%2BIEEyCiAgICBBMiAtLT4gQTMKICAgIEEzIC0tPiBBNAogICAgQTQgLS0%2BIEE1CiAgICBBNSAtLT4gTzE%3D" alt="Mermaid Diagram" width="346" height="992"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Full Hugo Frontmatter Support
&lt;/h4&gt;

&lt;p&gt;The action understands Hugo's frontmatter format natively:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;My&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Awesome&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Post"&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dive&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;into&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;something&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cool"&lt;/span&gt;
&lt;span class="na"&gt;publishdate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-01-25T22:23:37-04:00&lt;/span&gt;
&lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;tags&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;hugo"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;devto"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;automation"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hugo&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Automation"&lt;/span&gt;
&lt;span class="na"&gt;eyecatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/cover.png"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It automatically maps these fields to Dev.to's API format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt; → &lt;code&gt;title&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt; → &lt;code&gt;description&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tags&lt;/code&gt; → &lt;code&gt;tags&lt;/code&gt; (limited to 4)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;series&lt;/code&gt; → &lt;code&gt;series&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;eyecatch&lt;/code&gt; / &lt;code&gt;cover_image&lt;/code&gt; → &lt;code&gt;main_image&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;draft&lt;/code&gt; → &lt;code&gt;published&lt;/code&gt; (inverted)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. Automatic Canonical URL Generation
&lt;/h4&gt;

&lt;p&gt;One of the most important SEO considerations when cross-posting is setting the canonical URL to point back to your original post. The action automatically generates this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canonicalUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a post at &lt;code&gt;content/en/posts/my-post.md&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://blog.walsen.website/en/posts/my-post/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Multi-Language Support
&lt;/h4&gt;

&lt;p&gt;The action detects the language from the file path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content/en/posts/my-post.md  → English
content/es/posts/mi-post.md  → Spanish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is crucial for Hugo sites with i18n support.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Mermaid Diagram Support (v1.1.0)
&lt;/h4&gt;

&lt;p&gt;Hugo uses shortcodes for mermaid diagrams, but Dev.to doesn't support mermaid natively. The action automatically converts Hugo mermaid shortcodes to rendered PNG images using the mermaid.ink service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Hugo format (in your source) --&amp;gt;&lt;/span&gt;
{{&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;mermaid&lt;/span&gt; &lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;}}
flowchart TD
    A --&amp;gt; B
{{&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="err"&gt;*&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nt"&gt;mermaid&lt;/span&gt; &lt;span class="err"&gt;*/&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;}}

&lt;span class="c"&gt;&amp;lt;!-- Converted to (on Dev.to) --&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;Mermaid Diagram&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://mermaid.ink/img/base64encodeddiagram&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means your diagrams render beautifully on both platforms without any manual intervention.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Idempotent Updates
&lt;/h4&gt;

&lt;p&gt;The action checks if an article already exists on Dev.to (by canonical URL) and updates it instead of creating a duplicate. This means you can run the action multiple times safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation Highlights
&lt;/h3&gt;

&lt;p&gt;Here's a simplified version of the core logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Read and parse the markdown file&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frontmatter&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fileContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Extract metadata&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;slice&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Dev.to limit&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;published&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Generate canonical URL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/en/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canonicalUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Prepare Dev.to article&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body_markdown&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;published&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="na"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canonicalUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;main_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eyecatch&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cover_image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;frontmatter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Check if article exists&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existingArticle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findArticleByCanonicalUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonicalUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingArticle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Update existing article&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;updateArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existingArticle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create new article&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createArticle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&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;
  
  
  Using the Action: Practical Examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Basic Setup
&lt;/h3&gt;

&lt;p&gt;First, get your Dev.to API key from &lt;a href="https://dev.to/settings/extensions"&gt;https://dev.to/settings/extensions&lt;/a&gt; and add it to your repository secrets as &lt;code&gt;DEVTO_API_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then create &lt;code&gt;.github/workflows/publish-devto.yml&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to Dev.to&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content/*/posts/*.md'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="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;Publish to Dev.to&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Walsen/hugo2devto@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEVTO_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;file-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content/en/posts/my-post.md'&lt;/span&gt;
          &lt;span class="na"&gt;base-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://blog.walsen.website'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Advanced: Automatic Detection of Changed Posts
&lt;/h3&gt;

&lt;p&gt;For my blog, I wanted the action to automatically detect which posts changed and publish only those:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to Dev.to&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content/*/posts/*.md'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;detect-changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.changed-files.outputs.posts }}&lt;/span&gt;
      &lt;span class="na"&gt;has-changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.changed-files.outputs.has-changes }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&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;Get changed files&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changed-files&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;CHANGED_FILES=$(git diff --name-only HEAD^ HEAD | grep -E 'content/en/posts/.*\.md' || echo "")&lt;/span&gt;

          &lt;span class="s"&gt;if [ -n "$CHANGED_FILES" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;POSTS_JSON=$(echo "$CHANGED_FILES" | jq -R -s -c 'split("\n") | map(select(length &amp;gt; 0))')&lt;/span&gt;
            &lt;span class="s"&gt;echo "posts=$POSTS_JSON" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
            &lt;span class="s"&gt;echo "has-changes=true" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "posts=[]" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
            &lt;span class="s"&gt;echo "has-changes=false" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

  &lt;span class="na"&gt;publish-changed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;needs.detect-changes.outputs.has-changes == 'true'&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;detect-changes&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ fromJson(needs.detect-changes.outputs.posts) }}&lt;/span&gt;
      &lt;span class="na"&gt;fail-fast&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="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;Publish to Dev.to&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Walsen/hugo2devto@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEVTO_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;file-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ matrix.post }}&lt;/span&gt;
          &lt;span class="na"&gt;base-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://blog.walsen.website'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detects which markdown files changed in the last commit&lt;/li&gt;
&lt;li&gt;Converts them to a JSON array&lt;/li&gt;
&lt;li&gt;Uses a matrix strategy to publish multiple posts in parallel&lt;/li&gt;
&lt;li&gt;Sets &lt;code&gt;fail-fast: false&lt;/code&gt; so one failure doesn't stop others&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Manual Trigger
&lt;/h3&gt;

&lt;p&gt;You can also trigger publishing manually:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;post_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Path&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;post'&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;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="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;Publish to Dev.to&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Walsen/hugo2devto@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEVTO_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;file-path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.inputs.post_path }}&lt;/span&gt;
          &lt;span class="na"&gt;base-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://blog.walsen.website'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Real-World Results
&lt;/h2&gt;

&lt;p&gt;Since implementing this action on my blog, the results have been transformative:&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;⏱️ 15 minutes per post to cross-post manually&lt;/li&gt;
&lt;li&gt;🐛 Frequent mistakes (forgotten canonical URLs, wrong tags)&lt;/li&gt;
&lt;li&gt;😓 Discouraged from updating posts on Dev.to&lt;/li&gt;
&lt;li&gt;📉 Inconsistent presence on Dev.to&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;⚡ Automatic publishing in ~30 seconds&lt;/li&gt;
&lt;li&gt;✅ Zero manual intervention required&lt;/li&gt;
&lt;li&gt;🔄 Updates synchronized automatically&lt;/li&gt;
&lt;li&gt;📈 Consistent cross-posting to Dev.to&lt;/li&gt;
&lt;li&gt;🎯 More time for writing, less for publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building this action taught me several valuable lessons:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Start with a Script, Then Package It
&lt;/h3&gt;

&lt;p&gt;I initially built a TypeScript script (&lt;code&gt;publish-to-devto.ts&lt;/code&gt;) that worked locally. Once it was stable, I packaged it as a GitHub Action. This iterative approach made debugging much easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Frontmatter Mapping is Tricky
&lt;/h3&gt;

&lt;p&gt;Hugo and Dev.to use different field names and formats. Creating a robust mapping layer required careful testing with various post formats.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Idempotency Matters
&lt;/h3&gt;

&lt;p&gt;The action needed to handle both new posts and updates gracefully. Checking for existing articles by canonical URL was crucial.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Documentation is Key
&lt;/h3&gt;

&lt;p&gt;I created multiple documentation files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;README.md&lt;/code&gt; - Overview and quick start&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GETTING_STARTED.md&lt;/code&gt; - 5-minute setup guide&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SETUP.md&lt;/code&gt; - Comprehensive instructions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HUGO_COMPATIBILITY.md&lt;/code&gt; - Hugo-specific details&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;API_KEY_SETUP.md&lt;/code&gt; - Security best practices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This made the action accessible to users with different needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Community Feedback is Invaluable
&lt;/h3&gt;

&lt;p&gt;Publishing the action to the GitHub Marketplace exposed it to real-world use cases I hadn't considered. User feedback helped improve error handling and edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Enhancements
&lt;/h2&gt;

&lt;p&gt;While the action works great, there's always room for improvement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Batch Publishing&lt;/strong&gt;: Support publishing multiple posts in a single action invocation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dry Run Mode&lt;/strong&gt;: Preview what would be published without actually doing it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Field Mapping&lt;/strong&gt;: Allow users to configure their own frontmatter mappings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image Upload&lt;/strong&gt;: Automatically upload local images to Dev.to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics Integration&lt;/strong&gt;: Track publishing metrics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Platform Support&lt;/strong&gt;: Extend to Medium, Hashnode, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional Shortcode Support&lt;/strong&gt;: Transform other Hugo shortcodes (YouTube, Twitter embeds, etc.)&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;Building &lt;code&gt;hugo2devto&lt;/code&gt; solved a real problem I faced as a technical blogger: the friction of cross-posting content. By automating the process through GitHub Actions, I eliminated manual work, reduced errors, and made it effortless to maintain a presence on Dev.to.&lt;/p&gt;

&lt;p&gt;The action is open source and available for anyone to use. Whether you're running a Hugo blog, a Jekyll site, or any markdown-based platform, the core concepts apply: &lt;strong&gt;automate the boring stuff so you can focus on writing great content&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're interested in trying it out or contributing, check out the repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/hugo2devto" rel="noopener noreferrer"&gt;https://github.com/Walsen/hugo2devto&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Marketplace&lt;/strong&gt;: &lt;a href="https://github.com/marketplace/actions/hugo-to-dev-to-publisher" rel="noopener noreferrer"&gt;https://github.com/marketplace/actions/hugo-to-dev-to-publisher&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The future of technical blogging is automated, and I'm excited to see where this journey leads. Happy blogging!&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Action Repository&lt;/strong&gt;: &lt;a href="https://github.com/Walsen/hugo2devto" rel="noopener noreferrer"&gt;https://github.com/Walsen/hugo2devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My Blog&lt;/strong&gt;: &lt;a href="https://blog.walsen.website" rel="noopener noreferrer"&gt;https://blog.walsen.website&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to Profile&lt;/strong&gt;: &lt;a href="https://dev.to/w4ls3n"&gt;https://dev.to/w4ls3n&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to API Documentation&lt;/strong&gt;: &lt;a href="https://developers.forem.com/api" rel="noopener noreferrer"&gt;https://developers.forem.com/api&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>devto</category>
      <category>github</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A Wild Ride Into Vibe Coding</title>
      <dc:creator>Sergio D. Rodríguez Inclán</dc:creator>
      <pubDate>Fri, 23 Jan 2026 11:32:21 +0000</pubDate>
      <link>https://dev.to/w4ls3n/a-wild-ride-into-vibe-coding-5n</link>
      <guid>https://dev.to/w4ls3n/a-wild-ride-into-vibe-coding-5n</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Since I was a child, computers have held a magnetic fascination for me. I vividly remember the first time I saw one in my mother’s office at the age of eleven; in that exact moment, I knew precisely what I wanted to do with my life.&lt;/p&gt;

&lt;p&gt;Although destiny led me down the path of infrastructure, I never truly abandoned the dream of developing applications. While I have a solid foundation in programming and can hold my own in the terminal, a lack of daily practice and the time required for a deep dive had—until now—prevented me from producing professional-grade software.&lt;/p&gt;

&lt;p&gt;Today, however, the landscape has shifted. With the rise of Artificial Intelligence, LLMs, and Generative AI, a new horizon has opened for profiles like mine: the ability to bring complex applications to life without needing to be an expert in deep syntax, by leveraging AI-powered editors that amplify our creative potential.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge: AWS Community Day Bolivia 2025
&lt;/h3&gt;

&lt;p&gt;This adventure began with the organization of &lt;strong&gt;AWS Community Day Bolivia 2025&lt;/strong&gt;. This year, the honor of leading the event fell to the &lt;strong&gt;AWS User Group Cochabamba&lt;/strong&gt;, a team where I serve as one of the leaders. Organizing an event of this magnitude requires impeccable coordination with volunteers; while tools like Google Forms or Sheets are useful, I was looking for something more: a comprehensive, custom-built solution.&lt;/p&gt;

&lt;p&gt;My vision was to build a platform that would allow us to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manage Projects:&lt;/strong&gt; Create and administer specific initiatives for the event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Registration:&lt;/strong&gt; Allow volunteers to sign up directly for projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multichannel Communication:&lt;/strong&gt; Send announcements and updates from a centralized hub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Panel:&lt;/strong&gt; A secure portal for operations exclusive to authorized leaders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technological Innovation:&lt;/strong&gt; Test &lt;strong&gt;Kiro&lt;/strong&gt;, Amazon’s current IDE, and its powerful generative AI capabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community Legacy:&lt;/strong&gt; Create a project "by the community, for the community" that could serve as a real-world case study in our talks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Journey is the Reward
&lt;/h3&gt;

&lt;p&gt;Although the development wasn't finalized in time for the Community Day, the experience was a revelation. This process deeply enriched my understanding of Generative AI, application architecture, and the modern software development lifecycle. Above all, it allowed me to discover the "tips &amp;amp; tricks" of working with AI agents and the delicate human-machine synergy required to achieve the desired result.&lt;/p&gt;

&lt;p&gt;As of this writing, &lt;strong&gt;Kiro&lt;/strong&gt; has advanced significantly. Many of the initial limitations I encountered have been addressed—particularly regarding session management and memory—solidifying its position as a cutting-edge tool that leaves previous versions behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Vibe Coding" is No Joke
&lt;/h2&gt;

&lt;p&gt;I must confess, I was quite naive at first. I trusted Kiro’s output almost blindly because, initially at least, it generated the site’s starting structure correctly and surprisingly fast. The project kicked off with a specifications proposal (a &lt;em&gt;Spec&lt;/em&gt;) designed by Kiro itself; in my &lt;em&gt;prompt&lt;/em&gt;, I asked it to generate a three-tier serverless project: &lt;strong&gt;AstroJS&lt;/strong&gt; for the frontend, &lt;strong&gt;FastAPI&lt;/strong&gt; for the backend, and &lt;strong&gt;DynamoDB&lt;/strong&gt; for data persistence.&lt;/p&gt;

&lt;p&gt;During the early stages, I worked with three editors open and deployed directly from my local machine to the User Group’s AWS account. However, the time came to do things right and set up deployment via a &lt;strong&gt;CI/CD&lt;/strong&gt; workflow (because, as the saying goes: "the shoemaker's son always goes barefoot"). For this, I decided to use &lt;strong&gt;AWS CDK&lt;/strong&gt;. In my experience, managing state files is an additional headache I wanted to avoid, so I ruled out Terraform and Pulumi from the start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unifying All Projects Under a Single Session
&lt;/h3&gt;

&lt;p&gt;At the start of every task, I would enter a prompt to polish details that didn’t quite look right or weren't functioning correctly. This is where I hit my first major roadblock: I had four different Kiro sessions that shared no context with one another.&lt;/p&gt;

&lt;p&gt;If Kiro detected an API bottleneck while developing the frontend, the tool was essentially "blind" to the other components. This forced me into a constant cycle of copying and pasting to transfer data from one environment to another. To solve this, I decided to centralize everything into a &lt;strong&gt;single session&lt;/strong&gt;. I grouped all the repositories into a root directory so that Kiro could navigate between them with full awareness of how the components interacted.&lt;/p&gt;

&lt;p&gt;Below is the local structure I defined to achieve that synchrony:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;❯ tree &lt;span class="nt"&gt;-L&lt;/span&gt; 1 &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;.&lt;/span&gt;
├── .amazonq
├── .devbox
├── .envrc
├── .gitignore
├── .kiro
├── .python-version
├── .venv
├── README.md
├── devbox.json
├── devbox.lock
├── generated-diagrams
├── pyproject.toml
├── registry-api
├── registry-documentation
├── registry-frontend
├── registry-infrastructure
└── uv.lock

9 directories, 8 files

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reader will notice that I have Devbox configured in the root directory. I made this choice because Kiro frequently needed to run Python scripts to sanitize repositories, perform searches, or handle troubleshooting tasks. Therefore, I saw the need to provide it with an isolated dependency chain, thus avoiding the installation of unnecessary software on my primary operating system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clear Rules: The Art of Coexistence
&lt;/h2&gt;

&lt;p&gt;This was the most complex stage and the one that demanded the most time. It was a period of alignment between the AI and myself; the moment where we discovered our character, our limits, and just how much we could tolerate one another. Readers might be skeptical, but after working through dozens of sessions, I can confirm that each one develops a distinct "personality." There is a subtle but real difference: the speed at which they grasp previous context, the tone of the conversation, and the level of initiative varies from session to session.&lt;/p&gt;

&lt;p&gt;For those who haven't yet experimented with &lt;strong&gt;Kiro&lt;/strong&gt; (or Amazon Q), there are fundamental aspects that must be taken very seriously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session Volatility:&lt;/strong&gt; Sessions are temporary. Upon reaching a context limit, the session resets, transferring only a very brief summary of what happened. Critical details can be lost in this process, especially if the previous activity was intense.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust Management:&lt;/strong&gt; When running tools, Kiro asks if commands are safe. If you decline, it won't execute anything; if you accept, it will run everything without further confirmation. It’s an "all or nothing" situation that requires vigilance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git Structure Invisibility:&lt;/strong&gt; Kiro does not natively understand the concept of a "repository." It ignores the existence of commits, branches, or staged files. Because of this, you must be surgical: you have to tell it exactly which file to modify and which change to make.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project Ecosystem Disconnect:&lt;/strong&gt; It doesn’t automatically assume the existence of config files, dependencies, or CI/CD workflows. To Kiro, every file is an isolated entity unless you provide the full map.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ignoring these rules comes at a high price: Kiro will generate code that doesn't work or doesn't align with your goals. The most dangerous part is that the tool will always confidently assure you that everything is fine, leaving you with a false sense of success.&lt;/p&gt;

&lt;p&gt;The breaking point came during an iteration where I requested an architectural adjustment. Kiro misinterpreted the instruction and altered the &lt;strong&gt;entire&lt;/strong&gt; project: it transformed a Serverless architecture into one based on ECS and Aurora. It was a frustrating experience, but a necessary one. At that moment, I decided to establish rules of engagement: I created an &lt;strong&gt;"Architecture Primitives"&lt;/strong&gt; document. In it, I defined strict guidelines on project structure, repository management, and expected behavior when publishing changes.&lt;/p&gt;

&lt;p&gt;From that "contract" onward, the project stabilized. I moved forward with a fluidity that previously seemed impossible, and thanks to that, the beta version materialized much sooner than expected and is already available online.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Blueprint: Anatomy of an Efficient Architecture
&lt;/h2&gt;

&lt;p&gt;The project's architecture is illustrated in the following diagram:&lt;/p&gt;

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

&lt;p&gt;To achieve the goals of cost-efficiency and scalability, I designed a modular structure divided into logical layers. Here is how the components interact:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Access and Delivery Layer (Edge &amp;amp; Auth)
&lt;/h3&gt;

&lt;p&gt;This is the first line of contact with the user, prioritizing security and speed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon CloudFront:&lt;/strong&gt; Acts as a CDN to distribute content with low global latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Cognito:&lt;/strong&gt; Serves as the Identity Provider (IdP), managing authentication and the lifecycle of both users and administrators.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Interface Layer (API Gateway)
&lt;/h3&gt;

&lt;p&gt;We separated the control planes to ensure the security of sensitive operations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App API:&lt;/strong&gt; The public entry point for application functionalities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin API:&lt;/strong&gt; A dedicated, isolated Gateway for privileged administrative operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. The Brain: Orchestration and Serverless Logic
&lt;/h3&gt;

&lt;p&gt;This is where the system's intelligence resides, utilizing an &lt;strong&gt;event-driven microservices&lt;/strong&gt; approach.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Step Functions:&lt;/strong&gt; Orchestrates complex workflows, ensuring each step executes in the correct order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda Functions (Multi-purpose):&lt;/strong&gt; Specialized functions that execute business logic, ranging from the &lt;em&gt;Route Manager&lt;/em&gt; to access control (&lt;em&gt;HBAC&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon ECR:&lt;/strong&gt; Stores the container images that power our Lambdas, enabling consistent execution environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CDK:&lt;/strong&gt; The "glue" of the entire project, allowing this infrastructure to be 100% reproducible through code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Service Layer and Dynamic Registry
&lt;/h3&gt;

&lt;p&gt;The heart of the system uses the &lt;strong&gt;Service Registry&lt;/strong&gt; pattern to avoid tight coupling.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service Registry Pattern:&lt;/strong&gt; Implemented via Lambdas acting as a central directory, enabling dynamic service discovery at runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialized Services:&lt;/strong&gt; Dedicated modules for searches (&lt;em&gt;Search Service&lt;/em&gt;), scheduled tasks (&lt;em&gt;Cron Service&lt;/em&gt;), and personnel management (&lt;em&gt;People Service&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Persistence and Communications (Data Layer)
&lt;/h3&gt;

&lt;p&gt;A polyglot combination to handle different data types efficiently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon DynamoDB:&lt;/strong&gt; Our NoSQL database for high-speed caching and case management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon S3:&lt;/strong&gt; The central repository for image and object storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SES:&lt;/strong&gt; The notification engine for direct communication with volunteers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. Cross-Cutting Layers: Security and Observability
&lt;/h3&gt;

&lt;p&gt;Components that span the entire architecture to ensure health and protection.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security and Configuration:&lt;/strong&gt; Intensive use of &lt;strong&gt;AWS Secrets Manager&lt;/strong&gt; and &lt;strong&gt;Parameter Store&lt;/strong&gt; for secret management and zero-trust policies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;360° Observability:&lt;/strong&gt; Implementation of &lt;strong&gt;Amazon CloudWatch&lt;/strong&gt; for tracing, performance metrics, and log centralization.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Project Manifesto: Patterns and Guidelines
&lt;/h2&gt;

&lt;p&gt;To prevent our collaboration with AI from descending into the architectural chaos I mentioned earlier, I had to formalize our knowledge into two fundamental pillars. These documents didn't just guide Kiro; they established the foundation of what I consider modern assisted development.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Enterprise Architecture Patterns (EAP)
&lt;/h3&gt;

&lt;p&gt;It isn’t just about writing code; it’s about following principles that guarantee the system’s evolution. Based on the documentation from the &lt;a href="https://github.com/awscbba/registry-documentation" rel="noopener noreferrer"&gt;Registry Project&lt;/a&gt;, we implemented three golden rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolation and Decoupling:&lt;/strong&gt; Every service operates autonomously. The use of the &lt;strong&gt;Service Registry&lt;/strong&gt; is not optional; it is the mechanism that allows our infrastructure to scale without creating rigid dependencies (&lt;em&gt;spaghetti code&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for Resilience:&lt;/strong&gt; We apply the &lt;strong&gt;Saga Pattern&lt;/strong&gt; to manage distributed transactions in serverless environments, ensuring that if one step fails, the system can automatically recover or compensate for the error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security by Design (Zero-Trust):&lt;/strong&gt; No component trusts another by default. Every interaction requires identity validation through &lt;strong&gt;Cognito&lt;/strong&gt; and access control based on strict policies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. AI Coexistence Manual: The "AI Assistant Guidelines"
&lt;/h3&gt;

&lt;p&gt;Learning to talk to an agent like Kiro requires more than simple instructions; it requires a framework. These are the key takeaways from our &lt;a href="https://github.com/awscbba/registry-documentation/blob/main/workflows/ai-assistant-guidelines.md" rel="noopener noreferrer"&gt;Assistance Guidelines&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incremental Context:&lt;/strong&gt; Instead of firing off massive prompts, we feed the AI specific fragments of context. If Kiro knows the database structure but not the authentication flow, we explicitly remind it of the latter before asking for a change in that area.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mandatory Human Validation:&lt;/strong&gt; The AI proposes; the human disposes. We established that no deployment occurs without a manual review of the generated &lt;em&gt;diffs&lt;/em&gt;. This prevented the project from accidentally transforming back into costly and unnecessary infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Living Documentation:&lt;/strong&gt; Every time the AI generated an innovative solution, that logic was immediately documented. In this way, the "memory" of the project didn't rely solely on Kiro's current session, but on our central knowledge repository.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Reasoning Behind a "Multi-Cloud-Ready" Architecture in an AWS Environment
&lt;/h3&gt;

&lt;p&gt;Although the project lives and breathes in the &lt;strong&gt;Amazon Web Services&lt;/strong&gt; ecosystem, I made a strategic decision from day one: the architecture had to be &lt;strong&gt;Multi-Cloud-Ready&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Many might ask: &lt;em&gt;Why complicate the design if we already have the AWS toolset?&lt;/em&gt; The answer lies in technical sovereignty and cost control. By implementing patterns like the &lt;strong&gt;Service Registry&lt;/strong&gt; and using &lt;strong&gt;Devbox&lt;/strong&gt; to isolate environments, we avoid the dreaded &lt;em&gt;vendor lock-in&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Designing this way forces us to separate &lt;strong&gt;business logic&lt;/strong&gt; from &lt;strong&gt;infrastructure&lt;/strong&gt;. This means that if the community decided tomorrow to migrate part of the workload to another platform or integrate external services, the core of our application would not suffer a technical trauma. It is an architecture designed for freedom—where AWS is our choice of excellence, but not our only possibility.&lt;/p&gt;

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

&lt;p&gt;This project allowed me to experience a new way of working and a new way of thinking about software development. It isn’t about replacing developers; it’s about empowering them—freeing their minds so they can focus on what truly matters: creativity, innovation, and solving complex problems.&lt;/p&gt;

&lt;p&gt;Generative AI is a powerful tool, but it requires a human to guide it, correct it, and refine it. That is what I love most about this experience: the fact that there is a human behind every line of generated code, and that this human has the capacity to learn, evolve, and improve.&lt;/p&gt;

&lt;p&gt;It is not about letting the AI do everything; it is about learning to work alongside it, leveraging its power to do more, be more, and achieve things that previously seemed impossible.&lt;/p&gt;

&lt;p&gt;I hope this journey serves as an inspiration for you to explore new ways of working, thinking, and creating. May it also remind you that the future isn't something that just happens to us—it is something we build together, with tools, with technology, with AI, and with humanity.&lt;/p&gt;

&lt;p&gt;Finally, to all those who have always wanted to develop applications but haven't been able to: now is the time. From this point forward, nothing is impossible. However, do not go into this process blindly; you must do it with a solid foundation. So, it's time to hit the books!&lt;/p&gt;

&lt;p&gt;Thank you for reading this far, and as always, see you next time!&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://registry.cloud.org.bo" rel="noopener noreferrer"&gt;https://registry.cloud.org.bo&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API:&lt;/strong&gt; &lt;a href="https://github.com/awscbba/registry-api" rel="noopener noreferrer"&gt;https://github.com/awscbba/registry-api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FrontEnd:&lt;/strong&gt; &lt;a href="https://github.com/awscbba/registry-frontend" rel="noopener noreferrer"&gt;https://github.com/awscbba/registry-frontend&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; &lt;a href="https://github.com/awscbba/registry-infrastructure" rel="noopener noreferrer"&gt;https://github.com/awscbba/registry-infrastructure&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://github.com/awscbba/registry-documentation" rel="noopener noreferrer"&gt;https://github.com/awscbba/registry-documentation&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ai</category>
      <category>aws</category>
    </item>
    <item>
      <title>El Futuro del Trabajo</title>
      <dc:creator>Sergio D. Rodríguez Inclán</dc:creator>
      <pubDate>Mon, 10 Nov 2025 20:37:38 +0000</pubDate>
      <link>https://dev.to/w4ls3n/el-futuro-del-trabajo-5ccg</link>
      <guid>https://dev.to/w4ls3n/el-futuro-del-trabajo-5ccg</guid>
      <description>&lt;p&gt;&lt;em&gt;English version &lt;strong&gt;&lt;a href="https://blog.walsen.website/posts/the-future-of-work/" rel="noopener noreferrer"&gt;here!&lt;/a&gt;&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Situación actual
&lt;/h2&gt;

&lt;p&gt;En el momento de escribir este artículo, las empresas tecnológicas se encuentran en una fase de transición. Con la llegada de la inteligencia artificial, la forma de producir software cambiará, y el tipo de profesionales requeridos para su construcción también lo hará (si no ha empezado a suceder ya).&lt;/p&gt;

&lt;p&gt;En el ámbito de la informática, los roles profesionales más comunes incluyen: arquitectos y desarrolladores, especialistas en control de calidad (QA), ingenieros de infraestructura, analistas de bases de datos, y todas las variantes relacionadas con estas especialidades.&lt;/p&gt;

&lt;p&gt;Los arquitectos y desarrolladores diseñan e implementan todo el código relacionado con el producto.&lt;/p&gt;

&lt;p&gt;Los especialistas en control de calidad (QA) aseguran que el producto cumpla con los requisitos de calidad.&lt;/p&gt;

&lt;p&gt;Los ingenieros de infraestructura se encargan de la infraestructura de la empresa, abarcando elementos como servidores, bases de datos, automatización en la nube, y un largo etcétera.&lt;/p&gt;

&lt;p&gt;Los analistas de bases de datos se ocupan de estas, incluyendo el diseño de esquemas y la migración de datos, entre otras tareas.&lt;/p&gt;

&lt;p&gt;Obviamente, existen otros roles de carácter más administrativo, pero no nos centraremos en ellos en este análisis.&lt;/p&gt;

&lt;p&gt;La cuestión fundamental es que la IA está redefiniendo la manera en que la industria crea sus productos; el primer y más importante sector afectado es, sin duda, el software. Los ingenieros de software, que hasta hace no mucho tiempo eran muy demandados por su capacidad para producir código y llevarlo a producción, realizaban su trabajo de forma predominantemente artesanal. Aunque siempre se han seguido buenas prácticas y procesos, la mayor parte del software existente ha sido diseñado y programado por cerebros y manos humanas.&lt;/p&gt;

&lt;p&gt;Hoy en día, los modelos de lenguaje de gran escala (LLM, por sus siglas en inglés) han superado a otros modelos de IA anteriores (como BERT) y se han consolidado como la herramienta más potente para la industria del software.&lt;/p&gt;

&lt;p&gt;Además, las técnicas de IA generativa, como el &lt;em&gt;vibe coding&lt;/em&gt; (si es un término específico), los agentes y los servidores MCP (Machine Control Program), combinadas, podrían potencialmente reemplazar una parte considerable del trabajo de desarrollo tradicional que impera hoy en la industria del software.&lt;/p&gt;

&lt;p&gt;Por lo tanto, las empresas tecnológicas, especialmente las de mayor envergadura, han comenzado a reemplazar ingenieros de desarrollo tradicionales por ingenieros especializados en IA.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cuál es la diferencia entre el desarrollo tradicional y el desarrollo con IA?
&lt;/h2&gt;

&lt;p&gt;En pocas palabras, ya no será necesario que un desarrollador cree &lt;strong&gt;una&lt;/strong&gt; API, o que un &lt;strong&gt;especialista DevOps despliegue&lt;/strong&gt; una solución en la nube; un agente con esas capacidades específicas podrá hacerlo.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cómo funcionan los Agentes de IA?
&lt;/h3&gt;

&lt;p&gt;Los agentes de IA son sistemas que combinan modelos de lenguaje (LLM) con herramientas y capacidades de ejecución. A diferencia de un simple chatbot, un agente puede:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Razonar&lt;/strong&gt; sobre qué acciones tomar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ejecutar&lt;/strong&gt; herramientas y comandos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observar&lt;/strong&gt; los resultados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterar&lt;/strong&gt; hasta completar la tarea&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;He aquí un pequeño flujo de ejemplo de despliegue usando un agente de IA.&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%2F75gkieo483o99j3hl1cz.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%2F75gkieo483o99j3hl1cz.png" alt="Despliegue" width="800" height="719"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Componentes clave del sistema
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM (Modelo de Lenguaje)&lt;/strong&gt;: El "cerebro" que razona y toma decisiones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agente&lt;/strong&gt;: Orquestador que coordina entre el LLM y las herramientas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Herramientas&lt;/strong&gt;: APIs, comandos CLI, acceso a archivos, bases de datos, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entorno&lt;/strong&gt;: El sistema real donde se ejecutan las acciones&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Flujo de trabajo
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Entrada del usuario&lt;/strong&gt;: Define la tarea de alto nivel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Razonamiento&lt;/strong&gt;: El LLM analiza qué herramientas usar y en qué orden&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ejecución&lt;/strong&gt;: El agente invoca las herramientas necesarias&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observación&lt;/strong&gt;: Recibe los resultados de cada acción&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteración&lt;/strong&gt;: El LLM decide el siguiente paso basándose en los resultados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Completación&lt;/strong&gt;: Cuando la tarea está terminada, reporta al usuario&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este ciclo de &lt;strong&gt;Razonar → Actuar → Observar&lt;/strong&gt; se repite hasta que el agente completa la tarea o determina que no puede continuar.&lt;/p&gt;

&lt;p&gt;La imagen a continuación ilustra lo que denomino la "torre de la abstracción". Al igual que en todo proceso, existe una evolución natural donde los componentes se especializan progresivamente y su interacción se vuelve más concreta y optimizada.&lt;/p&gt;

&lt;p&gt;Consideremos un ejemplo: el cerebro humano no "sabe" directamente cómo hacer que la mano apriete un objeto. Simplemente decide que existe una situación en la que la acción de apretar es necesaria y envía la señal al órgano especializado, que en este caso son los músculos de la mano. En la imagen, podemos establecer la siguiente analogía: el LLM representa el cerebro, el transporte simboliza los nervios, y cada agente equivale a un músculo específico de la mano.&lt;/p&gt;

&lt;p&gt;De manera similar, en el mundo del software, el proceso evolucionó desde la implementación de un módulo monolítico único hasta la ejecución de tareas específicas en microservicios orquestados por un controlador. Este controlador decide qué microservicio debe ser invocado como resultado de un evento o una condición. Aplicando esta analogía a la imagen, el LLM se correspondería con un &lt;em&gt;Control Plane&lt;/em&gt;, el transporte con el &lt;em&gt;networking&lt;/em&gt; y las rutas, y los agentes con los &lt;em&gt;Pods/containers&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;En esta evolución, el agente de IA determina qué herramientas utilizar y en qué secuencia, mientras que el LLM decide el paso siguiente basándose en los resultados obtenidos.&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%2Faugr3gmso2gbex11t36g.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%2Faugr3gmso2gbex11t36g.png" alt="Torre de la abstracción" width="800" height="929"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;El siguiente paso en el desarrollo tradicional resultó ser, quizás, más inesperado que lógico. Esta evolución de la industria del software (y de muchas otras) se traduce en la delegación del trabajo de desarrollo, implementación, pruebas y despliegue a agentes inteligentes. Estos pueden realizar dicha labor de manera ininterrumpida y con una eficiencia superior a la humana, al consumir menos recursos, dejando así atrás la faceta artesanal del proceso.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Y entonces, qué pasará con...?
&lt;/h2&gt;

&lt;p&gt;Sí, la pregunta es crucial: ¿qué destino aguarda a los ingenieros tradicionales que, día a día, entregan tarea tras tarea?&lt;/p&gt;

&lt;p&gt;Se presentan, entonces, dos alternativas claras: o dejamos que los acontecimientos nos arrollen y el "tsunami" nos sepulte bajo toneladas de agua, o bien, nos reinventamos y adaptamos (una vez más), aprendiendo a navegar el vasto mar de cambios que se avecinan.&lt;/p&gt;

&lt;p&gt;De ahora en adelante, la industria ya no requerirá desarrolladores que elaboren código de principio a fin, ingenieros que desplieguen soluciones completas, o analistas que revisen exhaustivamente el código o la estructura de datos. En cambio, la industria demandará ingenieros capaces de orquestar los agentes ofrecidos por otros proveedores, así como de crear sus propios agentes para los casos específicos de su negocio.&lt;/p&gt;

&lt;p&gt;El trabajo que antes era realizado por equipos de decenas de personas, ahora será llevado a cabo por unos pocos, quienes, sin embargo, producirán lo mismo o incluso más que aquellos "pequeños ejércitos".&lt;/p&gt;

&lt;p&gt;Dado que la creación de productos será mucho más sencilla, es probable que surjan numerosas &lt;em&gt;startups&lt;/em&gt; que, aunque con pocas personas, lograrán un gran impacto y se posicionarán en el mercado. Además, se crearán muchas nuevas posiciones para aquellos que aprendan a interactuar con los agentes y a mejorarlos.&lt;/p&gt;

&lt;p&gt;La educación, sin duda, deberá transformarse también, especialmente la educación superior, que aún forma ingenieros con un perfil "clásico" que saldrán a un mercado que ya no los necesita. Las instituciones de educación superior deben reformar sus currículos ¡con urgencia!&lt;/p&gt;

&lt;p&gt;Del mismo modo, las empresas de software que pretendan retener a sus ingenieros deberán impulsarlos a actualizarse y a realizar esta transición de la forma menos traumática posible. Como ingenieros, nos enorgullecemos de nuestras capacidades, pero en ocasiones mostramos necedad y reticencia, o incluso incredulidad, ante el cambio, sobre todo cuando este parece no favorecernos.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Por dónde empezar?
&lt;/h2&gt;

&lt;p&gt;Como líder de un AWS User Group, uno de mis deberes consiste en guiar a la comunidad hacia este futuro.&lt;/p&gt;

&lt;p&gt;No hay herramienta más poderosa que la educación. Por lo tanto, mi primer consejo sería buscar las certificaciones relacionadas con el tema, creadas y diseñadas por las mismas empresas que están liderando esta transición. Personalmente, considero que no hay mejor incentivo para estudiar que una certificación, ya que el conocimiento que exige no solo es teórico, sino que abarca también el dominio práctico del día a día; en consecuencia, quien no posea esa experiencia se verá impulsado a adquirirla por medios propios.&lt;/p&gt;

&lt;p&gt;La primera y más importante certificación para iniciar este camino sería la "&lt;a href="https://aws.amazon.com/es/certification/certified-ai-practitioner/" rel="noopener noreferrer"&gt;AWS Certified AI Practitioner&lt;/a&gt;", donde se abordan los fundamentos: qué es un LLM, qué es una base de datos vectorial, qué es un agente, cómo crear &lt;em&gt;prompts&lt;/em&gt; efectivos para interactuar con un agente de IA, entre otros aspectos. Dado que es una certificación de AWS, requiere el conocimiento básico, al menos, de muchos de sus servicios y su configuración mínima. Por lo tanto, revisar el material de la certificación "&lt;a href="https://aws.amazon.com/es/certification/certified-cloud-practitioner/" rel="noopener noreferrer"&gt;AWS Certified Cloud Practitioner&lt;/a&gt;" resulta también muy beneficioso; y, por supuesto, si uno se anima a obtenerla, tanto mejor.&lt;/p&gt;

&lt;p&gt;Una vez obtenida la primera certificación, se puede comenzar a crear agentes de IA y, de este modo, avanzar paulatinamente en la transición.&lt;/p&gt;

&lt;p&gt;La primera opción es un servicio completamente administrado denominado &lt;a href="https://aws.amazon.com/es/bedrock/" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt;, que integra numerosos LLM y ofrece herramientas para crear agentes, integrar modelos, etc. &lt;a href="https://aws.amazon.com/es/blogs/machine-learning/category/artificial-intelligence/amazon-machine-learning/amazon-bedrock/" rel="noopener noreferrer"&gt;Aquí&lt;/a&gt; puede consultarse una lista exhaustiva de artículos relacionados con Amazon Bedrock.&lt;/p&gt;

&lt;p&gt;AWS también ofrece un &lt;em&gt;framework&lt;/em&gt; de código abierto llamado &lt;a href="https://strandsagents.com/latest/" rel="noopener noreferrer"&gt;Strands Agents&lt;/a&gt;, orientado a código (actualmente en Python) y de uso sencillo. Su repositorio en &lt;a href="https://github.com/strands-agents" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; contiene excelentes ejemplos para iniciarse, destacando particularmente el &lt;a href="https://github.com/strands-agents/samples/tree/main/02-samples/05-personal-assistant" rel="noopener noreferrer"&gt;asistente personal&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%2F0v7gbvaptwrkhj5btcaf.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%2F0v7gbvaptwrkhj5btcaf.png" alt="Agentic Personal Assistant" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Este ejemplo emplea varios agentes integrados entre sí, como los de búsqueda de información, imágenes y videos, entre otros; así como un agente principal encargado de responder a las preguntas del usuario. Constituye un ejemplo muy didáctico para comprender las capacidades de Strands en la creación de agentes.&lt;/p&gt;

&lt;p&gt;Una vez adquirida la experiencia necesaria, se podría optar a la certificación "&lt;a href="https://aws.amazon.com/es/certification/certified-generative-ai-developer-professional/" rel="noopener noreferrer"&gt;AWS Certified AI Developer Professional&lt;/a&gt;", la cual, en el momento de redactar este artículo, se encuentra en fase &lt;em&gt;beta&lt;/em&gt;. Obtenida esta certificación de nivel profesional, se podrá acreditar la proficiencia en el desarrollo de aplicaciones con y orientadas a la IA.&lt;/p&gt;

&lt;p&gt;Aunque esta certificación, a pesar de ser de nivel profesional, no exige ningún requisito previo para su realización, el sentir de la comunidad sugiere que las siguientes son certificaciones ideales como preparación:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://aws.amazon.com/es/certification/certified-ai-practitioner/" rel="noopener noreferrer"&gt;AWS Certified AI Practitioner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://aws.amazon.com/es/certification/certified-solutions-architect-associate/" rel="noopener noreferrer"&gt;AWS Certified Solutions Architect - Associate&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://aws.amazon.com/es/certification/certified-machine-learning-engineer-associate/" rel="noopener noreferrer"&gt;AWS Certified Machine Learning Engineer - Associate&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://aws.amazon.com/es/certification/certified-developer-associate/" rel="noopener noreferrer"&gt;AWS Certified Developer - Associate&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El tiempo dirá si es así.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Si hay algo seguro en &lt;strong&gt;esta&lt;/strong&gt; vida es que todo cambia. &lt;strong&gt;Por lo tanto, todos&lt;/strong&gt; debemos estar preparados para los cambios que inevitablemente llegarán.&lt;/p&gt;

&lt;p&gt;Personalmente, he comenzado mi &lt;strong&gt;propio&lt;/strong&gt; camino de transición y debo confesar que lo disfruto enormemente. Siempre fui un "Developer Wannabe" que no había encontrado el tiempo ni el coraje para concretar sus aspiraciones. Sin embargo, ahora puedo hacerlo sin la necesidad de invertir tanto tiempo profundizando en programación, y con la capacidad de crear aplicaciones funcionales en un tiempo significativamente menor. Sé que mis estimados compañeros desarrolladores critican el &lt;em&gt;vibe coding&lt;/em&gt; con vehemencia, pero es imperativo despojarse de prejuicios, reaprender y abrir la mente, porque este cambio, que ya ha comenzado, no se detendrá, le pese a quien le pese.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>cloud</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
