Code Structure For An AI Generated Infrastructure Deployment Script
How you structure your code makes it more maintainable and possibly consumes less tokens
This is part of my series of blog posts on creating an AWS Bootstrap Script to set up secure AI agent infrastructure.
In the last post I explained My Methodology For Writing an Infrastructure Script With AI Agents
Overview
I wrote about the general methodology I’m using to write my bootstrap script in the prior post. Now I’m going to expand a bit more on the structure of the code and why it matters. I also cover some software engineering principles that vibe coders with no prior software engineering experience may want to know.
Organizing your code so an agent can quickly find it
Menus and file names in bash
Protecting critical code From unwanted revisions
Agents are really bad at the DRY principle
Linked lists vs. numbered list
Measuring lines of code written with AI
Consistent user interface (UI) challenges
Organizing your code so an agent can find it
In the olden days (like a year ago) when you wrote software you thought about how you were going to organize the files before you started. The reason you think about the structure of your files and code doesn’t have to do with making things work. It has to do with the maintainability of your software.
Maintainability has to do with being able to modify your software in the future and not break everything. You also want to be able to extend your software to add new features without rewriting the whole thing.
If you write one big blob of code in one file, then whenever you change code you risk messing up the whole application. Back in the day people used GOTO statements to create what is sometimes referred to as spaghetti code where the application jumps around from one line to another using things like if then statements.
Here’s how a goto works in concept:
If this…then jump to line 634.A real example in bash:
# 1. Define your "label" as a function
goto_finish() {
echo "Jumped straight to the finish line!"
exit 0
}
echo "Starting the script..."
# 2. Trigger the "goto" jump
goto_finish
echo "This line will be completely skipped."That turned out to be a very problematic approach when it came to troubleshooting bugs and maintaining software. After that new constructs came along like functions, classes, and objects. I’ll let you read up on those so you can learn about proper software architecture.
Here’s an example of a bash function:
# Define the function
say_hello() {
echo "Hello, World!"
}
# Call the function
say_helloHere’s an example of a Python class and object:
class User:
def __init__(self, name, role):
self.name = name # Property 1
self.role = role # Property 2
# A method (function inside an object)
def say_hello(self):
return f"Hello, my name is {self.name} and I am a {self.role}."
# Create the object (Instance)
user_object = User(name="Alice", role="Developer")
# Access properties and methods
print(user_object.name)
print(user_object.say_hello())And beyond that architects came up with design patterns or common patterns for object and class definitions that worked for different use cases. Some people knock patterns because they can become overly complex. That is usually because people haven’t dealt with the problems that brought them into existence. They can be over-used and overly complex for basic use cases but in general, most of them do have a reason for being.
All that said, I’m building a bash script. A simple script runs once or maybe a few times to define my architecture. I have some things in it I might move out later but for this initial proof of concept (POC) I’m using the simplest, fastest method to get from A to B. Bash. Nothing to install. Fast. Lightweight. Not robust enough for a production website but good enough for my current use case and prototypes.
In bash (or any language really) I find it easiest to put code related to a single piece of functionality in a single file. That’s it. Call it a function, an object or whatever you want in other languages but putting related code into a file with a specific name has the benefit of making it easy to identify later when you are trying to modify the code.
And guess what - that makes it easier for agents too. Here’s the other benefit. From what I’m reading (an as of yet unproven theory) it helps agents only find and load what they really need to make a change. That means they need to load less into context, read less, and use less tokens. And if they don’t, they should. I hope some of the model providers read this because token usage is, as of now, far too inefficient.
I was reading something about file pointers and giving files funny short names (presumably to save on tokens) to improve memory usage and all that jazz. But if you name a file f134.sh then the model doesn’t know what the file is used for right?
What if you name your file say_hello.sh and you have a menu item 1. Say Hello 2. Say Goodbye. When your code processes 1. Say Hello then it goes to say_hello.sh. Doesn’t that just make sense and help the agent quickly find the code it needs to modify? Won’t the model use less tokens by easily identifying what it needs to edit if the names make sense? I don’t really know but it’s easier for me to understand what’s in the file and if the agent is doing what I want if I give files sensible names. They are still pointers, but pointers with names that include context.
In addition, I structure my readme so each section relates to one file. The definition of what goes into that file includes the file name and everything lines up structurally so the agent has to do less work to figure out what it’s supposed to edit and what goes into that file. I have no idea if my theories work or not but they should, if they don’t.
Agents seem to be good at pattern matching and structures that repeat. When I’m trying to create a structure and I have some one off thing that doesn’t match all the other things the agent keeps trying to write it to match the most common pattern. So why not structure your application with a common pattern that makes it easy for the agent to replicate and extend the common pattern? That’s what I do.
Menus and file names in bash
I’m deploying a whole bunch of resources in my script but I don’t want to write some huge monolithic script that has to run all at once for the same reasons I write CloudFormation as micro-templates. If you want to understand more about the reasoning behind that and why it matters check out this post:
https://medium.com/cloud-security/cloudformation-micro-templates-ae70236ae2d1
I create a menu with a numbered list like this:
OU
Accounts
Move accounts to OU
Organization policy
etc…
Each resource has its own section in the readme, its own file (or set of files as I’ll get to in a minute but one primary driving file) and each menu item relates to that main resource file.
Due to the way bash works I have landed on the method that works best for me. Some may argue it’s bad and I should be using functions but bash is wonky. That’s the best term for it. I get the most consistency when I SOURCE the files for reasons having to do with variables and error messages and issues with arguments passed to functions that contain spaces and such.
Here’s an example of a file I want to source named say_hello.sh
# say_hello.sh
USER_NAME="Alice"
greet_user() {
echo "Welcome back, $USER_NAME!"
}Here’s where I’m source it - at the point it is sourced that code will be executed:
# Main terminal or script
source ./say_hello.sh
# Both the variable and function are now available
echo $USER_NAME
greet_userWhen a user chooses a menu item it sources the related file, the code executes, and it deploys the resource.
Here are some of the key benefits to that approach:
You can just run the whole thing end to end
You can jump to the middle and only deploy what you want
You can REDEPLOY only what you want (which is the whole problem with monolithic CloudFormation templates by the way).
You can give different users with different permission the same script but they can only deploy the resources they have permission to deploy (see protecting critical code below for more on that) which enables separation of duties if you need that.
Separation of duties is something larger companies will use to require multiple people to make a critical change or access data. Financial institutions may require two people to allow certain financial transactions, for example. A large organization may have different people in charge of encryption, networking, and IAM such as was the case at a large bank where I worked.
This script supports that multi-admin use case while also supporting my case where I’m one person using my organizational role (the first time) to deploy all the base infrastructure. After I deploy it one time I can break it up into separate roles but while I’m testing it and deploying the first base infrastructure I can do it all with one role. In other words the structure of my menus and files has made this whole thing very flexible.
In addition to the menu of resources I’ve added few other nice things like:
Filtering the menu items based on letters and executing actions on number pushes so you can find what you’re looking for more easily
After a resource is deployed you can deploy the next one or go back to the menu to choose something else
Some resources have sub menus like which region you want to deploy the resource in, when applicable, and which specific resources you want to deploy
Some resources have list/add/remove functionality if resources already exist for a particular resource type. I may add that to all later.
There are various other submenus I’m probably forgetting here but you get the idea.
The menu is in a run.sh or similar script that drives the menu and sources all the other files. I have to keep reminding the agent even though it’s in my requirements and is the overall pattern not to put resource logic into run.sh. That said, I also have to reiterate not to duplicate code. Sometimes I have to refactor the repetitive code it has written for every single resource to work for all resources with a variable, for example.
I’ve used this menu approach in bash for a while now and it has worked very well. I can even create a dynamic menu in some applications by naming the files the same name as the menu item. In addition, instead of hard coding the numbers, I have the menu read all the resources and dynamically generate the menu. That’s related to the section on linked lists versus numbers below.
Protecting critical code from unwanted revisions
I have some code that assumes an AWS role using an AWS CLI profile. I’ve written about assuming AWS roles many times over. I have sample scripts in GitHub that assume roles with MFA which you can find here:
As mentioned this bootstrap script was written in such a way that you can let the user define the AWS CLI profile and thereby the AWS role they want to assume that is allowed to deploy the selected resources. When the user runs the script it asks for the environment for which they are running the script and the AWS profile they want to use to run the deployment. The user-entered configuration is stored in a file so they don’t have to re-enter that information every time. Note that it only stores the AWS CLI profile not any credentials so those do NOT get checked into GitHub.
Never check in credentials to GitHub or any source control repository.
Beyond that I set up my roles so that they all require MFA and can be used from a specific IP range in the role trust policy in AWS - something I’ve written about many times before. Even if an attacker did get the credentials they wouldn’t be able to use the AWS access keys without meeting those other two restrictions.
The user can configure the AWS CLI role profile using aws configure separately from this script or the script can guide them through the process. Once the AWS CLI profiles are configured, the script shows the user a list of profiles on the system and lets them pick one to use during the deployment process. On the next run the user can use the configuration or change it if they need to use a different AWS CLI profile.
That role configuration code and selecting which profile to use to execute the script is core to all the security of roles and credentials. I don’t want it to be changed in some way that does not enforce MFA or that stores the credentials in GitHub somehow or leaks them in some way. I’m writing a ton of code here and that bit is so critical that I moved it to a separate project.
The Kiro CLI agent I’m using to write the bootstrap script doesn’t have permission to change that code, though it can read it. What that also does is frees up that block of code to be reused in multiple projects so I’m not rewriting critical authentication code over and over again. Once I know it’s correct I don’t have to touch it again. I can write new bash scripts that take actions on AWS and reuse that code.
I’ve used that strategy many times over with critical code I want to share with other people to use, but not change or rewrite or introduce a bug into it. No matter how careful you are, things happen. I once helped architect a new version of a tax system that was off by over $300,000 a month for a large fortune 1000 retail company. We moved the multi-threaded code which is very hard to write and easy to mess up into a completely separate component that no one needed to touch if they were making changes to the code that handled the rules for calculating tax.
And even though I was so careful and had one of the best QA professionals I’ve ever worked with on that project, one developer made one change for some unknown reason that caused the tax calculation to be off by $23 on the first run. Comparatively speaking it was still a massive improvement and upper management claimed that it was inconsequential but I was still pissed off for lack of more politically correct words. I fixed it. Perfect. No errors. Exactly balanced. You’d love to just trust everyone but whether intentionally or not people (and AI agents) will mess stuff up. So make sure they can’t if it is something critical.
Side note: Write unit tests for all your calculations that fail if someone changes the code in a way that causes the calculation to produce an incorrect value. But that is a massive topic for another post. I’m not using unit tests for this project at the moment. They would actually be more like integration tests because I would need to access AWS to see if the correct resources got deployed or not. I’m doing that manually for now.
Variables
I try to reinforce to the agent to always put all variables into a common file sourced at the beginning of the script. It keeps forgetting. But by putting all my variables into one file, the agent doesn’t end up creating duplicate variables and cause me to get hard to troubleshoot errors because the agent is mixed up on which variable to use where.
I have it track certain user defined values in a config file. The problem there was that the agent started putting things it should be querying dynamically from the AWS environment into the config file and loading that into the variables. Then it ended up using stale data as things changed and the config file wasn’t updated. I had to reiterate numerous times to query the AWS resources dynamically and not pull them from the config file. This is one of the reasons I think Terraform’s drift detection “speed up” with a cache is somewhat problematic but I haven’t actually used it. Test it out for yourself.
Functions
I do use functions for some common constructs as long as they do not end up hiding errors in some way that makes things difficult to troubleshoot. They are usually simple, well defined code that doesn’t get error messages from AWS. Pure bash constructs that have stood the test of time are put into reusable functions so as not to duplicate code unnecessarily.
I put each function into its own file in a functions directory. The beauty of that method of organizing files is that I can source those functions all at once one time at the beginning of my script. You can argue about lazy loading or whatever but sometimes all your tricky concepts create more bugs than they are worth. This is clean and simple and any time I add a new function it is automatically included.
FUNCTIONS_DIR="./functions"
# Loop through and source every shell script in that directory
for file in "$FUNCTIONS_DIR"/*.sh; do
# Check if the file actually exists to prevent errors if the folder is empty
if [ -f "$file" ]; then
source "$file"
fiAgents are really bad at the DRY principle
The first way companies tried to measure how brilliantly agents were writing code and how much value they were adding was by boasting about the number of lines written. I have already explained over and over again that is a horrible metric.
Here’s one thing agents are really, really bad at right now. The DRY principle.
Here are all the reasons pre-AI you should care about the DRY principle (and what it is if you are not familiar):
https://medium.com/cloud-security/dry-dont-repeat-yourself-30e7a582ea4
Now that we are using AI agents the reason you should care about it is the amount of tokens your AI agent has to use to read all your code, memory, context and all the other things driving costs up for AI agents.
I’ve seen agents start to try to fix this by writing functions but sometimes the functions in bash are more trouble than they are worth. I’m constantly telling AI agents not to write code that hides errors - which they are also prone to do. Why? Because simply hiding an error instead of fixing it makes it look like the code worked when it didn’t. Success! Right.
But for the most part agents still write way too much duplicate code. I’m constantly fixing this though I’m sure I’ve still missed it in a lot of places. Here’s an example.
When I write code to deploy an S3 bucket really there’s one simple way to do that without much variation. You might use this code, for example:
aws s3api create-bucket \
--bucket my-unique-bucket-name-2026 \
--region us-east-1You tell the agent to write five buckets and you may end up with something like this - especially as you are adding new buckets over time (it does tend to get it right if you give it all the buckets at once):
aws s3api create-bucket --bucket bucket1 --region us-east-1
aws s3api create-bucket --bucket bucket2 --region us-east-1
aws s3api create-bucket --bucket bucket3 --region us-east-1
aws s3api create-bucket --bucket bucket4 --region us-east-1
aws s3api create-bucket --bucket bucket5 --region us-east-1
Well what I want is one create_bucket.sh script (this is not the exact code but you get the idea):
# create_bucket.sh
aws s3api create-bucket --bucket "$TARGET_BUCKET" --region "us-east-1"
Then I can set a variable and source that script for ANY bucket regardless of the name. I actually have more complex variables like encrypt the bucket or not, the KMS key ARN (after created), log bucket, log retention values, etc. etc. So I can have a very flexible way to create a bucket using whatever variables are set.
Now in my script there’s not a separate number in the menu for each bucket it’s just Create Buckets. What I have there is a submenu where the user can select some or all of the buckets to deploy. So I could have a create_buckets.sh script that sets the variables for each bucket and then deploys it. But really what I want is a separate script for each bucket. So I can have a list of files matching bucket names with the specific bucket variables set for each bucket in that script and the create_bucket.sh file can generate the menu from all the file names for the corresponding buckets.
File 1: cloud-sync-alpha-87391.sh
#!/bin/bash
export TARGET_BUCKET="cloud-sync-alpha-87391"
source ./create_bucket.shFile 2: data-stream-beta-44205.sh
#!/bin/bash
export TARGET_BUCKET="data-stream-beta-44205"
source ./create_bucket.shFile 3: nexus-vault-gamma-10652.sh
#!/bin/bash
export TARGET_BUCKET="nexus-vault-gamma-10652"
source ./create_bucket.shIn other words there’s one script corresponding to the outer menu and scripts corresponding to the bucket list menu. That means each bucket can have a unique configuration in a file name matching the button name and I can add and remove buckets without modifying the whole program if the agent has written the code correctly and maintained it that way. It should also be able to find the code it needs to modify very quickly based on that pattern. It can generate a list of buckets to deploy based on the file names.
Linked lists versus numbered lists
One of the problems with numbered lists is that when you add in a new item you have to renumber all the items in the list after that number. This is kind of a problem in my requirements and so generally I tell the agent to not add references to other requirements by number in my README or in my code comments. I don’t know why it keeps trying to do that. I also tend to try to get it to just add new requirements sections or items to the end. I don’t really care about the order as long as all the requirements are met.
When creating menu items I was having the same problem. In my example of deploying resources or deploying buckets after choosing to Create Buckets from the resources menu, if I insert a new item in the middle of the list it was trying to renumber the entire menu. It also ended up having mismatched requirement references and numbers in the requirements themselves. This is where some software architecture and understanding the problem and how to solve it helps.
Read up on the concept of linked lists if you are not familiar. With a linked list you can insert something into the middle of a list without repositioning everything else in the list. You don’t care about the actual numbered position of the items. You just care about which item comes before the next. So for example if I have a list like this:
Deploy OU
Deploy Accounts
Deploy S3 buckets
Now I want to add Deploy Organization Policy at the top. I have to renumber all the other items. Four changes.
Deploy Organization Policy
Deploy OU
Deploy Accounts
Deploy S3 buckets
Anything referencing those numbers is now messed up. The agent sometimes forgets to renumber things or renumbers them wrong. If I was using the position of the resource instead of file names for pointers to the code that needs to be executed to deploy that resource that would be messed up too.
Instead all I really care about is the order.
OU
Accounts comes after OU.
S3 buckets comes after Accounts.
Now insert Organizational Policy so it comes before Accounts. Two changes.
Accounts
OU comes after accounts
That’s it. The other two bullet points don’t need to change. And in a very long list you can imagine that’s much more efficient as I don’t have to rewrite the whole list every time.
So basically in my readme I list out the order of the resources like that without numbers. I tell it to deploy the resources in a certain order.
Then I changed the numbering to dynamically generate the numbers as the resources are displayed. Accessing the files is not dependent on those numbers or ordering it doesn’t matter. The only thing that matters is that when a user enters a number it finds the dynamic association between that number and a menu item.
Measuring lines of code written with AI
That last change alone which refactored the menu code with a better software architecture that is more efficient and less prone to errors removed 1700 lines of code. I’m doing stuff like that all the time so how do you take that into account in your code counting AI measurement algorithm? Do I get negative bonus points for writing less lines of code? Do you subtract from my efficiency because I don’t have to write or change as many lines of code every time I add a new menu item?
Please show this post to any executives evaluating developers based on how many lines of code they write with AI.
More lines of code often means more potential bugs and more places where things can go wrong. You don’t want the code to be so efficient you can’t read it like minified websites (or it needs to be converted to something humans can evaluate potentially if we do go in that direction) but you also don’t want to blindly assume more lines of code equates to better code and more output. People will be just gaming that system and creating a big mess.
You also will likely have more security problems that are harder to solve in the end as I explained in this post.
https://medium.com/cloud-security/every-line-of-code-is-a-potential-bug-49108a0d8045
Consistent user interface (UI) challenges
I was taking a look at an application written by someone with pretty much no programming experience written with AI and honestly it was impressive. There’s no doubt that it is truly impressive that someone with so little experience in the field can produce something like that. AI is amazing from that regard and for any naysayers on this matter - you’re wrong.
The only thing we have to figure out is how to train people with the software architecture and engineering (and security!) skills to understand how to use AI properly so they don’t create applications that are going to create more risk than the problems they solve. That will likely take some time.
But here’s one simple thing I noticed right off the bat. The application had buttons in all shapes and sizes. This is a common problem in AI generated code. It has problems with spacing and consistency. Even the AWS Kiro CLI I’m using (sans TUI) has a box that is a bit off where the right line on the intro box is jagged and misaligned when deployed on an EC2 Amazon Linux instance.
This is where some understanding of the above software principles will help. I had a similar problem building a UI in bash to manage my agents. To fix that problem I created a file that generates a button, similar to how I created a file to deploy my buckets above. I would set a variable with the button name before sourcing the script. Then the creation of the button was consistent and produced the exact same button every time but with a different name based on the variable.
Things like that may seem very basic to anyone who has programmed for a while. But imagine someone who has never written any code whatsoever and says “write me a program to do X.” That’s what is happening now. And what they produce is actually GOOD in a lot of ways. But when people program applications they have no idea what’s under the hood or how to fix the problems or all the ways it can be compromised if they push it to a web server.
Does it matter? Does it matter that you don’t know the underlying data structure that produces a Word or PDF document? We don’t want to stop these people from producing code. That’s not the answer. We need to help people learn how to produce good code. That’s the challenge. This post is just the tip of the iceberg of knowledge that somehow needs to be instilled into agents or put into deterministic guardrails to produce Clean Code such as was written about in one of the many books I read on software engineering and architecture when I started programming.
The book is now on its second edition, updated in 2025. In this day and age of all things AI, how many people are still reading books? Do we ask them to read this book or do we somehow bake the principles into the code that is being produced? Easier said than done, but a goal nonetheless.
Subscribe for more posts like this and follow Good Vibes.
This is part of my series of blog posts on creating an AWS Bootstrap Script to set up secure AI agent infrastructure.






