Build a Status API with CloudFormation

Deploy a repeatable AWS Lambda status API with CloudFormation.

Introduction

30 Second Summary

A cloud service that exists as a trail of console clicks is difficult for another developer to reproduce. One missed permission can leave the next person guessing.

In this project, you will build a repeatable status API from one version-controlled AWS CloudFormation template. A browser request will return a health response through an AWS Lambda function URL.

What You'll Build

You'll open one HTTPS address to see a healthy JSON response from infrastructure another developer can deploy from the same reviewed template.

By the end of this project, you'll have:

  • A live HTTPS endpoint you can open in a browser to see a healthy response.
  • A reviewed deployment whose infrastructure changes are visible before you apply them.
  • Traceable invocations you can follow from the endpoint into Amazon CloudWatch Logs.
  • Secret Mission: Parameterize one template so it deploys an independent dev stack plus an independent test stack.

Are there any prerequisites?

You'll need Windows plus an AWS account with permission to create the project's resources.

You'll also need Visual Studio Code plus Git. You do not need the AWS CLI or a local Python installation.

Before We Start

This first checkpoint locks in the repeatable status API you are about to build with AWS CloudFormation. It also connects that goal to making compute, identity, logging, access, and lifecycle management explicit for future platform users.

Set Up the Project Workspace

Before the template can become a live service, the AWS Management Console needs a controlled identity with permission to create project resources. Those resources span AWS CloudFormation, IAM, AWS Lambda, and Amazon CloudWatch Logs.

A fixed AWS Region keeps every stack operation in one location. A local Git repository gives the future template a version-controlled home in Visual Studio Code.

In this step, get ready to:
  • Validate a non-root AWS identity with permission to create the project resources.
  • Set the AWS console to US East (N. Virginia).
  • Initialize the aws-status-api Git repository in a Visual Studio Code workspace.
Validate your AWS access

A non-root identity keeps everyday cloud work separate from the account's most sensitive credentials. Its permissions must cover every resource that CloudFormation creates later.

This step only checks your existing access, so no project resources are created or billed.

  • Open your browser.
  • Search for AWS Management Console.
  • Open the official AWS result.
  • Sign in with a non-root identity.
  • Ask your AWS administrator to confirm that this identity can create CloudFormation stacks, IAM roles, Lambda functions, and CloudWatch Logs log groups.
  • Open the Region menu in the console header.
  • Choose US East (N. Virginia).
  • Keep the AWS console open in this browser tab.

The console header should now show US East (N. Virginia). Its Region code is us-east-1.

Good start. Your cloud work is now tied to an authorized identity in the intended Region.

Why Use a Non-Root Identity?

A non-root identity receives only the permissions needed for its work. This protects the root identity from routine use.

The same approach supports least privilege when the template creates the Lambda execution role later.

Access Denied?

  • Stop before deployment if the console denies access to one of the required services.
  • Switch to an identity authorized to create CloudFormation, IAM, Lambda, and CloudWatch Logs resources.

Ask for help: Help me identify which AWS permissions I need before deploying this CloudFormation project.

Prepare your local repository

PowerShell gives Git a predictable starting location for the repository. Placing the folder on your Desktop also makes it easy to find from Visual Studio Code.

  • Press the Windows key to open Windows search.
  • Type PowerShell into the search field.
  • Press Enter to open PowerShell.

You'll see a PowerShell window ready for commands.

  • Move to your Desktop by running this command:
Set-Location ~\Desktop

What Does This Command Do?

The Set-Location ~\Desktop command moves PowerShell to the Desktop folder. The repository created next therefore has a predictable location.

  • Create the aws-status-api folder as an empty Git repository by running this command:
git init aws-status-api

What Does This Command Do?

The git init aws-status-api command creates the named folder. Git stores repository metadata inside it so future template changes can be committed.

PowerShell should report that an empty Git repository was initialized inside the aws-status-api folder.

That gives your infrastructure definition a traceable local home before any cloud resource exists.

Repository Not Initialized?

  • Check that PowerShell is still pointing to your Desktop before retrying the initialization command.
  • Confirm that Git is available if PowerShell cannot run the command.

Ask for help: Help me diagnose why Git did not initialize my aws-status-api repository in PowerShell.

Verify the complete workspace

The Visual Studio Code command-line interface can open a named folder directly from PowerShell. This keeps the editor attached to the repository you just initialized.

Before you run the command, what do you expect Visual Studio Code to show? Hold that prediction for the result.

  • Open the initialized repository in Visual Studio Code by running this command:
code aws-status-api

What Does This Command Do?

The code aws-status-api command passes the repository folder to Visual Studio Code. The editor opens with that folder as the current workspace.

You'll see Visual Studio Code open the aws-status-api folder. The empty repository is now ready for its CloudFormation template.

  • Switch back to the AWS Management Console tab from earlier.
  • Confirm that the console header still shows US East (N. Virginia).

You now have Visual Studio Code open to aws-status-api. Your AWS console remains open in us-east-1.

Visual Studio Code Did Not Open?

  • Use Windows search to start Visual Studio Code if PowerShell cannot launch the editor.
  • Choose the existing aws-status-api folder from your Desktop in the editor's folder picker.

Ask for help: Help me open my aws-status-api repository when the VS Code command does not work in PowerShell.

That's the foundation locked in. Next, you'll define the function with its identity and logging dependencies in one CloudFormation template.

Deploy the First Working Function

The aws-status-api repository is ready in Visual Studio Code. Your AWS session remains fixed to us-east-1.

This first version proves that AWS CloudFormation can create a runnable AWS Lambda workload.

An explicit AWS Identity and Access Management (IAM) role exposes the function's identity dependency. A dedicated Amazon CloudWatch Logs group exposes its logging dependency.

In this step, get ready to:
  • Define a seven-day log group with a least-privilege Lambda execution role.
  • Add an inline Python 3.14 function that returns a JSON health response.
  • Prove the deployed function returns a successful response in the Lambda console.

Why CloudFormation here?

CloudFormation fits this AWS-only service because it manages the resources as one stack. It also supports reviewed change previews for later updates.

A tool such as Terraform becomes useful when one workflow must span several providers.

Define logging and identity

The log group keeps function logs for seven days. The execution role grants only the two actions needed to write those logs.

  • Select the aws-status-api folder in the VS Code sidebar.
  • Click the new-file control in the sidebar.
  • Enter template.yaml as the file name.
  • Fill template.yaml with this first section:
AWSTemplateFormatVersion: '2010-09-09'
Description: Repeatable serverless status API with explicit logging and function URL access.

Resources:
  StatusLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-function'
      RetentionInDays: 7

  StatusFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: WriteStatusLogs
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt StatusLogGroup.Arn

What does this template section do?

  • The template format declaration identifies the CloudFormation template syntax.
  • StatusLogGroup creates a named log group with seven-day retention.
  • StatusFunctionRole trusts the Lambda service through lambda.amazonaws.com.
  • WriteStatusLogs permits log stream creation within the project's log group.
  • The same policy permits log events to be written within that group.
  • Save template.yaml.
  • Confirm that StatusLogGroup appears directly beneath Resources.
  • Confirm that StatusFunctionRole aligns with StatusLogGroup.

You should see two resource definitions inside template.yaml. The role policy should contain only logs:CreateLogStream plus logs:PutLogEvents.

Does the YAML look misaligned?

  • Check that StatusLogGroup has two leading spaces.
  • Check that StatusFunctionRole uses the same indentation.
  • Check that each policy statement remains nested beneath Statement.

Ask for help: Help me compare the indentation in my CloudFormation log group and IAM role with the expected template structure.

Add the inline Lambda function

CloudFormation packages inline Python source from ZipFile into an index file. The handler path therefore uses index.lambda_handler.

  • Place your cursor on a blank line after Resource: !GetAtt StatusLogGroup.Arn.
  • Complete template.yaml by pasting this resource at that position:
  StatusFunction:
    Type: AWS::Lambda::Function
    DependsOn: StatusLogGroup
    Properties:
      FunctionName: !Sub '${AWS::StackName}-function'
      Runtime: python3.14
      Handler: index.lambda_handler
      Architectures:
        - arm64
      MemorySize: 128
      Timeout: 3
      Role: !GetAtt StatusFunctionRole.Arn
      Code:
        ZipFile: |
          def lambda_handler(event, context):
              print("status check received")
              return {
                  "statusCode": 200,
                  "headers": {
                      "Content-Type": "application/json"
                  },
                  "body": "{\"service\":\"platform-status-api\",\"status\":\"ok\"}"
              }

What does this function resource do?

  • DependsOn ensures that the log group exists before the function is created.
  • FunctionName derives the function name from the stack name.
  • Runtime selects python3.14 for the inline handler.
  • Architectures selects arm64.
  • MemorySize sets the function memory to 128 MB.
  • Timeout limits each invocation to 3 seconds.
  • Role connects the function to StatusFunctionRole.
  • lambda_handler returns a synchronous response with statusCode set to 200.
  • Save template.yaml.
  • Scroll from StatusLogGroup to StatusFunction.

You should see three resources beneath Resources. The last resource should end with the function's JSON body.

Is the function outside Resources?

  • Align StatusFunction with StatusFunctionRole.
  • Keep the Python handler nested beneath ZipFile.
  • Preserve the escaped double quotes inside the response body.

Ask for help: Help me check where StatusFunction belongs in template.yaml and verify the inline Python indentation.

✔️ Awesome, I've got everything!

Your complete template.yaml now defines logging, identity, and compute. Double check that you saved the file.

ⓧ I'd like to double check the full code

Compare your complete template.yaml with this version before deployment.

AWSTemplateFormatVersion: '2010-09-09'
Description: Repeatable serverless status API with explicit logging and function URL access.

Resources:
  StatusLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-function'
      RetentionInDays: 7

  StatusFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: WriteStatusLogs
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt StatusLogGroup.Arn

  StatusFunction:
    Type: AWS::Lambda::Function
    DependsOn: StatusLogGroup
    Properties:
      FunctionName: !Sub '${AWS::StackName}-function'
      Runtime: python3.14
      Handler: index.lambda_handler
      Architectures:
        - arm64
      MemorySize: 128
      Timeout: 3
      Role: !GetAtt StatusFunctionRole.Arn
      Code:
        ZipFile: |
          def lambda_handler(event, context):
              print("status check received")
              return {
                  "statusCode": 200,
                  "headers": {
                      "Content-Type": "application/json"
                  },
                  "body": "{\"service\":\"platform-status-api\",\"status\":\"ok\"}"
              }
Create and test the stack

A CloudFormation stack manages the template's resources as one lifecycle unit. Successful creation proves that the relationships in template.yaml are valid.

This is the first point where the project creates cloud resources. Normal lab usage is expected to remain within the published free tiers.

  • Switch back to the AWS Management Console from earlier.
  • Confirm that the console header shows US East (N. Virginia).
  • Use the console search bar to find AWS CloudFormation.
  • Select AWS CloudFormation from the results.
  • Choose Create stack.
  • Select With new resources (standard).
  • Select Choose an existing template.
  • Select Upload a template file.

Where does the uploaded template go?

CloudFormation stores an uploaded local template in a regional Amazon S3 bucket. AWS can create that encrypted bucket when one is not already available.

The bucket can be shared by later CloudFormation uploads. Cleanup therefore requires checking it separately from the stack.

  • Choose template.yaml from the aws-status-api folder.
  • Continue from the template selection page.
  • Enter platform-status-api as the stack name.
  • Continue from the stack details page.
  • Keep the defaults on the stack options page.
  • Continue to the review page.

The review page summarizes the resources CloudFormation is about to create. The IAM acknowledgment confirms that you understand the template creates an execution role.

  • Select the acknowledgment that the template may create IAM resources.
  • Submit the stack creation request.

Stack creation can take a few minutes. The Events tab updates while CloudFormation creates each resource.

  • Keep the Events tab open.
  • Wait until the stack status reaches CREATE_COMPLETE.

The first deployment is complete. Your template now controls the function, its execution identity, and its log destination.

  • Use the AWS console search bar to find AWS Lambda.
  • Select AWS Lambda from the results.
  • Select platform-status-api-function from the Event Function list.
  • Choose Test on the function page.
  • Replace the test event contents with {}.

Before you invoke the function, what status do you expect the handler to return?

  • Choose Test to invoke the function.
  • Expand Execution result.

You should see statusCode set to 200. The body should contain "service":"platform-status-api" plus "status":"ok".

That is the first working slice complete. CloudFormation has created a Lambda function that runs successfully through its explicit role.

Test not returning 200?

  • Return to the stack's Events tab if the stack did not reach CREATE_COMPLETE.
  • Confirm that the test event contains only {}.
  • Compare the saved StatusFunction section with the full-code tab.

Ask for help: Help me diagnose why platform-status-api-function does not return statusCode 200 in the Lambda console.

Your function now runs as a stack-managed workload. Next, you'll give it an HTTPS entry point and test the browser path.

Expose the Missing Access Policy

Your AWS CloudFormation stack now runs the status function successfully. The AWS Lambda console returned a 200 response from the handler.

A browser request tests a separate access boundary over HTTPS. In this step, you'll create the endpoint before testing that boundary.

In this step, get ready to:
  • Define a dedicated function URL for the status function.
  • Publish the endpoint through a stack output.
  • Update the stack to expose the missing access permission through a browser response.
Add the function URL

A Lambda function URL gives one function a dedicated web address. The URL resource controls how requests reach your existing function.

  • Return to template.yaml in the Visual Studio Code window from earlier.
  • Scroll to the blank line after the StatusFunction resource.
  • Extend the Resources section by pasting this block:
  StatusFunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      InvokeMode: BUFFERED
      TargetFunctionArn: !GetAtt StatusFunction.Arn

What does this resource do?

  • The AWS::Lambda::Url resource creates a dedicated HTTP(S) endpoint.
  • The AuthType value NONE removes signed-request authentication from the endpoint.
  • The BUFFERED invocation mode waits for the function's complete response.
  • The TargetFunctionArn property connects the URL to StatusFunction.
  • Save template.yaml.
  • Press Cmd+F (macOS) or Ctrl+F (Windows).
  • Enter StatusFunctionUrl in the search field.

Visual Studio Code highlights one resource definition beneath StatusFunction.

Function URL block not lining up?

  • Align StatusFunctionUrl with StatusFunction under Resources.
  • Keep each nested property indented by two additional spaces.
  • Check that TargetFunctionArn contains !GetAtt StatusFunction.Arn exactly.

Ask for help: Help me check the YAML indentation in my StatusFunctionUrl resource.

Publish the endpoint output

A stack output surfaces a useful value after deployment. Publishing the function URL here gives you one consistent place to retrieve the endpoint.

  • Move to the blank line after the StatusFunctionUrl resource.
  • Add the output section by pasting this block:
Outputs:
  StatusUrl:
    Description: Public HTTPS endpoint for the status API.
    Value: !GetAtt StatusFunctionUrl.FunctionUrl

What does this output do?

  • The StatusUrl output publishes the endpoint after the stack update.
  • The FunctionUrl attribute supplies the generated address from StatusFunctionUrl.
  • Save template.yaml.
  • Press Cmd+F (macOS) or Ctrl+F (Windows).
  • Enter StatusUrl in the search field.

Visual Studio Code highlights one output definition at the end of template.yaml.

Output not at the end of the template?

  • Align Outputs with Resources at the left edge.
  • Keep StatusUrl indented beneath Outputs.
  • Place the output after every resource definition.

Ask for help: Help me check where the Outputs section belongs in my CloudFormation template.

✔️ Awesome, I've got everything!

Good work. Your saved template now defines the endpoint resource plus its stack output.

ⓧ I'd like to double check the full code

Compare your complete template.yaml file with this version.

AWSTemplateFormatVersion: '2010-09-09'
Description: Repeatable serverless status API with explicit logging and function URL access.

Resources:
  StatusLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-function'
      RetentionInDays: 7

  StatusFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: WriteStatusLogs
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt StatusLogGroup.Arn

  StatusFunction:
    Type: AWS::Lambda::Function
    DependsOn: StatusLogGroup
    Properties:
      FunctionName: !Sub '${AWS::StackName}-function'
      Runtime: python3.14
      Handler: index.lambda_handler
      Architectures:
        - arm64
      MemorySize: 128
      Timeout: 3
      Role: !GetAtt StatusFunctionRole.Arn
      Code:
        ZipFile: |
          def lambda_handler(event, context):
              print("status check received")
              return {
                  "statusCode": 200,
                  "headers": {
                      "Content-Type": "application/json"
                  },
                  "body": "{\"service\":\"platform-status-api\",\"status\":\"ok\"}"
              }

  StatusFunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      InvokeMode: BUFFERED
      TargetFunctionArn: !GetAtt StatusFunction.Arn

Outputs:
  StatusUrl:
    Description: Public HTTPS endpoint for the status API.
    Value: !GetAtt StatusFunctionUrl.FunctionUrl
Update the stack and test access

A direct stack update compares your replacement template with the deployed stack. Its preview lets you confirm that the existing function remains in place.

  • Switch back to the AWS CloudFormation console from earlier.
  • Confirm the Region selector displays us-east-1.
  • Select the platform-status-api stack.
  • Choose Update.
  • Select Replace existing template.
  • Select Upload a template file.
  • Use the template file picker to select template.yaml from the aws-status-api folder.

The replacement template is now selected. CloudFormation prepares a preview before applying the update.

  • Continue to the update preview using the console's review controls.
  • Review the preview for the new StatusFunctionUrl resource.
  • Confirm the existing function has no replacement listed.
  • Choose Update stack.
  • Stay on the Events tab until the stack reaches UPDATE_COMPLETE.

The update is complete. Your stack now publishes an HTTPS endpoint while preserving the working function.

  • Select the stack's Outputs tab.
  • Locate the StatusUrl output.

Before you open the endpoint, consider this question: do you think the browser request can pass the access boundary.

  • Open the HTTPS address shown for StatusUrl.

You'll see 403 Forbidden in the browser. This intended failure proves that the URL exists without a resource-based policy authorizing public invocation.

This Is the Designed Failure

The function URL can receive a browser request. AWS Lambda still checks whether its resource policy permits that invocation.

  • The lambda:InvokeFunctionUrl permission authorizes function URL invocation.
  • The lambda:InvokeFunction permission authorizes the function invocation itself.
  • Your current template grants neither permission to public callers.

Seeing a Different Result?

  • Return to the stack's Outputs tab if the browser cannot load the endpoint.
  • Open the exact address listed under StatusUrl.
  • Confirm the stack shows UPDATE_COMPLETE if the output is missing.
  • Check that you selected the platform-status-api stack in us-east-1 if the browser returns JSON.

Ask for help: Help me diagnose why my StatusUrl is not showing the expected 403 response.

You exposed the exact permission gap between creating an endpoint and authorizing its callers. Next, you'll add the scoped public guardrail that turns this designed failure into a working status API.

Add the Public Invocation Guardrail

Your Lambda function URL now exists. Its 403 Forbidden response proves that the endpoint still lacks permission to invoke your function.

A resource-based policy makes that public access decision explicit. You will add two scoped permissions through AWS CloudFormation so ordinary function invocation remains limited to the URL path.

In this step, get ready to:
  • Add the two public invocation permissions to template.yaml.
  • Update the platform-status-api stack with the revised template.
  • Refresh the public endpoint to verify that it returns the status response.
Add the scoped resource policy

Public function URLs require permission for the URL invocation action. They also require permission for the underlying function invocation. The conditions below keep both permissions tied to the public function URL path.

  • Switch back to template.yaml in VS Code.
  • Find the StatusFunctionUrl resource.
  • Add the following resource below StatusFunctionUrl and above Outputs:
  StatusFunctionPolicy:
    Type: AWS::Lambda::ResourcePolicy
    Properties:
      ResourceArn: !GetAtt StatusFunction.Arn
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: FunctionURLAllowPublicAccess
            Effect: Allow
            Principal: '*'
            Action: lambda:InvokeFunctionUrl
            Resource: !GetAtt StatusFunction.Arn
            Condition:
              StringEquals:
                lambda:FunctionUrlAuthType: NONE
          - Sid: FunctionURLInvokeAllowPublicAccess
            Effect: Allow
            Principal: '*'
            Action: lambda:InvokeFunction
            Resource: !GetAtt StatusFunction.Arn
            Condition:
              Bool:
                lambda:InvokedViaFunctionUrl: 'true'

Why are there two statements?

  • The first statement permits lambda:InvokeFunctionUrl when the URL uses NONE authentication.
  • The second statement permits lambda:InvokeFunction when the request arrives through the function URL.
  • The conditions keep public invocation attached to the URL path defined by this stack.
  • Save template.yaml.
  • Scroll from StatusFunctionUrl to Outputs in the saved file.

You should see StatusFunctionPolicy between the existing URL resource and the output section.

Policy block looking out of place?

  • Check that StatusFunctionPolicy uses the same two-space indentation as StatusFunctionUrl.
  • Confirm the policy remains inside Resources.
  • Verify that the final value is the quoted string 'true'.

Ask for help: Help me compare the StatusFunctionPolicy indentation and placement with the surrounding CloudFormation resources.

✔️ Awesome, I've got everything!

Great. Double check that you saved template.yaml before updating the stack.

ⓧ I'd like to double check the full code

Compare your complete template.yaml file with this version.

AWSTemplateFormatVersion: '2010-09-09'
Description: Repeatable serverless status API with explicit logging and function URL access.

Resources:
  StatusLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-function'
      RetentionInDays: 7

  StatusFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: WriteStatusLogs
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt StatusLogGroup.Arn

  StatusFunction:
    Type: AWS::Lambda::Function
    DependsOn: StatusLogGroup
    Properties:
      FunctionName: !Sub '${AWS::StackName}-function'
      Runtime: python3.14
      Handler: index.lambda_handler
      Architectures:
        - arm64
      MemorySize: 128
      Timeout: 3
      Role: !GetAtt StatusFunctionRole.Arn
      Code:
        ZipFile: |
          def lambda_handler(event, context):
              print("status check received")
              return {
                  "statusCode": 200,
                  "headers": {
                      "Content-Type": "application/json"
                  },
                  "body": "{\"service\":\"platform-status-api\",\"status\":\"ok\"}"
              }

  StatusFunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      InvokeMode: BUFFERED
      TargetFunctionArn: !GetAtt StatusFunction.Arn

  StatusFunctionPolicy:
    Type: AWS::Lambda::ResourcePolicy
    Properties:
      ResourceArn: !GetAtt StatusFunction.Arn
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: FunctionURLAllowPublicAccess
            Effect: Allow
            Principal: '*'
            Action: lambda:InvokeFunctionUrl
            Resource: !GetAtt StatusFunction.Arn
            Condition:
              StringEquals:
                lambda:FunctionUrlAuthType: NONE
          - Sid: FunctionURLInvokeAllowPublicAccess
            Effect: Allow
            Principal: '*'
            Action: lambda:InvokeFunction
            Resource: !GetAtt StatusFunction.Arn
            Condition:
              Bool:
                lambda:InvokedViaFunctionUrl: 'true'

Outputs:
  StatusUrl:
    Description: Public HTTPS endpoint for the status API.
    Value: !GetAtt StatusFunctionUrl.FunctionUrl
Update the CloudFormation stack

The saved template defines the permission change. Updating the existing stack lets CloudFormation add the policy while retaining the function URL and its current address.

This endpoint becomes public

Anyone who has the function URL can invoke the function while this policy exists. Requests can therefore consume Lambda usage and generate CloudWatch Logs.

  • Return to the AWS CloudFormation console from earlier.
  • Select the platform-status-api stack.
  • Choose Update.
  • Select Replace existing template.
  • Select Upload a template file.
  • Upload the saved template.yaml file from your aws-status-api repository.
  • Review the change preview.

The preview should show that CloudFormation adds only the StatusFunctionPolicy resource.

  • Choose Update stack.
  • Wait for the stack status to reach UPDATE_COMPLETE.

Expect the update to take a few minutes while CloudFormation creates the policy. Once it finishes, your stack has the access guardrail required by the function URL.

Update not completing?

  • Open the stack's Events tab to identify which resource stopped the update.
  • Check the uploaded template for misplaced indentation under StatusFunctionPolicy.
  • Confirm your signed-in identity can update Lambda resource-based policies.

Ask for help: Help me diagnose why my CloudFormation update failed after I added StatusFunctionPolicy.

Verify the public endpoint

The stack update changes the endpoint's authorization path. A browser refresh now tests whether both policy statements work together.

Before you refresh, what response do you expect from the endpoint now?

  • Return to the browser tab containing the StatusUrl from earlier.
  • Refresh the page.

You should see {"service":"platform-status-api","status":"ok"} in the browser. The previous 403 Forbidden response is gone.

That access gap is closed. Your public endpoint now invokes the function through the scoped URL path.

Still seeing 403 Forbidden?

  • Confirm the stack reached UPDATE_COMPLETE before refreshing the page.
  • Check that both policy statements appear under StatusFunctionPolicy in the uploaded template.
  • Verify that ResourceArn references StatusFunction.Arn.

Ask for help: Help me find why my Lambda function URL still returns 403 Forbidden after the stack update.

Your status API now has a working public path with explicit access rules. Next, you will ship a reviewed update and trace its invocation through operational logs.

Ship a Reviewed and Observable Update

The last step made your public Lambda function URL return a successful status response. A live endpoint still needs a safe release process for future changes.

An AWS CloudFormation change set previews the effect of your revised template before it changes the stack. This review gate helps catch unexpected infrastructure changes.

This step also connects the browser response to Amazon CloudWatch Logs. Recording the working template in Git gives you a versioned release point.

In this step, get ready to:
  • Prepare a visible health response update.
  • Deploy the revised template through a reviewed change set.
  • Record operational evidence in version control.
Change the health response

A release needs an observable difference between the old deployment and the new one. Changing the health value gives you a precise browser check after deployment.

  • Find the response body inside StatusFunction in template.yaml that contains "status":"ok".
  • Replace the response body line with this updated line:
              "body": "{\"service\":\"platform-status-api\",\"status\":\"healthy\"}"

What Does This Change Do?

The response body now uses healthy as the release marker. The service name stays unchanged so you can isolate the status update during verification.

  • Save template.yaml.
  • Confirm the saved response line contains "status":"healthy".

Response Line Looking Wrong?

Check that the surrounding escaped quotes remain in place. A missing backslash makes the inline Python string invalid.

Ask for help: Help me compare my updated response body with the expected template line.

✔️ Awesome, I've got everything!

Your saved template now contains the new health value. It is ready for a reviewed deployment.

ⓧ I'd like to double check the full code

Compare your saved template.yaml with this complete version.

AWSTemplateFormatVersion: '2010-09-09'
Description: Repeatable serverless status API with explicit logging and function URL access.

Resources:
  StatusLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-function'
      RetentionInDays: 7

  StatusFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: WriteStatusLogs
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: !GetAtt StatusLogGroup.Arn

  StatusFunction:
    Type: AWS::Lambda::Function
    DependsOn: StatusLogGroup
    Properties:
      FunctionName: !Sub '${AWS::StackName}-function'
      Runtime: python3.14
      Handler: index.lambda_handler
      Architectures:
        - arm64
      MemorySize: 128
      Timeout: 3
      Role: !GetAtt StatusFunctionRole.Arn
      Code:
        ZipFile: |
          def lambda_handler(event, context):
              print("status check received")
              return {
                  "statusCode": 200,
                  "headers": {
                      "Content-Type": "application/json"
                  },
                  "body": "{\"service\":\"platform-status-api\",\"status\":\"healthy\"}"
              }

  StatusFunctionUrl:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      InvokeMode: BUFFERED
      TargetFunctionArn: !GetAtt StatusFunction.Arn

  StatusFunctionPolicy:
    Type: AWS::Lambda::ResourcePolicy
    Properties:
      ResourceArn: !GetAtt StatusFunction.Arn
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: FunctionURLAllowPublicAccess
            Effect: Allow
            Principal: '*'
            Action: lambda:InvokeFunctionUrl
            Resource: !GetAtt StatusFunction.Arn
            Condition:
              StringEquals:
                lambda:FunctionUrlAuthType: NONE
          - Sid: FunctionURLInvokeAllowPublicAccess
            Effect: Allow
            Principal: '*'
            Action: lambda:InvokeFunction
            Resource: !GetAtt StatusFunction.Arn
            Condition:
              Bool:
                lambda:InvokedViaFunctionUrl: 'true'

Outputs:
  StatusUrl:
    Description: Public HTTPS endpoint for the status API.
    Value: !GetAtt StatusFunctionUrl.FunctionUrl

What Should You Compare?

  • The StatusFunction response body uses healthy.
  • Every existing resource definition remains unchanged.
Review the change set

The saved template describes your intended state. A change set reveals how CloudFormation plans to reach that state before it touches the running endpoint.

  • Switch back to the AWS CloudFormation console from earlier.
  • Select the platform-status-api stack.
  • Choose Stack actions.
  • Choose Create a change set.
  • Choose Replace existing template.
  • Choose Upload a template file.
  • Select template.yaml from the aws-status-api repository.

Why Use a Change Set?

A change set separates review from execution. You can inspect the planned resource changes before the live stack adopts the revised template.

  • Leave the existing stack settings unchanged.
  • Submit the change set.

The change set reaches CREATE_COMPLETE when its preview is ready.

That's your review gate in place. The proposed update is visible before the endpoint changes.

  • Review the change list for the modified StatusFunction resource.
  • Confirm no other resource is listed as modified.
  • Choose Execute change set.
  • Wait for the platform-status-api stack to reach UPDATE_COMPLETE.

CloudFormation has applied the reviewed function update. The existing role remains in place.

The log group remains in place. The public access policy also remains in place.

Does the Preview Show Extra Changes?

Leave the change set unexecuted if it lists unexpected resources. Compare template.yaml with the full-code tab before creating a new preview.

Ask for help: Help me identify unexpected resources in my change set.

Verify, observe, and commit

The endpoint proves the user-visible result. The function's log stream proves that the deployed handler processed your browser request.

Before you refresh, predict whether the browser will still show ok or return healthy.

  • Return to the browser tab containing the StatusUrl from earlier.
  • Refresh the page.

You should see {"service":"platform-status-api","status":"healthy"} in the browser.

Your reviewed release is live. The endpoint now exposes the revised health status through the existing public guardrail.

  • Return to the stack-created Event Function in the AWS Lambda console from earlier.
  • Choose Monitor.
  • Choose View CloudWatch logs.
  • Choose the latest log stream for platform-status-api-function.

The latest stream should contain status check received. This line proves that your handler processed the browser request.

You should also see separate START, END, and REPORT records for the invocation.

Missing the Latest Invocation?

Refresh the endpoint again before checking the newest log stream. Confirm that the selected stream belongs to platform-status-api-function.

Ask for help: Help me find my latest browser invocation in the function logs.

The cloud behavior now matches your local template. A Git commit records this working infrastructure definition as a durable release point.

  • Stage template.yaml in the VS Code terminal by running:
git add template.yaml

What Does This Command Do?

This command stages the current contents of template.yaml for the next commit. Only the saved infrastructure definition enters this release point.

Before you create the commit, predict what Git will report after it records the staged template.

  • Create the infrastructure commit by running:
git commit -m "Build repeatable AWS status API"

What Does This Command Record?

This command creates a commit from the staged template. The message identifies the repeatable status API as the completed infrastructure release.

Git should report that it created a commit containing the template change. Your reviewed infrastructure definition is now version controlled.

Did Git Stop Before Committing?

A Git installation without an author identity may stop before creating the commit. Follow the terminal's configuration guidance before running the same commit command again.

Ask for help: Help me interpret the Git message that stopped my infrastructure commit.

Secret mission

Turn the Stack into a Two-Environment Golden Path

Turn your fixed status API into a reusable golden path with a controlled environment parameter. You will use one template to deploy isolated dev and test stacks whose endpoints and structured logs identify their environment.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep both environments available, pause public access, or delete the project resources entirely. Normal lab usage is expected to cost $0 within the published free tiers, but unexpected traffic can create usage-based charges.

Cost warning

AWS CloudFormation adds no charge for the resource types used in this project.

AWS Lambda requests and duration remain usage based. Amazon CloudWatch Logs ingestion and storage also remain usage based.

Amazon S3 storage for uploaded template objects remains usage based. Normal lab usage is expected to fit within the published free tiers.

Each public Lambda function URL can receive traffic from anyone who has its address. Pausing public access or deleting both stacks is the safer choice when your demonstration is complete.

Resources you used:

  • Development stack platform-status-api.
  • Test stack platform-status-api-test.
  • Per-stack AWS resources: a Lambda function, AWS Identity and Access Management (IAM) execution role, CloudWatch Logs log group, Lambda function URL, and resource policy.
  • Regional CloudFormation-managed Amazon S3 upload bucket containing the project template objects.
  • Local aws-status-api Git repository containing template.yaml.

Keep everything running

No action is needed. Choose this option if you want to keep demonstrating both environments or continue refining the golden path.

  • Both function URLs remain public. Anyone with either URL can invoke its Lambda function.
  • Each CloudWatch Logs log group retains invocation records for seven days.
  • Unexpected endpoint traffic can consume the available free-tier allowances.
  • The local aws-status-api repository remains available for future updates.

Pause - I'll come back to this later

Pause public access while preserving both environments. Removing the resource policy keeps each function URL allocated while blocking unauthenticated invocation.

  • Return to template.yaml in the aws-status-api repository.
  • Locate the entire resource block that begins with StatusFunctionPolicy:.
  • Delete the StatusFunctionPolicy resource block.
  • Save template.yaml.
  • Switch back to AWS CloudFormation in us-east-1.
  • Use the direct stack update flow from earlier to upload the revised template to platform-status-api.
  • Wait for platform-status-api to reach UPDATE_COMPLETE.
  • Repeat the same direct update flow for platform-status-api-test.
  • Open each stack's StatusUrl after both updates finish.

You should see 403 Forbidden from both URLs. This confirms that the endpoints remain allocated while public invocation is paused.

Delete - I don't want to use this again

Deleting both stacks is permanent for the deployed resources. Your version-controlled template.yaml still preserves the golden path until you remove the local repository.

  • Return to AWS CloudFormation in us-east-1.
  • Select the platform-status-api stack.
  • Choose Delete.
  • Confirm Delete.
  • Wait for the deletion to reach DELETE_COMPLETE.
  • Repeat the same deletion flow for platform-status-api-test.
  • Confirm that neither project stack remains in the stack list.

AWS CloudFormation removes all resources owned by each stack. Both public endpoint URLs stop working.

Stack deletion failed?

  • Inspect the latest stack events to identify the resource that blocked deletion.
  • Resolve the listed dependency or permission problem.
  • Choose Delete again.

help me troubleshoot a failed CloudFormation stack deletion.

The regional Amazon S3 upload bucket sits outside the project stacks. It may contain templates for other CloudFormation stacks.

  • Return to Amazon S3 in the same AWS console.
  • Locate the regional CloudFormation-managed upload bucket used for this project's template uploads.
  • Review the bucket objects to identify the project template uploads.
  • Delete only the template objects created for this project.
  • Delete the bucket only if it is dedicated to this project.
  • Preserve the bucket if another stack uses it.
  • Confirm that the project template objects are absent.

Your local Git repository is separate from the deployed AWS resources. Remove it only if you no longer need the template or its commit history.

  • Close the editor window that has aws-status-api open.
  • Use Windows File Explorer to locate aws-status-api in the folder where Step 1 created it.
  • Delete the aws-status-api folder.
  • Confirm that aws-status-api no longer appears in that folder.

Nice Work!

Nice Work!

You made it! Your public status API now deploys from one version-controlled AWS CloudFormation template. The same golden path produces isolated dev and test environments with visible health responses.

You learned how to:

  • Built a serverless status API on AWS Lambda with a least-privilege IAM role. Its Lambda function URL returns a JSON health response.
  • Found the designed access failure when the public endpoint returned 403 Forbidden. Fixed the gap with a scoped resource-based policy that permits invocation only through the function URL path.
  • Used an AWS CloudFormation change set to preview the infrastructure update. Verified the revised behavior in Amazon CloudWatch Logs. Recorded the working template.yaml in Git.
  • Secret Mission: Turned one parameterized template into a two-environment golden path. The independent dev and test endpoints report their own environment.

Ready to quiz yourself?