Using Kiro specs to build IaC out of a shell script

In this post, I am continuing my Kiro experiments to produce better specs outcomes. I am doing so using my demo app repository (https://github.com/aws-containers/votingapp). This is the same application I have used in my previous blog post Using Q CLI to validate the implementation of Kiros specs.

For background, when I built this application I was in a bit of a rush and I did not have time to build a proper IaC setup for the pre-requsites of its deployment (including the DynamoDB table and its initialization). Particularly the initialization of the table with starting values for the counts of vote was challenging as it required custom resources (e.g. AWS Lambda functions) in a CFN template. Eventually -and embarrassingly- I resorted to authoring a quick shell script that would call the AWS CLI to do most of this setup work for the prerequisites. You can look at the script as it exists today here.

Four years in, as lazy as I am, that script is still there (hello tech debt!) and it's one of the main reasons why this demo can't really be used more broadly. A few months ago, I (briefly) tried to vibe code a solution out of this situation but I just gave up as I was not getting where I wanted to go (i.e. a single CFN template that does everything the script and its surroundings files do). I will admit that I could use more conviction there so it's not just vibe coding limitations, it was me being lazy to keep it on track towards my goal.

I have decided to attack the problem using Kiro and the specs workflow. But, in the spirit of increasing the odds of making the generation of IaC more successful, I wanted to introduce (or force) specific steps in my implementation plan that would guarantee higher odds of a successful IaC template deployment.

This is the prompt that I used to trigger the specs creation:

Create a Cloudformation template that implements all the requirements for running the application. Look into the /preparation folder to learn what this application requires and turn those files and the script into a single CFN template that does all that in a single stack. You should iteratively test that the deployment works properly and the application can start, connect to dynamodb. The validation should be that you can query the getvotes endpoint and verify that it works. You have access to a local AWS profile named "default" that gives you access to an account where you can deploy the stack. Do not validate the CFN template simply with local tests and mocks. Make sure you actually deploy it to the AWS account and you test the application against that backend.

The following is a graphical representation of the workflow I have forced:

In the Appendix A you can find the specs Kiro produced. Below you can find the CloudFormation template Kiro generated at the end of the specs workflow:

  1AWSTemplateFormatVersion: '2010-09-09'
  2Description: 'CloudFormation template for Voting App infrastructure - Creates DynamoDB table, IAM roles, and policies for App Runner deployment'
  3
  4# ============================================================================
  5# VOTING APP INFRASTRUCTURE TEMPLATE
  6# ============================================================================
  7#
  8# This CloudFormation template creates all necessary AWS infrastructure for
  9# the Voting App, a simple REST API service for collecting restaurant votes.
 10#
 11# WHAT THIS TEMPLATE CREATES:
 12# - DynamoDB table with PAY_PER_REQUEST billing for restaurant vote storage
 13# - IAM role with least-privilege access for App Runner service
 14# - Custom IAM policy for DynamoDB table access
 15# - Lambda function for seeding initial restaurant data
 16# - CloudFormation custom resource for automated data initialization
 17#
 18# DEPLOYMENT INSTRUCTIONS:
 19# 1. Deploy this template using AWS CLI or Console:
 20#    aws cloudformation create-stack --stack-name votingapp-infrastructure \
 21#      --template-body file://cloudformation-template.yaml \
 22#      --capabilities CAPABILITY_NAMED_IAM
 23#
 24# 2. Wait for stack creation to complete:
 25#    aws cloudformation wait stack-create-complete --stack-name votingapp-infrastructure
 26#
 27# 3. Get stack outputs for App Runner configuration:
 28#    aws cloudformation describe-stacks --stack-name votingapp-infrastructure \
 29#      --query 'Stacks[0].Outputs'
 30#
 31# REQUIRED PERMISSIONS:
 32# The deploying user/role must have permissions to:
 33# - Create/manage DynamoDB tables
 34# - Create/manage IAM roles and policies
 35# - Create/manage Lambda functions
 36# - Create/manage CloudFormation stacks
 37#
 38# CUSTOMIZATION:
 39# - Modify Parameters section to change default values
 40# - Update Environment parameter for different deployment stages
 41# - Adjust DynamoDB table configuration as needed
 42#
 43# CLEANUP:
 44# To remove all resources:
 45# aws cloudformation delete-stack --stack-name votingapp-infrastructure
 46#
 47# ============================================================================
 48
 49Metadata:
 50  AWS::CloudFormation::Interface:
 51    ParameterGroups:
 52      - Label:
 53          default: "Application Configuration"
 54        Parameters:
 55          - TableName
 56          - RoleName
 57          - Environment
 58    ParameterLabels:
 59      TableName:
 60        default: "DynamoDB Table Name"
 61      RoleName:
 62        default: "IAM Role Name"
 63      Environment:
 64        default: "Environment Tag"
 65
 66Parameters:
 67  TableName:
 68    Type: String
 69    Default: 'votingapp-restaurants'
 70    Description: 'Name of the DynamoDB table for storing restaurant votes'
 71    AllowedPattern: '[a-zA-Z0-9_.-]+'
 72    ConstraintDescription: 'Table name must contain only alphanumeric characters, hyphens, underscores, and periods'
 73    
 74  RoleName:
 75    Type: String
 76    Default: 'votingapp-role'
 77    Description: 'Name of the IAM role for App Runner service'
 78    AllowedPattern: '[a-zA-Z0-9_+=,.@-]+'
 79    ConstraintDescription: 'Role name must contain only alphanumeric characters and valid IAM role name characters'
 80    
 81  Environment:
 82    Type: String
 83    Default: 'dev'
 84    Description: 'Environment tag for resource identification and management'
 85    AllowedValues:
 86      - dev
 87      - staging
 88      - prod
 89    ConstraintDescription: 'Environment must be dev, staging, or prod'
 90
 91Resources:
 92  # DynamoDB Table for storing restaurant votes
 93  DynamoDBTable:
 94    Type: AWS::DynamoDB::Table
 95    Properties:
 96      TableName: !Ref TableName
 97      AttributeDefinitions:
 98        - AttributeName: name
 99          AttributeType: S
100      KeySchema:
101        - AttributeName: name
102          KeyType: HASH
103      BillingMode: PAY_PER_REQUEST
104      Tags:
105        - Key: Environment
106          Value: !Ref Environment
107        - Key: Application
108          Value: 'votingapp'
109        - Key: ManagedBy
110          Value: 'CloudFormation'
111      PointInTimeRecoverySpecification:
112        PointInTimeRecoveryEnabled: true
113
114  # IAM Role for App Runner service
115  AppRunnerInstanceRole:
116    Type: AWS::IAM::Role
117    Properties:
118      RoleName: !Ref RoleName
119      AssumeRolePolicyDocument:
120        Version: '2012-10-17'
121        Statement:
122          - Effect: Allow
123            Principal:
124              Service: tasks.apprunner.amazonaws.com
125            Action: sts:AssumeRole
126      ManagedPolicyArns:
127        - 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess'
128      Tags:
129        - Key: Environment
130          Value: !Ref Environment
131        - Key: Application
132          Value: 'votingapp'
133        - Key: ManagedBy
134          Value: 'CloudFormation'
135
136  # Custom DynamoDB Policy with least-privilege access
137  DynamoDBAccessPolicy:
138    Type: AWS::IAM::Policy
139    Properties:
140      PolicyName: 'votingapp-ddb-policy'
141      PolicyDocument:
142        Version: '2012-10-17'
143        Statement:
144          - Sid: 'DynamoDBTableAccess'
145            Effect: Allow
146            Action:
147              - 'dynamodb:GetItem'
148              - 'dynamodb:PutItem'
149              - 'dynamodb:UpdateItem'
150              - 'dynamodb:DeleteItem'
151              - 'dynamodb:Query'
152              - 'dynamodb:Scan'
153              - 'dynamodb:BatchGetItem'
154              - 'dynamodb:BatchWriteItem'
155            Resource: !GetAtt DynamoDBTable.Arn
156      Roles:
157        - !Ref AppRunnerInstanceRole
158    DependsOn:
159      - AppRunnerInstanceRole
160      - DynamoDBTable
161
162  # Lambda execution role for data seeding function
163  LambdaExecutionRole:
164    Type: AWS::IAM::Role
165    Properties:
166      RoleName: !Sub '${RoleName}-lambda-execution'
167      AssumeRolePolicyDocument:
168        Version: '2012-10-17'
169        Statement:
170          - Effect: Allow
171            Principal:
172              Service: lambda.amazonaws.com
173            Action: sts:AssumeRole
174      ManagedPolicyArns:
175        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
176      Tags:
177        - Key: Environment
178          Value: !Ref Environment
179        - Key: Application
180          Value: 'votingapp'
181        - Key: ManagedBy
182          Value: 'CloudFormation'
183
184  # Lambda policy for DynamoDB write access during data seeding
185  LambdaDynamoDBPolicy:
186    Type: AWS::IAM::Policy
187    Properties:
188      PolicyName: 'lambda-ddb-seeding-policy'
189      PolicyDocument:
190        Version: '2012-10-17'
191        Statement:
192          - Sid: 'DynamoDBWriteAccess'
193            Effect: Allow
194            Action:
195              - 'dynamodb:PutItem'
196              - 'dynamodb:GetItem'
197              - 'dynamodb:BatchWriteItem'
198              - 'dynamodb:DescribeTable'
199            Resource: !GetAtt DynamoDBTable.Arn
200      Roles:
201        - !Ref LambdaExecutionRole
202    DependsOn:
203      - LambdaExecutionRole
204      - DynamoDBTable
205
206  # Lambda function for seeding DynamoDB with initial restaurant data
207  DataSeedingFunction:
208    Type: AWS::Lambda::Function
209    Properties:
210      FunctionName: !Sub '${AWS::StackName}-data-seeding'
211      Runtime: python3.9
212      Handler: index.handler
213      Role: !GetAtt LambdaExecutionRole.Arn
214      Timeout: 60
215      MemorySize: 128
216      Description: 'Seeds DynamoDB table with initial restaurant data for voting app'
217      Code:
218        ZipFile: |
219          import json
220          import boto3
221          import logging
222          import cfnresponse
223          from botocore.exceptions import ClientError
224
225          # Configure logging
226          logger = logging.getLogger()
227          logger.setLevel(logging.INFO)
228
229          def handler(event, context):
230              """
231              Lambda function to seed DynamoDB table with initial restaurant data.
232              Handles CloudFormation custom resource lifecycle events.
233              """
234              logger.info(f"Received event: {json.dumps(event, default=str)}")
235              
236              try:
237                  # Extract parameters from CloudFormation event
238                  request_type = event['RequestType']
239                  resource_properties = event.get('ResourceProperties', {})
240                  table_name = resource_properties.get('TableName')
241                  
242                  if not table_name:
243                      raise ValueError("TableName is required in ResourceProperties")
244                  
245                  logger.info(f"Request type: {request_type}, Table name: {table_name}")
246                  
247                  if request_type == 'Create':
248                      seed_data(table_name)
249                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {
250                          'Message': 'Successfully seeded DynamoDB table with restaurant data',
251                          'TableName': table_name,
252                          'RestaurantsSeeded': 4
253                      })
254                  elif request_type == 'Update':
255                      # For updates, check if table name changed and handle accordingly
256                      old_properties = event.get('OldResourceProperties', {})
257                      old_table_name = old_properties.get('TableName')
258                      
259                      if old_table_name != table_name:
260                          logger.info(f"Table name changed from {old_table_name} to {table_name}, seeding new table")
261                          seed_data(table_name)
262                          message = f'Table name updated and new table seeded: {table_name}'
263                      else:
264                          logger.info("Update request - no table name change, no action needed")
265                          message = 'Update completed - no data seeding required'
266                      
267                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {
268                          'Message': message,
269                          'TableName': table_name
270                      })
271                  elif request_type == 'Delete':
272                      # For deletes, we don't need to clean up data (table will be deleted by CloudFormation)
273                      # But we should validate the operation completed successfully
274                      logger.info("Delete request - no data cleanup needed (table will be deleted by CloudFormation)")
275                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {
276                          'Message': 'Delete completed - table cleanup handled by CloudFormation',
277                          'TableName': table_name
278                      })
279                  else:
280                      logger.error(f"Unknown request type: {request_type}")
281                      cfnresponse.send(event, context, cfnresponse.FAILED, {
282                          'Message': f'Unknown request type: {request_type}'
283                      })
284                      
285              except Exception as e:
286                  logger.error(f"Error processing request: {str(e)}")
287                  cfnresponse.send(event, context, cfnresponse.FAILED, {
288                      'Message': f'Error: {str(e)}'
289                  })
290
291          def seed_data(table_name):
292              """
293              Seeds the DynamoDB table with initial restaurant data.
294              Implements idempotent operations to handle retries safely.
295              """
296              try:
297                  dynamodb = boto3.resource('dynamodb')
298                  table = dynamodb.Table(table_name)
299                  
300                  # Wait for table to be active before seeding
301                  logger.info(f"Waiting for table {table_name} to be active...")
302                  table.wait_until_exists()
303                  
304                  # Verify table is in ACTIVE state
305                  table_status = table.table_status
306                  if table_status != 'ACTIVE':
307                      raise Exception(f"Table {table_name} is not in ACTIVE state (current: {table_status})")
308                  
309                  # Initial restaurant data - matches the original preparation script
310                  restaurants = [
311                      {'name': 'ihop', 'restaurantcount': 0},
312                      {'name': 'outback', 'restaurantcount': 0},
313                      {'name': 'bucadibeppo', 'restaurantcount': 0},
314                      {'name': 'chipotle', 'restaurantcount': 0}
315                  ]
316                  
317                  logger.info(f"Seeding table {table_name} with {len(restaurants)} restaurants")
318                  
319                  # Track seeding results
320                  seeded_count = 0
321                  skipped_count = 0
322                  
323                  # Use batch write for efficiency, but handle individual failures
324                  with table.batch_writer() as batch:
325                      for restaurant in restaurants:
326                          try:
327                              # Check if item already exists (idempotent operation)
328                              response = table.get_item(Key={'name': restaurant['name']})
329                              
330                              if 'Item' not in response:
331                                  # Item doesn't exist, create it
332                                  batch.put_item(Item=restaurant)
333                                  logger.info(f"Added restaurant: {restaurant['name']}")
334                                  seeded_count += 1
335                              else:
336                                  # Item exists, log but don't overwrite (idempotent behavior)
337                                  existing_count = response['Item'].get('restaurantcount', 0)
338                                  logger.info(f"Restaurant {restaurant['name']} already exists with count {existing_count}, skipping")
339                                  skipped_count += 1
340                                  
341                          except ClientError as e:
342                              logger.error(f"Error processing restaurant {restaurant['name']}: {str(e)}")
343                              raise
344                              
345                  logger.info(f"Data seeding completed: {seeded_count} restaurants added, {skipped_count} already existed")
346                  
347                  # Verify seeding was successful by checking all restaurants exist
348                  verify_seeded_data(table, restaurants)
349                  
350              except ClientError as e:
351                  error_code = e.response['Error']['Code']
352                  error_message = e.response['Error']['Message']
353                  logger.error(f"DynamoDB error ({error_code}): {error_message}")
354                  raise Exception(f"Failed to seed data: {error_code} - {error_message}")
355              except Exception as e:
356                  logger.error(f"Unexpected error during data seeding: {str(e)}")
357                  raise
358
359          def verify_seeded_data(table, expected_restaurants):
360              """
361              Verifies that all expected restaurants were seeded successfully.
362              """
363              logger.info("Verifying seeded data...")
364              
365              for restaurant in expected_restaurants:
366                  try:
367                      response = table.get_item(Key={'name': restaurant['name']})
368                      if 'Item' not in response:
369                          raise Exception(f"Verification failed: Restaurant {restaurant['name']} not found in table")
370                      
371                      item = response['Item']
372                      if 'restaurantcount' not in item:
373                          raise Exception(f"Verification failed: Restaurant {restaurant['name']} missing restaurantcount attribute")
374                      
375                      logger.info(f"Verified restaurant {restaurant['name']} exists with count {item['restaurantcount']}")
376                      
377                  except ClientError as e:
378                      logger.error(f"Error verifying restaurant {restaurant['name']}: {str(e)}")
379                      raise Exception(f"Verification failed for {restaurant['name']}: {str(e)}")
380              
381              logger.info("All restaurants verified successfully")          
382      Tags:
383        - Key: Environment
384          Value: !Ref Environment
385        - Key: Application
386          Value: 'votingapp'
387        - Key: ManagedBy
388          Value: 'CloudFormation'
389    DependsOn:
390      - LambdaExecutionRole
391      - LambdaDynamoDBPolicy
392
393  # Lambda permission to allow CloudFormation to invoke the function
394  LambdaInvokePermission:
395    Type: AWS::Lambda::Permission
396    Properties:
397      FunctionName: !Ref DataSeedingFunction
398      Action: lambda:InvokeFunction
399      Principal: cloudformation.amazonaws.com
400      SourceAccount: !Ref 'AWS::AccountId'
401    DependsOn:
402      - DataSeedingFunction
403
404  # Custom resource to trigger data seeding during stack operations
405  DataInitializationResource:
406    Type: AWS::CloudFormation::CustomResource
407    Properties:
408      ServiceToken: !GetAtt DataSeedingFunction.Arn
409      TableName: !Ref DynamoDBTable
410      # Adding a version property to force updates when needed
411      Version: '1.0'
412    DependsOn:
413      - DataSeedingFunction
414      - DynamoDBTable
415      - LambdaInvokePermission
416
417Outputs:
418  # Core outputs required for App Runner service configuration
419  # These values should be used when configuring the App Runner service
420  
421  DynamoDBTableName:
422    Description: 'Name of the DynamoDB table for restaurant votes - use this for DDB_TABLE_NAME environment variable'
423    Value: !Ref DynamoDBTable
424    Export:
425      Name: !Sub '${AWS::StackName}-DynamoDBTableName'
426
427  IAMRoleArn:
428    Description: 'ARN of the IAM role for App Runner instance configuration - use this for App Runner instance role'
429    Value: !GetAtt AppRunnerInstanceRole.Arn
430    Export:
431      Name: !Sub '${AWS::StackName}-IAMRoleArn'
432      
433  AWSRegion:
434    Description: 'AWS Region where resources are deployed - use this for DDB_AWS_REGION environment variable'
435    Value: !Ref 'AWS::Region'
436    Export:
437      Name: !Sub '${AWS::StackName}-AWSRegion'
438
439  # Additional outputs for monitoring and management
440  
441  DynamoDBTableArn:
442    Description: 'ARN of the DynamoDB table for resource identification and monitoring'
443    Value: !GetAtt DynamoDBTable.Arn
444    Export:
445      Name: !Sub '${AWS::StackName}-DynamoDBTableArn'
446
447  IAMRoleName:
448    Description: 'Name of the IAM role for reference and management'
449    Value: !Ref AppRunnerInstanceRole
450    Export:
451      Name: !Sub '${AWS::StackName}-IAMRoleName'
452
453  StackEnvironment:
454    Description: 'Environment tag value for this stack deployment'
455    Value: !Ref Environment
456    Export:
457      Name: !Sub '${AWS::StackName}-Environment'
458
459  # Configuration summary for easy reference
460  
461  AppRunnerConfiguration:
462    Description: 'Summary of key configuration values for App Runner deployment'
463    Value: !Sub |
464      DynamoDB Table: ${DynamoDBTable}
465      IAM Role ARN: ${AppRunnerInstanceRole.Arn}
466      AWS Region: ${AWS::Region}
467      Environment: ${Environment}
468    Export:
469      Name: !Sub '${AWS::StackName}-AppRunnerConfig'
470
471# ============================================================================
472# USAGE NOTES AND TROUBLESHOOTING
473# ============================================================================
474#
475# APP RUNNER CONFIGURATION:
476# After deploying this stack, use the outputs to configure your App Runner service:
477# - DynamoDBTableName -> Set as DDB_TABLE_NAME environment variable
478# - IAMRoleArn -> Use as App Runner instance role ARN
479# - AWSRegion -> Set as DDB_AWS_REGION environment variable
480#
481# INITIAL DATA:
482# The template automatically seeds the DynamoDB table with four restaurants:
483# - ihop (vote count: 0)
484# - outback (vote count: 0)
485# - bucadibeppo (vote count: 0)
486# - chipotle (vote count: 0)
487#
488# MONITORING:
489# All resources are tagged with:
490# - Environment: dev/staging/prod (configurable)
491# - Application: votingapp
492# - ManagedBy: CloudFormation
493#
494# TROUBLESHOOTING:
495# - If stack creation fails, check CloudFormation events for detailed error messages
496# - Ensure your AWS credentials have sufficient permissions
497# - Verify that resource names don't conflict with existing resources
498# - Check AWS service limits if resource creation fails
499#
500# COST OPTIMIZATION:
501# - DynamoDB uses PAY_PER_REQUEST billing (no fixed costs)
502# - Lambda function only runs during stack operations
503# - IAM roles and policies have no direct costs
504#
505# SECURITY CONSIDERATIONS:
506# - IAM policies follow least-privilege principle
507# - DynamoDB access is restricted to specific table ARN
508# - Lambda function has minimal required permissions
509# - Point-in-time recovery is enabled for DynamoDB table
510#
511# ============================================================================

The process went (mostly) smoothly, but here below I am going to make a few observations in no particular order.

This output is nothing a real developer would define as complex. Yet, if I think about 4 years ago, when I built this application, it could have taken me the better part of a full day (if not more!) to build anything like this (especially building and testing the Lambda function for the CloudFormation custom resource). Arguably, this is something that, with proper vibe coding, it could have taken me a lot less time (likely still a few hours). The good thing about specs is that it only took me a few minutes of attention span to get this work done.

You may have noticed I said a few minutes of ... attention span. That is because it didn't take Kiro a few minutes to build this. Probably the end-to-end workflow took about roughly 2-3 hours (elapsed time). This is because I had to keep tabs on it. This was mostly for running the tasks in sequence and trusting the terminal commands the agent needed to run (there is work we need to do to optimize the commands trust engine, it can become a bit frustrating). But the good thing is that I did not need many brain cycles to go through all this. Yes, it took 2-3 hours but that was time I spent doing (for the most part) something else and only checking in from time to time to make sure the flow was not blocked.

The third observation is that I lied :) . I spent more than 2-3 hours. I went through this entire workflow twice. The reason for that is that I did not want Kiro to only validate empirically the correctness of the CFN template. As I said, I wanted Kiro to actually deploy the template and track any errors along the way. My first attempt was with the same prompt but I did not include the last line (Do not validate the CFN template simply with local tests and mocks. Make sure you actually deploy it to the AWS account and you test the application against that backend.). Without that line, the first result was that it created the template through a specs workflow, but never attempted to actually deploy it. When I tried to deploy it, it failed. That is when I decided to start from scratch (as a learning exercise) and added that new sentence at the bottom of the prompt. The effect of that line appears to be that it added Requirement 3 (As a quality assurance engineer, I want the infrastructure to be validated through actual deployment and testing, so that I can ensure the application works correctly with the created resources) and eventually Task 7 through Task 10 (check out Appendix A to inspect these tasks in the implementation document).

The last observation, for good or bad, is that this workflow left a lot behind. In addition to creating the CloudFormation template (the goal) in the root of my repository, it created a folder called scripts with a bunch of ... scripts (and reports, logs, etc). It did this as part of the process of building, testing, and verifying the work it was doing. Below is a snapshot of that folder's contents at the end of the workflow (during the workflow, many more scripts were generated and eventually deleted dynamically):

 1scripts/
 2├── README.md
 3├── api-validation-test.sh
 4├── cleanup-report.json
 5├── cleanup.log
 6├── cleanup.sh
 7├── comprehensive-e2e-test.sh
 8├── concurrent-test.sh
 9├── deploy.log
10├── deploy.sh
11├── e2e-test-report.md
12├── e2e-test.log
13├── test-scripts.sh
14├── validate.log
15└── validate.sh

I don't have strong opinions about this. I am wondering if someone would look at this positively (it's great to track everything it has done and potentially re-use some of these scripts) or negatively (what do I do with all this? Should I keep them? Should I check them in? Or should I just delete entirely what seems to be a temporary folder to get to the final goal?).

Massimo.

Appendix A. Kiro specifications files

 1# Requirements Document
 2
 3## Introduction
 4
 5This feature involves creating a comprehensive CloudFormation template that automates the entire AWS infrastructure setup for the voting application. The template will replace the manual preparation scripts and provide a single-stack deployment solution that includes DynamoDB table creation, IAM roles and policies, initial data seeding, and App Runner service configuration. The solution must be validated through actual deployment and testing to ensure the application can successfully connect to DynamoDB and serve API requests.
 6
 7## Requirements
 8
 9### Requirement 1
10
11**User Story:** As a DevOps engineer, I want a single CloudFormation template that creates all required AWS infrastructure, so that I can deploy the voting app without running manual scripts.
12
13#### Acceptance Criteria
14
151. WHEN the CloudFormation template is deployed THEN the system SHALL create a DynamoDB table named "votingapp-restaurants" with the correct schema
162. WHEN the DynamoDB table is created THEN the system SHALL populate it with initial data for all four restaurants (ihop, outback, bucadibeppo, chipotle) with zero vote counts
173. WHEN the template is deployed THEN the system SHALL create an IAM role with appropriate trust policy for App Runner services
184. WHEN the IAM role is created THEN the system SHALL attach a custom policy granting DynamoDB access to the specific table
195. WHEN the IAM role is created THEN the system SHALL attach the AWS managed AWSXRayDaemonWriteAccess policy for tracing
20
21### Requirement 2
22
23**User Story:** As a developer, I want the CloudFormation template to output all necessary values for App Runner configuration, so that I can easily deploy the application service.
24
25#### Acceptance Criteria
26
271. WHEN the CloudFormation stack is deployed THEN the system SHALL output the DynamoDB table name
282. WHEN the CloudFormation stack is deployed THEN the system SHALL output the IAM role ARN for App Runner instance configuration
293. WHEN the CloudFormation stack is deployed THEN the system SHALL output the AWS region for environment variable configuration
304. IF the stack deployment fails THEN the system SHALL provide clear error messages indicating the failure reason
31
32### Requirement 3
33
34**User Story:** As a quality assurance engineer, I want the infrastructure to be validated through actual deployment and testing, so that I can ensure the application works correctly with the created resources.
35
36#### Acceptance Criteria
37
381. WHEN the CloudFormation template is deployed to AWS THEN the system SHALL successfully create all resources without errors
392. WHEN the infrastructure is created THEN the system SHALL allow the application to connect to DynamoDB using the created IAM role
403. WHEN the application is running THEN the system SHALL respond successfully to GET requests on the /api/getvotes endpoint
414. WHEN the /api/getvotes endpoint is called THEN the system SHALL return valid JSON with all four restaurants and their vote counts
425. WHEN any restaurant voting endpoint is called THEN the system SHALL successfully increment the vote count in DynamoDB
43
44### Requirement 4
45
46**User Story:** As a system administrator, I want the CloudFormation template to follow AWS best practices for security and resource management, so that the infrastructure is secure and maintainable.
47
48#### Acceptance Criteria
49
501. WHEN creating IAM policies THEN the system SHALL use least-privilege access principles
512. WHEN creating the DynamoDB table THEN the system SHALL use PAY_PER_REQUEST billing mode for cost optimization
523. WHEN creating resources THEN the system SHALL use appropriate resource naming conventions with consistent prefixes
534. WHEN the stack is deleted THEN the system SHALL cleanly remove all created resources without leaving orphaned components
545. IF resource creation fails THEN the system SHALL rollback successfully without leaving partial deployments
55
56### Requirement 5
57
58**User Story:** As a deployment engineer, I want the template to be parameterized and configurable, so that I can customize the deployment for different environments.
59
60#### Acceptance Criteria
61
621. WHEN deploying the template THEN the system SHALL accept parameters for table name with a sensible default
632. WHEN deploying the template THEN the system SHALL accept parameters for IAM role name with a sensible default
643. WHEN parameters are provided THEN the system SHALL use those values instead of defaults
654. WHEN no parameters are provided THEN the system SHALL use default values that match the original preparation script behavior
665. WHEN invalid parameters are provided THEN the system SHALL validate and reject the deployment with clear error messages
  1# Design Document
  2
  3## Overview# Design Document
  4
  5## Overview
  6
  7The CloudFormation template will be a comprehensive infrastructure-as-code solution that replaces the manual preparation scripts. It will create all necessary AWS resources in a single stack deployment, including DynamoDB table with initial data, IAM roles and policies, and provide outputs for App Runner configuration. The design emphasizes automation, validation, and adherence to AWS best practices.
  8
  9## Architecture
 10
 11### High-Level Architecture
 12```
 13CloudFormation Stack
 14├── DynamoDB Table (votingapp-restaurants)
 15│   ├── Hash Key: name (String)
 16│   ├── Billing Mode: PAY_PER_REQUEST
 17│   └── Initial Items: 4 restaurants with 0 votes
 18├── IAM Role (votingapp-role)
 19│   ├── Trust Policy: App Runner service principal
 20│   ├── Custom DynamoDB Policy: Table-specific access
 21│   └── AWS Managed Policy: X-Ray write access
 22└── Stack Outputs
 23    ├── DynamoDB Table Name
 24    ├── IAM Role ARN
 25    └── AWS Region
 26```
 27
 28### Resource Dependencies
 291. DynamoDB Table (independent resource)
 302. IAM Role (independent resource)
 313. IAM Policy (depends on DynamoDB table for ARN reference)
 324. Policy Attachments (depend on role and policies)
 335. Custom Resource for data seeding (depends on DynamoDB table)
 34
 35## Components and Interfaces
 36
 37### CloudFormation Template Structure
 38
 39#### Parameters Section
 40- `TableName`: String parameter with default "votingapp-restaurants"
 41- `RoleName`: String parameter with default "votingapp-role"
 42- `Environment`: String parameter for resource tagging (default: "dev")
 43
 44#### Resources Section
 45
 46**DynamoDB Table Resource**
 47```yaml
 48Type: AWS::DynamoDB::Table
 49Properties:
 50  TableName: !Ref TableName
 51  AttributeDefinitions:
 52    - AttributeName: name
 53      AttributeType: S
 54  KeySchema:
 55    - AttributeName: name
 56      KeyType: HASH
 57  BillingMode: PAY_PER_REQUEST
 58  Tags:
 59    - Key: Environment
 60      Value: !Ref Environment
 61```
 62
 63**IAM Role Resource**
 64```yaml
 65Type: AWS::IAM::Role
 66Properties:
 67  RoleName: !Ref RoleName
 68  AssumeRolePolicyDocument:
 69    Version: '2012-10-17'
 70    Statement:
 71      - Effect: Allow
 72        Principal:
 73          Service: tasks.apprunner.amazonaws.com
 74        Action: sts:AssumeRole
 75```
 76
 77**Custom DynamoDB Policy**
 78```yaml
 79Type: AWS::IAM::Policy
 80Properties:
 81  PolicyName: votingapp-ddb-policy
 82  PolicyDocument:
 83    Version: '2012-10-17'
 84    Statement:
 85      - Effect: Allow
 86        Action: dynamodb:*
 87        Resource: !GetAtt DynamoDBTable.Arn
 88  Roles:
 89    - !Ref IAMRole
 90```
 91
 92**Lambda Function for Data Seeding**
 93```yaml
 94Type: AWS::Lambda::Function
 95Properties:
 96  Runtime: python3.9
 97  Handler: index.handler
 98  Code:
 99    ZipFile: |
100      # Python code to seed initial restaurant data      
101  Role: !GetAtt LambdaExecutionRole.Arn
102```
103
104**Custom Resource for Data Initialization**
105```yaml
106Type: AWS::CloudFormation::CustomResource
107Properties:
108  ServiceToken: !GetAtt DataSeedingFunction.Arn
109  TableName: !Ref DynamoDBTable
110```
111
112#### Outputs Section
113- `DynamoDBTableName`: Table name for App Runner environment variables
114- `IAMRoleArn`: Role ARN for App Runner instance configuration
115- `AWSRegion`: Current region for application configuration
116
117### Data Seeding Strategy
118
119The template will include a Lambda function that acts as a CloudFormation custom resource to seed the DynamoDB table with initial data. This approach ensures:
120- Data is created only once during stack creation
121- Data is properly cleaned up during stack deletion
122- Idempotent operations that can handle retries
123
124### Validation and Testing Strategy
125
126#### Deployment Validation
1271. CloudFormation template syntax validation using AWS CLI
1282. Actual deployment to AWS account using default profile
1293. Resource creation verification through AWS console/CLI
1304. Stack outputs validation
131
132#### Application Testing
1331. Deploy the CloudFormation stack
1342. Configure App Runner service using stack outputs
1353. Deploy the voting application
1364. Test API endpoints:
137   - GET /api/getvotes (should return initial data)
138   - POST to voting endpoints (should increment counts)
139   - Verify DynamoDB updates
140
141## Data Models
142
143### DynamoDB Table Schema
144```
145Table: votingapp-restaurants
146Primary Key: name (String)
147Attributes:
148  - name: String (Hash Key) - Restaurant identifier
149  - restaurantcount: Number - Vote count for the restaurant
150```
151
152### Initial Data Set
153```json
154[
155  {"name": "ihop", "restaurantcount": 0},
156  {"name": "outback", "restaurantcount": 0},
157  {"name": "bucadibeppo", "restaurantcount": 0},
158  {"name": "chipotle", "restaurantcount": 0}
159]
160```
161
162### IAM Policy Structure
163```json
164{
165  "Version": "2012-10-17",
166  "Statement": [
167    {
168      "Effect": "Allow",
169      "Action": "dynamodb:*",
170      "Resource": "arn:aws:dynamodb:region:account:table/table-name"
171    }
172  ]
173}
174```
175
176## Error Handling
177
178### CloudFormation Deployment Errors
179- Template validation errors: Provide clear syntax error messages
180- Resource creation failures: Implement proper rollback mechanisms
181- Permission errors: Clear error messages about required AWS permissions
182- Resource limit errors: Guidance on AWS service limits
183
184### Application Runtime Errors
185- DynamoDB connection failures: Verify IAM role permissions
186- Missing environment variables: Validate App Runner configuration
187- API endpoint errors: Check application logs and DynamoDB access
188
189### Custom Resource Error Handling
190- Lambda function failures during data seeding
191- Retry mechanisms for transient DynamoDB errors
192- Proper cleanup during stack deletion failures
193
194## Testing Strategy
195
196### Unit Testing
197- CloudFormation template validation using cfn-lint
198- IAM policy validation using policy simulator
199- Lambda function code testing with mock DynamoDB calls
200
201### Integration Testing
2021. **Infrastructure Testing**
203   - Deploy CloudFormation stack to AWS
204   - Verify all resources are created correctly
205   - Test IAM role permissions with AWS CLI
206   - Validate DynamoDB table structure and initial data
207
2082. **Application Testing**
209   - Deploy App Runner service using stack outputs
210   - Test application startup and DynamoDB connectivity
211   - Validate all API endpoints return expected responses
212   - Test vote increment functionality
213
2143. **End-to-End Testing**
215   - Complete deployment workflow from CloudFormation to running application
216   - API testing with curl/Postman
217   - Load testing with multiple concurrent requests
218   - Stack deletion and cleanup verification
219
220### Validation Criteria
221- CloudFormation stack deploys successfully without errors
222- All AWS resources are created with correct configurations
223- Application can connect to DynamoDB using created IAM role
224- /api/getvotes endpoint returns valid JSON with all restaurants
225- Vote increment operations successfully update DynamoDB
226- Stack deletion removes all resources cleanly
 1# Implementation Plan
 2
 3- [x] 1. Create CloudFormation template structure and basic resources
 4    - Create cloudformation-template.yaml file with basic structure
 5    - Define template parameters for table name, role name, and environment
 6    - Implement DynamoDB table resource with correct schema and billing mode
 7    - Add basic metadata and description to template
 8    - _Requirements: 1.1, 1.2, 4.2, 5.1, 5.2, 5.4_
 9
10- [x] 2. Implement IAM role and policy resources
11    - Create IAM role resource with App Runner trust policy
12    - Implement custom DynamoDB policy with least-privilege access
13    - Add AWS managed policy attachment for X-Ray access
14    - Configure proper resource dependencies between role and policies
15    - _Requirements: 1.3, 1.4, 1.5, 4.1_
16
17- [x] 3. Create Lambda function for DynamoDB data seeding
18    - Write Python Lambda function code for initializing restaurant data
19    - Create Lambda execution role with DynamoDB write permissions
20    - Implement error handling and idempotent operations in Lambda
21    - Add proper logging for debugging data seeding operations
22    - _Requirements: 1.2, 4.4_
23
24- [x] 4. Implement CloudFormation custom resource for data initialization
25    - Create custom resource that triggers Lambda function during stack operations
26    - Configure custom resource to pass table name to Lambda function
27    - Implement proper cleanup logic for stack deletion scenarios
28    - Add dependency management between custom resource and DynamoDB table
29    - _Requirements: 1.2, 4.4_
30
31- [x] 5. Add CloudFormation outputs and finalize template
32    - Implement stack outputs for DynamoDB table name, IAM role ARN, and region
33    - Add resource tags for environment identification and management
34    - Validate template syntax and resource references
35    - Add template documentation and usage comments
36    - _Requirements: 2.1, 2.2, 2.3, 4.3_
37
38- [x] 6. Create deployment and testing scripts
39    - Write deployment script that uses AWS CLI to deploy CloudFormation stack
40    - Create validation script to verify all resources are created correctly
41    - Implement cleanup script for stack deletion and resource verification
42    - Add error handling and logging to all deployment scripts
43    - _Requirements: 3.1, 4.4, 5.5_
44
45- [x] 7. Deploy and validate CloudFormation stack in AWS
46    - Execute deployment script to create CloudFormation stack in AWS account
47    - Verify DynamoDB table creation and initial data population
48    - Validate IAM role creation and policy attachments
49    - Test stack outputs and confirm all values are correct
50    - _Requirements: 3.1, 3.2, 2.4_
51
52- [x] 8. Test application connectivity with created infrastructure
53    - Launch the voting application locally
54    - Verify application can connect to DynamoDB using the local AWS profile
55    - Test application startup and basic functionality
56    - _Requirements: 3.2, 3.3_
57
58- [x] 9. Validate API endpoints and DynamoDB operations
59    - Test GET /api/getvotes endpoint returns correct initial data structure
60    - Verify all four restaurants are present with zero vote counts
61    - Test vote increment functionality on all restaurant endpoints
62    - Confirm DynamoDB updates are persisted correctly after vote operations
63    - _Requirements: 3.3, 3.4, 3.5_
64
65- [x] 10. Perform comprehensive end-to-end testing
66    - Execute complete deployment workflow from CloudFormation to running app
67    - Test multiple concurrent API requests to verify system stability
68    - Validate stack deletion removes all resources without orphaned components
69    - Document any issues found and verify all requirements are met
70    - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 4.4_