Keyless Serverless CI/CD on AWS
Deploy a Lambda endpoint from GitHub Actions with keyless AWS OIDC.
Introduction
30 Second Summary
A small code change can take seconds. Getting that change online safely often becomes a manual handoff that is easy to miss and hard to prove.
In this project, you will build a public AWS Lambda health endpoint that updates from a Git push through keyless CI/CD. Its deployment path combines AWS CloudFormation with GitHub Actions authentication through OpenID Connect.
What You'll Build
You'll watch one Git push turn the live health response from v1 to v2 through a successful keyless workflow.
By the end of this project, you'll have:
- A browser-accessible HTTPS health endpoint that reports a healthy status with the deployed release version.
- A keyless delivery loop where a push to main triggers a CloudFormation update with short-lived credentials.
- Observable deployment proof that connects the workflow's assumed AWS identity to each request's Amazon CloudWatch Logs record.
- Secret Mission: Publish the deployed endpoint and function name in every successful workflow run.
Are there any prerequisites?
Your Windows environment needs Git, Visual Studio Code, plus AWS CLI.
You also need a GitHub account plus an AWS account with permission to create this project's cloud resources.
Before We Start
Before any hands-on work begins, commit to building a keyless GitHub Actions delivery system for an AWS CloudFormation-managed AWS Lambda health endpoint. The finished project gives you a portfolio-ready demonstration of automated delivery.
Set Up the Portfolio Repository
A repeatable delivery system needs one public source of truth. It also needs a verified AWS identity before any infrastructure is created.
You’ll establish that source of truth on GitHub. You’ll also prepare Visual Studio Code plus Windows PowerShell for the infrastructure work ahead.
In this step, get ready to:
- Establish the public serverless-cicd-status repository as your local workspace.
- Add the empty infra folder plus the .github/workflows folder.
- Verify your AWS command-line environment against the intended identity.
Create the public repository
The repository becomes the source that every later deployment follows. Public visibility also keeps standard GitHub-hosted runner usage on the prescribed no-cost path.
- Open the new repository page in the browser where you use GitHub.
- Choose your personal account from the Owner dropdown.
- Enter serverless-cicd-status in the Repository name field.
- Choose Public for the repository visibility.
- Turn on Add README.
- Leave the .gitignore option unchanged.
- Leave the license option unchanged.
- Click Create repository.
You’ll see the serverless-cicd-status repository page with public visibility. The README gives the repository an initial main branch that your later pushes can update.
Why Make the Repository Public?
Standard GitHub-hosted runners are free for public repositories. Their included use is unlimited.
Public visibility also lets your completed workflow act as portfolio evidence. Never commit credentials or private values to this repository.
- Click Code above the repository files.
- Select HTTPS in the clone menu.
- Click the copy icon beside the repository URL.
- Paste the copied URL here: your repository HTTPS URL.
A clone gives you a complete local Git repository. Git also connects the local copy to GitHub through a remote named origin.
- Press the Windows key to open Windows search.
- Type Visual Studio Code.
- Press Enter to open Visual Studio Code.
- Press Ctrl+Shift+P to open the Command Palette.
- Type Git: Clone.
- Select Git: Clone from the command list.
- Paste your repository HTTPS URL into the URL prompt.
- Press Enter to start the clone.
What Does Cloning Create?
Visual Studio Code asks where the new local repository should live. Choosing your Desktop creates a serverless-cicd-status folder that is easy to find.
The local repository keeps its connection to GitHub. Later pushes use that connection to publish your changes.
- Select your Desktop as the local destination.
- Confirm the Desktop as the repository destination.
- Open the cloned repository when Visual Studio Code prompts you.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
Good progress. The Explorer sidebar now shows serverless-cicd-status with its README.
Clone Not Completing?
- Return to the repository page to copy the URL from the HTTPS section again.
- Confirm that you selected the Desktop as the clone destination.
Ask for help diagnosing the clone. You can also visit GitHub Community for repository support.
Create the project folders
The infra folder will hold the infrastructure templates. The .github/workflows folder is reserved for the delivery workflow.
- Hover over the serverless-cicd-status heading in the Explorer sidebar.
- Click the New Folder icon.
- Type infra.
- Press Enter.
- Click the New Folder icon again.
- Type .github/workflows.
- Press Enter.
- Expand .github in the Explorer sidebar.
You’ll see the empty infra folder. You’ll also see workflows nested under .github.
Why Keep Workflows Empty?
The empty workflow folder preserves the manual deployment stage of the project. You need to experience the source-to-cloud gap before automation closes it.
Git begins tracking these folders when later steps add files inside them. For now, the folder structure exists in your local workspace.
- Click View in the Visual Studio Code menu bar.
- Click Terminal.
- Open the Launch Profile dropdown in the terminal panel.
- Select PowerShell.
The PowerShell prompt starts inside the open serverless-cicd-status workspace. This terminal is ready to run project commands from the correct repository.
Verify AWS CLI and your identity
The AWS Command Line Interface connects PowerShell to your AWS account. This project continues with major version v2.
- Check the installed AWS CLI version by running:
aws --version
What Does This Check Show?
The output reports the installed AWS CLI release. Use its major version to choose the matching tab below.
✔️ AWS CLI v2 is installed
Your AWS CLI installation uses the required major version.
- Continue to the identity check below.
ⓧ AWS CLI v1 is installed
AWS CLI v1 cannot continue on this project path. Install AWS CLI v2 for your Windows user.
- Install AWS CLI v2 by running this PowerShell command:
irm https://awscli.amazonaws.com/v2/install.ps1 | iex
What Does This Installer Do?
This official installer sets up AWS CLI v2 for the current Windows user. A new PowerShell session loads the updated command.
- Close the current PowerShell terminal tab.
- Open a new PowerShell terminal from the Launch Profile dropdown.
- Check the installed version again by running:
aws --version
What Should the New Session Confirm?
The new output should identify AWS CLI v2 as the active installation. You can continue once the major version is correct.
AWS CLI Still Reports v1?
- Close every open PowerShell terminal before starting a fresh session.
- Confirm that the installer completed without a network interruption.
Ask for help fixing the AWS CLI version. You can also visit the AWS CLI community on AWS re:Post for installation support.
The version check confirms that PowerShell can reach the required CLI. The next check reveals which authenticated AWS identity receives your commands.
Before you run the check, which AWS identity do you expect this terminal to reveal?
- Verify the authenticated AWS identity by running:
aws sts get-caller-identity
What Does This Identity Check Show?
- The Account value identifies the AWS account receiving your commands.
- The Arn value describes the authenticated AWS principal.
- The UserId value identifies that principal inside AWS.
You’ll see Account, Arn, plus UserId in the terminal output. That is the setup confirmed: your repository commands now have a known AWS identity behind them.
Identity Check Not Working?
- Confirm that the AWS sign-in or credentials method you prepared before this project is still active.
- Confirm that the new PowerShell terminal uses the AWS CLI v2 installation you verified above.
Ask for help checking the active AWS identity. You can also visit the AWS CLI community on AWS re:Post for command-line support.
Your public repository is ready. Next, you’ll use this verified AWS identity to deploy the first working endpoint manually.
Deploy the First Endpoint Manually
Your repository is ready. Your authenticated AWS identity is ready too. The next goal is a live health endpoint on AWS Lambda that is managed by AWS CloudFormation.
A manual deployment gives you a visible v1 baseline before automation enters the project. This baseline makes the source-to-cloud gap unmistakable in the next step.
In this step, get ready to:
- Define the endpoint infrastructure in infra/app.yaml.
- Validate the CloudFormation template.
- Deploy the stack and open the public v1 health endpoint.
Create the infrastructure template
Infrastructure as code stores the desired cloud setup in a file that you can review or deploy repeatedly. Your template defines the function runtime plus every supporting resource required by the endpoint.
- In the Explorer sidebar in VS Code, hover over the existing infra folder and click the New File icon.
- Type app.yaml and press Enter.
You should see app.yaml inside the infra folder.
- Add the template header plus the log group by pasting this first block into infra/app.yaml.
AWSTemplateFormatVersion: "2010-09-09"
Description: Keyless CI/CD portfolio endpoint and its runtime resources.
Resources:
StatusLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/lambda/serverless-cicd-status
RetentionInDays: 1
What does this section do?
- The template header identifies the CloudFormation template format.
- The description records the purpose of this stack.
- The log group stores invocation output in Amazon CloudWatch Logs for one day.
- Save infra/app.yaml.
You should see StatusLogGroup under the Resources section in your saved file.
Does the YAML look misaligned?
Keep each indentation level at two spaces. Tabs can change the YAML structure even when the text looks close.
Use the vertical indentation guides in VS Code to check that Type and Properties sit beneath StatusLogGroup.
help me check the log group YAML.
The function needs permission to write its structured invocation record. A dedicated execution role keeps that runtime access separate from deployment access.
- Append the execution role below the StatusLogGroup resource by pasting this block.
StatusFunctionRole:
Type: AWS::IAM::Role
Properties:
RoleName: serverless-cicd-lambda-execution
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
How does the execution role work?
The AWS Identity and Access Management role trusts the Lambda service. The inline policy lets the function create a log stream plus write events to the project log group.
The resource reference connects the permission directly to StatusLogGroup.
- Save infra/app.yaml.
You should see StatusFunctionRole aligned with StatusLogGroup under Resources.
Is the role nested under the log group?
Move StatusFunctionRole left until it has the same two-space indentation as StatusLogGroup.
help me align the CloudFormation resources.
The next resource contains the Python handler that answers each health request. This first version returns the visible release label v1.
- Append the Lambda function below StatusFunctionRole by pasting this block.
StatusFunction:
Type: AWS::Lambda::Function
DependsOn:
- StatusLogGroup
- StatusFunctionRole
Properties:
FunctionName: serverless-cicd-status
Description: Returns the health and deployed version of the portfolio service.
Runtime: python3.14
Handler: index.lambda_handler
Role: !GetAtt StatusFunctionRole.Arn
MemorySize: 128
Timeout: 3
Code:
ZipFile: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"headers": {
"content-type": "application/json"
},
"body": json.dumps({
"service": "serverless-cicd-status",
"status": "healthy",
"version": "v1"
})
}
What does the function do?
- The function waits for the log group plus the execution role before AWS creates it.
- The python3.14 runtime executes the inline lambda_handler function.
- The handler returns an HTTP success response containing the service name plus its health status plus its deployed version.
- Save infra/app.yaml.
You should now see the StatusFunction resource with the inline Python response ending in "version": "v1".
Is the Python indentation uneven?
Keep the Python code beneath ZipFile: | indented consistently. Keep the dictionary contents nested beneath the handler.
help me check the inline Lambda code.
A Function URL turns the Lambda function into an HTTPS endpoint. Two permission resources allow public requests to reach the function through that URL.
- Append the Function URL plus its public permissions below StatusFunction by pasting this block.
StatusFunctionUrl:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
InvokeMode: BUFFERED
TargetFunctionArn: !GetAtt StatusFunction.Arn
FunctionUrlInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunctionUrl
FunctionName: !Ref StatusFunction
FunctionUrlAuthType: NONE
Principal: "*"
FunctionInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref StatusFunction
InvokedViaFunctionUrl: true
Principal: "*"
How does the public URL work?
The URL uses unauthenticated access plus buffered responses. The first permission authorizes Function URL invocation.
The second permission authorizes the underlying Lambda invocation only through the Function URL. Anyone with the generated address can call this endpoint.
- Save infra/app.yaml.
You should see three aligned resources named StatusFunctionUrl, FunctionUrlInvokePermission, plus FunctionInvokePermission.
Is the endpoint missing a permission?
Check that both permission resources are present. Confirm that each one references StatusFunction.
help me check the Function URL permissions.
A stack output exposes the generated HTTPS address after deployment. This gives you a stable place to retrieve the endpoint without searching through individual resources.
- Append the output section at the end of infra/app.yaml by pasting this block.
Outputs:
FunctionUrl:
Description: Public HTTPS health endpoint.
Value: !GetAtt StatusFunctionUrl.FunctionUrl
What does the output expose?
The FunctionUrl output reads the address generated for StatusFunctionUrl. You will retrieve this output after the stack is deployed.
- Save infra/app.yaml.
You should see the top-level Outputs section aligned with Resources.
Is Outputs inside Resources?
Move Outputs all the way left. Top-level sections have no indentation.
help me align the Outputs section.
The endpoint also records a structured line for every request. The record contains a fixed event name plus the unique Lambda request identifier.
- Inside lambda_handler, find the line beginning with return {.
- Insert this logging block directly above that return line.
log_record = {
"event": "status_check",
"aws_request_id": context.aws_request_id
}
print(json.dumps(log_record))
What does the log record capture?
The function writes a JSON record whenever the endpoint runs. The request ID links that application record to one specific Lambda invocation.
- Save infra/app.yaml.
You should see log_record plus print(json.dumps(log_record)) immediately before the response.
Is the log block outside the handler?
Align log_record with the existing return statement. Both lines belong inside lambda_handler.
help me place the Lambda logging block.
✔️ Awesome, I've got everything!
Great. Make sure infra/app.yaml is saved before validating it.
ⓧ I'd like to double check the full code
Compare your complete infra/app.yaml file with this reference.
AWSTemplateFormatVersion: "2010-09-09"
Description: Keyless CI/CD portfolio endpoint and its runtime resources.
Resources:
StatusLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/lambda/serverless-cicd-status
RetentionInDays: 1
StatusFunctionRole:
Type: AWS::IAM::Role
Properties:
RoleName: serverless-cicd-lambda-execution
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
- StatusFunctionRole
Properties:
FunctionName: serverless-cicd-status
Description: Returns the health and deployed version of the portfolio service.
Runtime: python3.14
Handler: index.lambda_handler
Role: !GetAtt StatusFunctionRole.Arn
MemorySize: 128
Timeout: 3
Code:
ZipFile: |
import json
def lambda_handler(event, context):
log_record = {
"event": "status_check",
"aws_request_id": context.aws_request_id
}
print(json.dumps(log_record))
return {
"statusCode": 200,
"headers": {
"content-type": "application/json"
},
"body": json.dumps({
"service": "serverless-cicd-status",
"status": "healthy",
"version": "v1"
})
}
StatusFunctionUrl:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
InvokeMode: BUFFERED
TargetFunctionArn: !GetAtt StatusFunction.Arn
FunctionUrlInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunctionUrl
FunctionName: !Ref StatusFunction
FunctionUrlAuthType: NONE
Principal: "*"
FunctionInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref StatusFunction
InvokedViaFunctionUrl: true
Principal: "*"
Outputs:
FunctionUrl:
Description: Public HTTPS health endpoint.
Value: !GetAtt StatusFunctionUrl.FunctionUrl
How to use this reference
Compare the resource names plus indentation plus the v1 response with your saved file. The next command checks whether CloudFormation can parse the result.
Validate and deploy the stack
Template validation checks the file structure before AWS creates anything. This catches malformed YAML plus unsupported template declarations early.
- Validate infra/app.yaml from the PowerShell terminal you used earlier by running this command:
aws cloudformation validate-template --template-body file://infra/app.yaml --region us-east-1
What does this command check?
The AWS CLI sends the local template to CloudFormation for validation in us-east-1. It checks whether the service can read the template before deployment.
PowerShell should print template information without reporting a validation problem. Your file is ready for deployment once the command completes successfully.
Did validation fail?
Check the line mentioned by the validation response. YAML failures usually come from inconsistent indentation or a property nested beneath the wrong resource.
Compare your file with the full-code reference above. Pay particular attention to Resources plus Outputs.
help me fix my CloudFormation validation problem.
The next command creates live AWS resources. The endpoint is public, so requests to its generated URL use your account's Lambda allowance.
CloudFormation can take a few minutes to create the role plus the function plus the endpoint. A short wait here is expected.
- Deploy the application stack by running this command:
aws cloudformation deploy --template-file infra/app.yaml --stack-name serverless-cicd-app --capabilities CAPABILITY_NAMED_IAM --region us-east-1
What does the deployment command do?
- The command deploys infra/app.yaml as a stack named serverless-cicd-app.
- The named IAM capability acknowledges that the template creates a role with an explicit name.
- CloudFormation creates the resources in us-east-1.
PowerShell returns control after CloudFormation finishes the deployment. That confirms the stack operation completed.
Did the deployment stop?
Read the deployment response for the resource that could not be created. Permission failures mean the authenticated AWS identity cannot create one of the resources in the template.
If CloudFormation reports a template problem, rerun the validation command after comparing infra/app.yaml with the full-code reference.
help me diagnose the CloudFormation deployment.
Verify the live v1 endpoint
The stack description contains the generated endpoint address. This final check proves that the template created a callable service instead of only defining resources on paper.
Before you run the check, what stack output do you expect to find for the public endpoint?
- Inspect the deployed stack by running this command:
aws cloudformation describe-stacks --stack-name serverless-cicd-app --region us-east-1
What does this command reveal?
The stack description includes its current state plus its declared outputs. The FunctionUrl output contains the generated HTTPS endpoint.
You should see the deployed stack information plus an output named FunctionUrl.
- Record the FunctionUrl output here: your FunctionUrl output.
- Press the Windows key to open Windows search.
- Type the name of your browser and press Enter.
- Paste your FunctionUrl output into the browser address bar.
- Press Enter to invoke the endpoint.
You should see {"service":"serverless-cicd-status","status":"healthy","version":"v1"} in the browser.
Why does the endpoint open without signing in?
The Function URL uses public access. The two permission resources restrict that public invocation path to the generated Function URL.
This makes the health check easy to demonstrate. It also means anyone with the address can invoke it.
Does the endpoint fail to load?
Copy the complete HTTPS value from the FunctionUrl output. Missing characters can point the browser at an invalid address.
Confirm that the deployment command completed before refreshing the browser. The endpoint only exists after the stack finishes creating its resources.
help me diagnose the Function URL response.
That is your first live result. The browser now proves that serverless-cicd-app is serving a healthy v1 release.
Your manual baseline is live. Next, you will change the source to v2 without deploying it so you can see the delivery gap for yourself.
Expose the Manual Deployment Gap
Your endpoint is managed by AWS CloudFormation. The live service now provides a working v1 baseline.
A repeatable delivery system needs source changes to reach the live service. This step tests whether a Git push carries a new release version into that service.
In this step, get ready to:
- Change the source release marker from v1 to v2.
- Publish the committed source change to main.
- Compare the source version with the live AWS Lambda Function URL.
Change the source version to v2
The response version acts as a visible release marker. A one-character edit lets you compare the repository state with the deployed state.
- In Visual Studio Code, switch back to infra/app.yaml from earlier.
- Find this line inside the body JSON response:
"version": "v1"
What does this line show?
This line sets the release marker returned by the endpoint. The current value identifies the manually deployed baseline.
- Replace v1 with v2 so the line looks like this:
"version": "v2"
What changed?
The template now declares v2 as the desired release version. The surrounding infrastructure definition remains unchanged.
- Save infra/app.yaml.
- Confirm the saved version line now displays v2.
Your local template now contains the new release marker.
Cannot find the version line?
- Expand the inline Python beneath ZipFile if that section is folded in the editor.
- Search infra/app.yaml for "status": "healthy" to locate the response body.
Ask for help if the version marker is still missing: help me locate the version response in infra/app.yaml
✔️ Awesome, I've got everything!
Great. Your saved infra/app.yaml now declares release version v2.
ⓧ I'd like to double check the full code
The complete file should now match this reference:
AWSTemplateFormatVersion: "2010-09-09"
Description: Keyless CI/CD portfolio endpoint and its runtime resources.
Resources:
StatusLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: /aws/lambda/serverless-cicd-status
RetentionInDays: 1
StatusFunctionRole:
Type: AWS::IAM::Role
Properties:
RoleName: serverless-cicd-lambda-execution
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
- StatusFunctionRole
Properties:
FunctionName: serverless-cicd-status
Description: Returns the health and deployed version of the portfolio service.
Runtime: python3.14
Handler: index.lambda_handler
Role: !GetAtt StatusFunctionRole.Arn
MemorySize: 128
Timeout: 3
Code:
ZipFile: |
import json
def lambda_handler(event, context):
log_record = {
"event": "status_check",
"aws_request_id": context.aws_request_id
}
print(json.dumps(log_record))
return {
"statusCode": 200,
"headers": {
"content-type": "application/json"
},
"body": json.dumps({
"service": "serverless-cicd-status",
"status": "healthy",
"version": "v2"
})
}
StatusFunctionUrl:
Type: AWS::Lambda::Url
Properties:
AuthType: NONE
InvokeMode: BUFFERED
TargetFunctionArn: !GetAtt StatusFunction.Arn
FunctionUrlInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunctionUrl
FunctionName: !Ref StatusFunction
FunctionUrlAuthType: NONE
Principal: "*"
FunctionInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref StatusFunction
InvokedViaFunctionUrl: true
Principal: "*"
Outputs:
FunctionUrl:
Description: Public HTTPS health endpoint.
Value: !GetAtt StatusFunctionUrl.FunctionUrl
What should I check?
Only the inline response version has moved to v2. Every infrastructure resource remains unchanged so this test isolates the release marker.
Publish the source change
A commit records the exact template edit in source history. A push sends that commit to the public GitHub repository.
- Publish the v2 template to main from PowerShell by running these commands:
git add .
git commit -m "Change endpoint to v2"
git push
What do these commands do?
- The first command stages the saved template change for the next commit.
- The second command records the staged change with a descriptive message.
- The third command sends the commit to the remote branch.
- Confirm the PowerShell output shows that the commit reached the remote repository.
Your public repository now holds the v2 source change. The push gives you a clear point to compare against the live endpoint.
Push did not finish?
- Save infra/app.yaml if the commit contains no change.
- Repeat the command block after saving the file.
- Complete the GitHub sign-in flow if Git requests authentication.
Ask for targeted help if the push still fails: help me diagnose this Git push
Reveal the deployment gap
The repository history now contains v2. The browser gives you a separate view of the deployed state.
- Switch back to the Function URL browser tab from earlier.
- Arrange the browser beside Visual Studio Code.
- Keep the v2 line in infra/app.yaml visible.
Before you refresh the endpoint, do you expect the browser to show v1 or v2?
- Refresh the Function URL browser tab.
The browser still shows a healthy response with version v1. Your source says v2, so the two states are visibly out of sync.
Why is the endpoint still on v1?
The repository now declares v2. AWS CloudFormation still manages the deployed v1 function.
This source-to-runtime mismatch is a controlled example of deployment drift. The push updated source history without executing a cloud deployment.
You made the missing delivery mechanism visible through a real source-to-runtime mismatch. Next, you will establish OpenID Connect trust for repository-scoped credentials.
Establish Keyless GitHub-to-AWS Trust
Your v2 template is now in GitHub. The live endpoint still returns v1, which proves that your source changes have no delivery path to AWS.
The next deployment needs GitHub Actions to authenticate without stored access keys. You will establish that path with OpenID Connect and an AWS Identity and Access Management role that receives short-lived credentials for this repository.
In this step, get ready to:
- Record the repository owner ID and repository ID used by the immutable trust subject.
- Define the OIDC provider and deployment role in an AWS CloudFormation template.
- Deploy the trust stack and store the AWS account ID as a repository variable.
Record GitHub IDs and check the OIDC provider
A repository name can identify where code lives. The immutable OIDC subject also includes numeric owner and repository IDs so AWS can restrict trust to this exact repository on main.
- Open the public serverless-cicd-status repository page in your browser.
- Record the owner name shown before serverless-cicd-status in the repository address: YOUR_OWNER.
- Replace YOUR_OWNER in https://api.github.com/repos/YOUR_OWNER/serverless-cicd-status with the owner name you recorded.
- Paste the completed address into your browser address bar.
- Press Enter.
You will see the repository metadata as JSON. It contains the top-level repository id and a separate id inside the owner object.
Finding the two values is a little fiddly because the page contains several fields with similar names. The top-level value belongs to the repository.
- Locate the top-level id near the beginning of the response.
- Record the top-level value here: YOUR_REPOSITORY_ID.
- Locate the nested owner object.
- Record the nested owner id here: YOUR_OWNER_ID.
Why use immutable IDs?
The trust subject combines the owner name with its numeric ID. It also combines the repository name with its numeric ID.
The final subject ends with ref:refs/heads/main. Only workflow runs from this exact repository and branch can request the deployment role.
- Switch back to the PowerShell window authenticated to AWS CLI v2.
- List the account's existing OIDC providers by running this command:
aws iam list-open-id-connect-providers
What does this command check?
The command returns the ARNs of the OIDC providers registered in this AWS account. You are checking whether the account already trusts the GitHub token service.
No Existing GitHub Provider
No returned provider corresponds to https://token.actions.githubusercontent.com. The bootstrap stack creates it for this account.
- Leave the ExistingGitHubOidcProviderArn parameter empty when you deploy the template.
Existing GitHub Provider
The returned list includes the GitHub OIDC provider. Reusing it prevents the bootstrap stack from trying to create a duplicate account-level provider.
- Copy the ARN belonging to the GitHub OIDC provider.
- Record the ARN here: your existing GitHub OIDC provider ARN.
Provider list command failed?
Confirm that PowerShell is still using the authenticated AWS CLI v2 setup from Step 1. An expired local sign-in or the wrong AWS profile can prevent the provider list from loading.
Share the command output without including credentials when you ask for help with the OIDC provider check.
Create and deploy the trust stack
The bootstrap template has two jobs. It creates or reuses the GitHub OIDC provider before defining a deployment role with repository-scoped trust and deployment permissions.
- In the VS Code Explorer sidebar, right-click the infra folder.
- Select New File.
- Enter bootstrap.yaml.
- Add the template parameters and provider condition by copying this code:
AWSTemplateFormatVersion: "2010-09-09"
Description: GitHub OIDC trust and deployment permissions for the portfolio endpoint.
Parameters:
GitHubOwner:
Type: String
GitHubOwnerId:
Type: String
GitHubRepository:
Type: String
GitHubRepositoryId:
Type: String
ExistingGitHubOidcProviderArn:
Type: String
Default: ""
Description: Leave empty to create the GitHub OIDC provider in this stack.
Conditions:
CreateGitHubOidcProvider: !Equals [!Ref ExistingGitHubOidcProviderArn, ""]
What does this template foundation do?
- The four GitHub parameters hold the owner and repository names with their numeric IDs.
- The optional provider ARN lets the same template work in accounts that already have the GitHub OIDC provider.
- The condition becomes true only when the existing-provider parameter is empty.
- Save infra/bootstrap.yaml by pressing Ctrl+S.
- Confirm the file now shows five parameters followed by CreateGitHubOidcProvider.
Condition missing from the file?
Check that Conditions starts at the far left. Its child line needs two spaces of indentation.
You can ask for help checking the parameter and condition indentation.
- Append the conditional OIDC provider resource by copying this code below the condition:
Resources:
GitHubOidcProvider:
Type: AWS::IAM::OIDCProvider
Condition: CreateGitHubOidcProvider
Properties:
Url: https://token.actions.githubusercontent.com
ClientIdList:
- sts.amazonaws.com
What does the provider resource do?
The resource registers GitHub as an OIDC identity provider only when the condition is true. The provider uses sts.amazonaws.com as the audience expected by the AWS credentials action.
- Save infra/bootstrap.yaml.
- Confirm GitHubOidcProvider appears under Resources with its condition attached.
Provider resource looks nested?
Make sure Resources starts at the far left. It must be a top-level section beside Parameters and Conditions.
You can ask for help fixing the provider resource indentation.
- Append the deployment role and immutable trust policy below the provider resource:
GitHubDeployRole:
Type: AWS::IAM::Role
Properties:
RoleName: serverless-cicd-github-deploy
MaxSessionDuration: 3600
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Federated: !If
- CreateGitHubOidcProvider
- !Ref GitHubOidcProvider
- !Ref ExistingGitHubOidcProviderArn
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
"token.actions.githubusercontent.com:aud": sts.amazonaws.com
"token.actions.githubusercontent.com:sub": !Sub "repo:${GitHubOwner}@${GitHubOwnerId}/${GitHubRepository}@${GitHubRepositoryId}:ref:refs/heads/main"
How does the trust policy stay scoped?
- The federated principal uses the newly created provider or the existing provider ARN supplied during deployment.
- The audience condition accepts tokens intended for AWS Security Token Service.
- The subject condition permits only the recorded owner ID and repository ID on the main branch.
- Save infra/bootstrap.yaml.
- Confirm the trust subject ends with ref:refs/heads/main.
Trust subject split incorrectly?
Keep the entire token.actions.githubusercontent.com:sub value on one line. Check that all four parameter references remain inside the same quoted subject.
You can ask for help checking the immutable OIDC subject.
- Add the change-set permissions inside GitHubDeployRole by copying this code below the trust policy:
Policies:
- PolicyName: DeployStatusStack
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: ManageApplicationChangeSets
Effect: Allow
Action:
- cloudformation:CreateChangeSet
- cloudformation:DescribeChangeSet
- cloudformation:ExecuteChangeSet
- cloudformation:DeleteChangeSet
- cloudformation:DescribeStacks
- cloudformation:DescribeStackEvents
- cloudformation:GetTemplate
- cloudformation:GetTemplateSummary
Resource: "*"
Why does the role need change-set permissions?
The deployment command creates and executes a CloudFormation change set when it updates the application stack. The read actions let the workflow inspect the stack and template during that lifecycle.
- Save infra/bootstrap.yaml.
- Confirm the policy lists eight CloudFormation actions under ManageApplicationChangeSets.
Permissions appear outside the role?
The Policies key needs six leading spaces so it remains inside the deployment role's Properties section.
You can ask for help nesting the change-set policy correctly.
- Append the Lambda update and execution-role permissions below the first policy statement:
- Sid: UpdatePortfolioFunction
Effect: Allow
Action:
- lambda:GetFunction
- lambda:GetFunctionConfiguration
- lambda:UpdateFunctionCode
- lambda:UpdateFunctionConfiguration
- lambda:ListTags
- lambda:TagResource
- lambda:UntagResource
Resource: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:serverless-cicd-status"
- Sid: PassPortfolioExecutionRole
Effect: Allow
Action:
- iam:GetRole
- iam:PassRole
Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/serverless-cicd-lambda-execution"
How are the deployment permissions limited?
The Lambda actions target only serverless-cicd-status. The IAM actions target only the serverless-cicd-lambda-execution role used by the function.
The deployment role can update the existing application resources without receiving broad access to every Lambda function or IAM role.
- Save infra/bootstrap.yaml.
- Confirm the final two statement IDs are UpdatePortfolioFunction and PassPortfolioExecutionRole.
Policy statements not aligned?
Both new Sid lines must align with ManageApplicationChangeSets. A deeper indent can turn them into properties of the first statement.
You can ask for help aligning the three deployment policy statements.
- Add the stack outputs by copying this code at the end of the file:
Outputs:
DeployRoleArn:
Description: Role assumed by the GitHub Actions workflow.
Value: !GetAtt GitHubDeployRole.Arn
OidcProviderArn:
Description: Created or reused GitHub OIDC provider.
Value: !If
- CreateGitHubOidcProvider
- !Ref GitHubOidcProvider
- !Ref ExistingGitHubOidcProviderArn
What do these outputs provide?
The first output exposes the role ARN that GitHub Actions assumes. The second reports the provider ARN created or reused by the stack.
- Save infra/bootstrap.yaml.
- Confirm Outputs starts at the far left beneath the completed Resources section.
Outputs nested under the policy?
Move Outputs to the far left if VS Code shows it inside the role or policy. It is a top-level template section.
You can ask for help placing the CloudFormation outputs correctly.
✔️ Awesome, I've got everything!
Great. Make sure infra/bootstrap.yaml is saved before deploying the trust stack.
ⓧ I'd like to double check the full code
Compare your complete infra/bootstrap.yaml file with this reference:
AWSTemplateFormatVersion: "2010-09-09"
Description: GitHub OIDC trust and deployment permissions for the portfolio endpoint.
Parameters:
GitHubOwner:
Type: String
GitHubOwnerId:
Type: String
GitHubRepository:
Type: String
GitHubRepositoryId:
Type: String
ExistingGitHubOidcProviderArn:
Type: String
Default: ""
Description: Leave empty to create the GitHub OIDC provider in this stack.
Conditions:
CreateGitHubOidcProvider: !Equals [!Ref ExistingGitHubOidcProviderArn, ""]
Resources:
GitHubOidcProvider:
Type: AWS::IAM::OIDCProvider
Condition: CreateGitHubOidcProvider
Properties:
Url: https://token.actions.githubusercontent.com
ClientIdList:
- sts.amazonaws.com
GitHubDeployRole:
Type: AWS::IAM::Role
Properties:
RoleName: serverless-cicd-github-deploy
MaxSessionDuration: 3600
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Federated: !If
- CreateGitHubOidcProvider
- !Ref GitHubOidcProvider
- !Ref ExistingGitHubOidcProviderArn
Action: sts:AssumeRoleWithWebIdentity
Condition:
StringEquals:
"token.actions.githubusercontent.com:aud": sts.amazonaws.com
"token.actions.githubusercontent.com:sub": !Sub "repo:${GitHubOwner}@${GitHubOwnerId}/${GitHubRepository}@${GitHubRepositoryId}:ref:refs/heads/main"
Policies:
- PolicyName: DeployStatusStack
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: ManageApplicationChangeSets
Effect: Allow
Action:
- cloudformation:CreateChangeSet
- cloudformation:DescribeChangeSet
- cloudformation:ExecuteChangeSet
- cloudformation:DeleteChangeSet
- cloudformation:DescribeStacks
- cloudformation:DescribeStackEvents
- cloudformation:GetTemplate
- cloudformation:GetTemplateSummary
Resource: "*"
- Sid: UpdatePortfolioFunction
Effect: Allow
Action:
- lambda:GetFunction
- lambda:GetFunctionConfiguration
- lambda:UpdateFunctionCode
- lambda:UpdateFunctionConfiguration
- lambda:ListTags
- lambda:TagResource
- lambda:UntagResource
Resource: !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:serverless-cicd-status"
- Sid: PassPortfolioExecutionRole
Effect: Allow
Action:
- iam:GetRole
- iam:PassRole
Resource: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/serverless-cicd-lambda-execution"
Outputs:
DeployRoleArn:
Description: Role assumed by the GitHub Actions workflow.
Value: !GetAtt GitHubDeployRole.Arn
OidcProviderArn:
Description: Created or reused GitHub OIDC provider.
Value: !If
- CreateGitHubOidcProvider
- !Ref GitHubOidcProvider
- !Ref ExistingGitHubOidcProviderArn
The deployment command depends on whether the provider check found an existing GitHub OIDC provider. Choose the matching path below.
No Existing Provider
CloudFormation may take a few minutes while it creates the provider and deployment role. PowerShell can remain quiet during parts of that process.
- Deploy serverless-cicd-bootstrap with the recorded repository values by running this command:
aws cloudformation deploy --template-file infra/bootstrap.yaml --stack-name serverless-cicd-bootstrap --parameter-overrides GitHubOwner=[[YOUR_OWNER="YOUR_OWNER"]] GitHubOwnerId=[[YOUR_OWNER_ID="YOUR_OWNER_ID"]] GitHubRepository=serverless-cicd-status GitHubRepositoryId=[[YOUR_REPOSITORY_ID="YOUR_REPOSITORY_ID"]] --capabilities CAPABILITY_NAMED_IAM --region us-east-1
What does this deployment do?
The parameter overrides bind the trust policy to your immutable repository identity. CAPABILITY_NAMED_IAM acknowledges that the template creates the named deployment role.
Because the existing-provider parameter stays empty, the stack also creates the account-level GitHub OIDC provider.
Reuse Existing Provider
CloudFormation may take a few minutes while it creates the deployment role. The existing provider stays outside this stack.
- Deploy serverless-cicd-bootstrap with the recorded provider ARN by running this command:
aws cloudformation deploy --template-file infra/bootstrap.yaml --stack-name serverless-cicd-bootstrap --parameter-overrides GitHubOwner=[[YOUR_OWNER="YOUR_OWNER"]] GitHubOwnerId=[[YOUR_OWNER_ID="YOUR_OWNER_ID"]] GitHubRepository=serverless-cicd-status GitHubRepositoryId=[[YOUR_REPOSITORY_ID="YOUR_REPOSITORY_ID"]] ExistingGitHubOidcProviderArn=[[EXISTING_GITHUB_OIDC_PROVIDER_ARN="your existing GitHub OIDC provider ARN"]] --capabilities CAPABILITY_NAMED_IAM --region us-east-1
What does this deployment do?
The repository values still bind the trust policy to your immutable GitHub identity. The supplied provider ARN makes the create-provider condition false.
The stack creates the deployment role and reports the reused provider through its outputs.
The deployment completes with a successful stack deployment message. Your application stack remains unchanged, so the live endpoint still serves v1.
Trust stack deployment failed?
Check that the owner and repository IDs contain only the numeric values from the GitHub metadata response. Confirm that the repository owner name matches the value in the repository URL.
If the account already had the GitHub provider, confirm that you used the reuse command with its complete ARN. You can ask for help diagnosing the bootstrap stack deployment.
Store the AWS account ID and verify trust
The workflow will construct the deployment-role ARN from the AWS account ID. A GitHub repository variable keeps that non-secret value available to the workflow without placing it directly in the workflow file.
- Return to the authenticated PowerShell window.
- Print only the current AWS account ID by running this command:
aws sts get-caller-identity --query Account --output text
What does this command return?
The query selects the Account field from the authenticated caller identity. The text output leaves you with the 12-digit account ID needed by the workflow.
- Record the 12-digit value here: your 12-digit AWS account ID.
- Return to the serverless-cicd-status repository page in your browser.
- Click Settings in the repository navigation.
- Select Secrets and variables in the left sidebar.
- Select Actions.
- Select the Variables tab.
- Click New repository variable.
- Enter AWS_ACCOUNT_ID in the Name field.
- Enter your 12-digit AWS account ID in the Value field.
- Click Add variable.
- Confirm AWS_ACCOUNT_ID appears in the repository variables list.
Repository variable missing?
Confirm you created a repository variable under the Variables tab. A repository secret uses a different storage area and will not be available through the workflow's variables context.
You can ask for help finding the GitHub Actions repository variables page.
Before you run the final check, do you expect the bootstrap stack to report an in-progress state or a completed state?
- Switch back to PowerShell.
- Verify the trust stack and its outputs by running this command:
aws cloudformation describe-stacks --stack-name serverless-cicd-bootstrap --region us-east-1
What does this check prove?
The stack description reports the deployment status and the template outputs. Together they confirm that AWS accepted the trust policy and created the deployment role.
You will see CREATE_COMPLETE for serverless-cicd-bootstrap. The DeployRoleArn output ends in role/serverless-cicd-github-deploy.
Stack is not complete?
If the stack still reports an in-progress status, wait for the deployment to finish before checking again. If it reports failure, inspect the stack events for the resource that failed.
You can ask for help interpreting the bootstrap stack status.
That is the security-heavy part complete. AWS now trusts short-lived credentials from this exact repository on main, so the next step can turn a Git push into a deployment.
Automate and Observe Every Deployment
Your trust stack now gives GitHub a repository-scoped path into AWS. The live endpoint still serves v1 because no workflow uses that path.
This step connects a push to AWS CloudFormation through GitHub Actions. The run uses short-lived OpenID Connect credentials to identify itself before it updates the existing stack.
In this step, get ready to:
- Create the deployment workflow inside .github/workflows/deploy.yml.
- Push the workflow to trigger a keyless CloudFormation update.
- Confirm the v2 release through its endpoint. Trace its invocation in Amazon CloudWatch Logs.
Create the deployment workflow
A workflow stores your delivery process beside the infrastructure it deploys. Its trigger defines when GitHub starts the deployment.
Why GitHub Actions here?
The public repository can use standard GitHub-hosted runners without usage charges. OpenID Connect gives each run temporary AWS credentials.
AWS CodePipeline could orchestrate this delivery process. GitHub Actions keeps the workflow beside the source while avoiding another delivery service.
- In the Explorer sidebar in VS Code, expand the .github folder.
- Expand the workflows folder.
- Hover over the workflows folder.
- Click the New File icon in the Explorer toolbar.
- Type deploy.yml.
- Press Enter.
- Define the workflow trigger and its top-level controls by pasting this configuration into deploy.yml:
name: Deploy status endpoint
on:
push:
branches:
- main
paths:
- "infra/app.yaml"
- ".github/workflows/deploy.yml"
workflow_dispatch:
permissions:
contents: read
id-token: write
env:
AWS_REGION: us-east-1
STACK_NAME: serverless-cicd-app
What does this configuration do?
- The push trigger starts the workflow when a matching change reaches main.
- The paths filter limits automatic runs to the application template or deployment workflow.
- The workflow_dispatch key also allows a manual run.
- The contents: read permission allows the runner to read the repository.
- The id-token: write permission allows the workflow to request an OIDC token.
- The environment values keep the AWS region and application stack name consistent throughout the job.
- Save deploy.yml by pressing Ctrl+S.
- Confirm deploy.yml appears inside .github/workflows in the Explorer sidebar.
Does the workflow structure look wrong?
YAML uses spaces to represent nesting. Make sure each nested line aligns with the matching lines in the code block.
If the file appears outside the workflow folder, drag deploy.yml into .github/workflows in the Explorer sidebar.
help me check my workflow trigger and permissions
The job now needs a runner plus the steps that authenticate and deploy. Each step leaves evidence in the workflow log.
- Add the deployment job directly below the STACK_NAME line by pasting this configuration:
jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
- name: Configure short-lived AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/serverless-cicd-github-deploy
aws-region: ${{ env.AWS_REGION }}
- name: Show assumed identity
run: aws sts get-caller-identity
- name: Deploy application stack
run: >
aws cloudformation deploy
--template-file infra/app.yaml
--stack-name "$STACK_NAME"
--capabilities CAPABILITY_NAMED_IAM
--region "$AWS_REGION"
How does the deployment job work?
- The job runs on the pinned ubuntu-24.04 runner image.
- The actions/checkout@v7.0.1 step retrieves the repository files.
- The aws-actions/configure-aws-credentials@v6.3.0 step exchanges the OIDC token for short-lived AWS credentials.
- The role ARN uses the existing AWS_ACCOUNT_ID repository variable.
- The identity step prints the AWS caller used by the workflow.
- The final step updates serverless-cicd-app from infra/app.yaml.
- Save deploy.yml by pressing Ctrl+S.
- Confirm the final line in the file reads --region "$AWS_REGION".
Does the job look misaligned?
Make sure jobs begins at the far left. Every line beneath it must follow the indentation shown above.
Check that the credential and deployment steps both remain inside the steps list.
help me correct my GitHub Actions indentation
✔️ Awesome, I've got everything!
Your saved workflow now contains the trigger, permissions, environment values, and deployment job.
ⓧ I'd like to double check the full code
The complete .github/workflows/deploy.yml file should match this configuration exactly:
name: Deploy status endpoint
on:
push:
branches:
- main
paths:
- "infra/app.yaml"
- ".github/workflows/deploy.yml"
workflow_dispatch:
permissions:
contents: read
id-token: write
env:
AWS_REGION: us-east-1
STACK_NAME: serverless-cicd-app
jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- name: Check out repository
uses: actions/checkout@v7.0.1
- name: Configure short-lived AWS credentials
uses: aws-actions/configure-aws-credentials@v6.3.0
with:
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/serverless-cicd-github-deploy
aws-region: ${{ env.AWS_REGION }}
- name: Show assumed identity
run: aws sts get-caller-identity
- name: Deploy application stack
run: >
aws cloudformation deploy
--template-file infra/app.yaml
--stack-name "$STACK_NAME"
--capabilities CAPABILITY_NAMED_IAM
--region "$AWS_REGION"
Push and inspect the deployment
Git records the workflow as part of the project history. Pushing that commit to main activates the workflow trigger.
- Return to the authenticated PowerShell terminal from earlier.
Before you push, predict whether the live endpoint can change once this workflow reaches main.
- Commit the workflow and push it to GitHub by running these commands:
git add .
git commit -m "Add keyless AWS deployment"
git push
What do these commands do?
- The first command stages the new workflow file.
- The second command records the workflow in the repository history.
- The final command sends the commit to GitHub.
The push reaches main. GitHub starts a Deploy status endpoint workflow run.
Did the push fail?
If GitHub requests authentication, complete the sign-in prompt for the account that owns serverless-cicd-status.
If Git reports no changes, confirm deploy.yml is saved inside .github/workflows.
help me push my deployment workflow
- Switch back to the serverless-cicd-status repository in your browser.
- Select the Actions tab.
- Select the workflow run named Deploy status endpoint.
The deployment can remain in progress while CloudFormation applies the change set. This pause means AWS is updating the existing stack.
- Wait for the workflow run to finish.
- Confirm the run finishes successfully.
- Expand the Show assumed identity step.
- Confirm the output includes Account, Arn, and UserId.
- Confirm the ARN names the serverless-cicd-github-deploy role.
That is the keyless connection working. The workflow can now enter AWS with a temporary identity scoped to this repository.
Did the workflow fail?
If the workflow never starts, confirm deploy.yml exists under .github/workflows on the main branch.
If credential configuration fails, confirm the AWS_ACCOUNT_ID repository variable contains the same 12-digit account ID used by the bootstrap stack.
help me diagnose my keyless deployment run
Verify the endpoint and logs
The endpoint proves that the workflow updated AWS Lambda. Amazon CloudWatch Logs proves that the deployed function handled a real request.
- Switch back to the browser tab containing the Function URL from earlier.
Before you refresh, picture which release you expect the endpoint to report.
- Refresh the Function URL.
You should see {"service":"serverless-cicd-status","status":"healthy","version":"v2"}. The v2 value proves the push updated the live function.
Does the endpoint still show v1?
Confirm the workflow run completed successfully before refreshing the Function URL again.
Check that the deployment step used infra/app.yaml and the serverless-cicd-app stack.
help me trace why my endpoint still shows v1
- Refresh the Function URL once more to create a fresh invocation.
- Return to the authenticated PowerShell terminal from earlier.
Before you query the logs, picture what one fresh endpoint request should leave behind.
- Read the function's recent log events by running this command:
aws logs tail /aws/lambda/serverless-cicd-status --since 5m --region us-east-1
What does this command check?
- The command reads events from /aws/lambda/serverless-cicd-status.
- The --since 5m option limits the output to the last five minutes.
- The --region us-east-1 option queries the region containing the function.
You should see a structured record containing "event": "status_check" and an aws_request_id. The request ID ties that log entry to one Lambda invocation.
Are the recent logs empty?
Refresh the Function URL again to create another invocation. Rerun the command above immediately afterward.
Confirm the command queries us-east-1 and the exact /aws/lambda/serverless-cicd-status log group.
help me find my Lambda status_check log
You have closed the source-to-cloud delivery gap. Every matching push can now deploy with short-lived credentials and leave evidence in the endpoint response plus its invocation logs.
Secret mission
Publish Deployment Evidence
Turn every successful workflow run into deployment evidence. The run will publish the live Function URL. It will also publish the deployed function name without widening the existing trust policy.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep everything running, pause the runtime for later, or delete the project resources completely. Enough traffic to the public AWS Lambda Function URL can create AWS charges despite the planned demo fitting within the cited free allowances.
Cost warning
Public requests use Lambda compute. Each invocation also writes logs to Amazon CloudWatch Logs.
Unexpected traffic can exceed free allowances. Existing usage in your AWS account also counts toward those allowances.
- Select the Pause or Delete tab below when you no longer need the public endpoint.
Resources you used:
- One AWS CloudFormation application stack named serverless-cicd-app. It manages the Lambda function. It also manages the public Function URL. Its IAM execution role is deleted with the stack. Its log group is deleted with the stack.
- One bootstrap CloudFormation stack named serverless-cicd-bootstrap. It manages the GitHub deployment role. It also manages the account-level OpenID Connect provider when the stack created that provider.
- One public GitHub repository named serverless-cicd-status. It contains the enhanced GitHub Actions workflow. It also contains the repository variable named AWS_ACCOUNT_ID.
Keep everything running
Leave every resource in place if you are still demonstrating the delivery workflow or testing the public endpoint.
- Keep serverless-cicd-app deployed. The healthy v2 endpoint remains available on the internet.
- Keep serverless-cicd-bootstrap deployed. The repository-scoped deployment role remains available to the workflow.
- Keep the public serverless-cicd-status repository. Its deployment history remains visible in GitHub Actions.
- Monitor AWS usage while the public Function URL remains available.
Pause - I'll come back to this later
Pausing removes the live endpoint while preserving the repository plus trust stack for a later manual deployment.
- Start deleting the application stack from PowerShell by running this command:
aws cloudformation delete-stack --stack-name serverless-cicd-app --region us-east-1
What gets paused?
AWS starts deleting the resources managed by serverless-cicd-app. The public endpoint becomes unavailable once deletion finishes.
The repository remains untouched. The bootstrap stack also remains deployed.
- Confirm the application stack has finished deleting by running this command periodically:
aws cloudformation describe-stacks --stack-name serverless-cicd-app --region us-east-1
How Do I Know It Is Gone?
During deletion, the command still returns the stack status. After deletion, it reports that the named stack is unavailable.
- Keep serverless-cicd-bootstrap deployed for the repository-scoped trust configuration.
- Keep the serverless-cicd-status repository for its templates plus workflow history.
Delete - I don't want to use this again
Deleting both stacks removes the endpoint plus deployment role from AWS. Your templates remain in GitHub until you complete the final repository action.
Delete the application stack
- Start deleting serverless-cicd-app from PowerShell by running this command:
aws cloudformation delete-stack --stack-name serverless-cicd-app --region us-east-1
What Does Application Deletion Remove?
This command starts deleting the Lambda function plus its public Function URL. It also removes the execution role plus one-day log group managed by the stack.
- Confirm the application stack has finished deleting by running this command periodically:
aws cloudformation describe-stacks --stack-name serverless-cicd-app --region us-east-1
What Does This Check Prove?
The stack status remains available while deletion is underway. The command reports the stack as unavailable once AWS has removed it.
Delete the bootstrap stack
- Start deleting serverless-cicd-bootstrap only after the application stack is gone by running this command:
aws cloudformation delete-stack --stack-name serverless-cicd-bootstrap --region us-east-1
What Does Bootstrap Deletion Remove?
This command removes the serverless-cicd-github-deploy role. It also removes the GitHub OIDC provider when this stack created that provider.
A provider supplied through ExistingGitHubOidcProviderArn remains untouched because it is shared outside this stack.
- Confirm the bootstrap stack has finished deleting by running this command periodically:
aws cloudformation describe-stacks --stack-name serverless-cicd-bootstrap --region us-east-1
What Does the Final Check Prove?
The command returns the bootstrap stack while deletion is underway. It reports the stack as unavailable once the deployment role has been removed.
Delete the public repository
- Return to the serverless-cicd-status repository on GitHub.
- Use the repository's Settings page to delete the public repository.
- Confirm that the repository URL no longer opens the project.
Nice Work!
Nice Work!
You did it! A push to main now updates your AWS Lambda health endpoint to a live v2 release through keyless CI/CD.
You've learned how to:
- Define a browser-accessible health endpoint with AWS CloudFormation. You managed its runtime resources through infrastructure as code.
- Expose deployment drift by committing v2 while the live endpoint remained on v1. You closed the gap with GitHub Actions. The workflow uses short-lived OIDC credentials scoped to the immutable repository subject on main.
- Verify each automated release through the assumed AWS identity. You confirmed the live endpoint returned v2. You traced structured status_check records in Amazon CloudWatch Logs.
- Secret Mission: Published FunctionUrl in every successful workflow run. Published FunctionName beside it. Each deployment now carries visible evidence of what went live.
Ready to quiz yourself?