Deployment
Environments
samconfig.toml has one config environment per stack. global parameters (stack name, region,
and EnvName as the only parameter override) apply to build, deploy and validate.
Everything else that differs between environments is in the Env mapping of template.yaml.
stage= |
Stack | Account | Deployed by |
|---|---|---|---|
|
|
625663093496 |
GitHub Actions on every push to |
|
|
605668041275 |
GitHub Actions on every push to |
By hand
aws sso login --profile dev
AWS_PROFILE=dev make deploy stage=stage
make deploy runs, in order:
-
uv export --frozen --no-dev … -o requirements.txt— the locked runtime dependencies. -
uv run sam build --config-env <stage>—uv runputs.venv’s Python 3.12 (with setuptools) first on `PATH; SAM’s pip builder uses it to build the git-sourced SDKs. -
sam deploy --config-env <stage> --no-confirm-changeset --no-fail-on-empty-changeset— uploads to the SAM-managed bucket (resolve_s3) and applies a change set withCAPABILITY_IAM.
Preview a change without applying it:
make build stage=stage
AWS_PROFILE=dev sam deploy --config-env stage --confirm-changeset
CI/CD (GitHub Actions)
Both workflows run the same make targets as a developer, so there are no CI-only scripts. uv
and the SAM CLI are pinned by UV_VERSION / SAM_CLI_VERSION at the top of each workflow.
| Workflow | Runs on | Does |
|---|---|---|
|
every pull request; called by |
|
|
push to |
Runs |
A push to develop deploys stage and a push to release deploys prod; no other branch
deploys, and there is no manual trigger (use Re-run jobs on a run to deploy the same commit
again, or deploy by hand). Deploys to one environment are serialised (concurrency), and the
job runs in the GitHub environment of the same name: add required reviewers to the prod
environment to gate production deploys.
Secrets
All three are organisation secrets shared with this repository; the repository defines none of its own.
| Secret | Used for |
|---|---|
|
A token with read access to the private |
|
Deploy user in the dev account (605668041275), for |
|
Deploy user in the prod account (625663093496), for |
List what the repository can see with
gh api repos/Payability/ActionsToolsApp/actions/organization-secrets --jq '.secrets[].name'.
Secrets are not passed to workflows triggered by Dependabot or by pull requests from forks, so
those test runs fail at the SDK step.
Permissions of the deploy user
SAM deploys through CloudFormation, so the deploy identity needs CloudFormation, IAM (the stack
creates the functions' roles), Lambda, API Gateway, EventBridge and the SAM artifacts bucket. A
policy scoped to the prod stack (for stage, replace the account and prod with
605668041275 and stage):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudFormation",
"Effect": "Allow",
"Action": [
"cloudformation:CreateChangeSet",
"cloudformation:DescribeChangeSet",
"cloudformation:ExecuteChangeSet",
"cloudformation:DeleteChangeSet",
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackEvents",
"cloudformation:DescribeStackResources",
"cloudformation:GetTemplate",
"cloudformation:GetTemplateSummary",
"cloudformation:ListStackResources"
],
"Resource": [
"arn:aws:cloudformation:us-east-1:625663093496:stack/actionstools-prod/*",
"arn:aws:cloudformation:us-east-1:625663093496:stack/aws-sam-cli-managed-default/*",
"arn:aws:cloudformation:us-east-1:aws:transform/Serverless-2016-10-31"
]
},
{
"Sid": "SamArtifactsBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:GetBucketLocation"],
"Resource": [
"arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*",
"arn:aws:s3:::aws-sam-cli-managed-default-samclisourcebucket-*/*"
]
},
{
"Sid": "FunctionRoles",
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:DeleteRole",
"iam:GetRole",
"iam:TagRole",
"iam:UntagRole",
"iam:PutRolePolicy",
"iam:DeleteRolePolicy",
"iam:GetRolePolicy",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:PassRole"
],
"Resource": "arn:aws:iam::625663093496:role/actionstools-prod-*"
},
{
"Sid": "Functions",
"Effect": "Allow",
"Action": [
"lambda:CreateFunction",
"lambda:DeleteFunction",
"lambda:GetFunction",
"lambda:GetFunctionConfiguration",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:ListTags",
"lambda:TagResource",
"lambda:UntagResource",
"lambda:AddPermission",
"lambda:RemovePermission",
"lambda:PutFunctionEventInvokeConfig",
"lambda:UpdateFunctionEventInvokeConfig",
"lambda:DeleteFunctionEventInvokeConfig",
"lambda:GetFunctionEventInvokeConfig"
],
"Resource": "arn:aws:lambda:us-east-1:625663093496:function:actionstools-prod-*"
},
{
"Sid": "LogGroups",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:DeleteLogGroup",
"logs:PutRetentionPolicy",
"logs:DeleteRetentionPolicy",
"logs:TagResource",
"logs:UntagResource",
"logs:ListTagsForResource"
],
"Resource": "arn:aws:logs:us-east-1:625663093496:log-group:/aws/lambda/actionstools-prod-*"
},
{
"Sid": "DescribeLogGroups",
"Effect": "Allow",
"Action": ["logs:DescribeLogGroups"],
"Resource": "*"
},
{
"Sid": "EventSourceMappings",
"Effect": "Allow",
"Action": [
"lambda:CreateEventSourceMapping",
"lambda:UpdateEventSourceMapping",
"lambda:DeleteEventSourceMapping",
"lambda:GetEventSourceMapping"
],
"Resource": "*"
},
{
"Sid": "ApiGateway",
"Effect": "Allow",
"Action": ["apigateway:GET", "apigateway:POST", "apigateway:PUT", "apigateway:PATCH", "apigateway:DELETE"],
"Resource": ["arn:aws:apigateway:us-east-1::/restapis", "arn:aws:apigateway:us-east-1::/restapis/*", "arn:aws:apigateway:us-east-1::/tags/*"]
},
{
"Sid": "Schedule",
"Effect": "Allow",
"Action": [
"events:PutRule",
"events:DeleteRule",
"events:DescribeRule",
"events:EnableRule",
"events:DisableRule",
"events:PutTargets",
"events:RemoveTargets"
],
"Resource": "arn:aws:events:us-east-1:625663093496:rule/actionstools-prod-*"
},
{
"Sid": "VpcLookups",
"Effect": "Allow",
"Action": ["ec2:DescribeSecurityGroups", "ec2:DescribeSubnets", "ec2:DescribeVpcs"],
"Resource": "*"
}
]
}
The first sam deploy in an account creates the aws-sam-cli-managed-default stack and its
bucket; if the deploy user should not be allowed to do that, run the first deploy by hand.
Rollback
-
Bad code: revert the commit on
release; CI redeploys the previous version. To skip CI, deploy the previous commit by hand:git checkout <sha> && AWS_PROFILE=prod make deploy stage=prod. -
Stuck update:
aws cloudformation cancel-update-stack --stack-name actionstools-prodwhile it is in progress. -
Failed update: CloudFormation rolls back automatically; the stack events name the failing resource.
Data is never touched by a rollback: the tables are outside the stack.
Smoke test after a deploy
URL=$(aws cloudformation describe-stacks --stack-name actionstools-stage \
--query "Stacks[0].Outputs[?OutputKey=='ApiUrl'].OutputValue" --output text --profile dev)
curl -s -o /dev/null -w '%{http_code}\n' "$URL" # 200, Swagger UI
curl -s "$URL/notes/" -H "X-Session-Token: $TOKEN" \
-H "X-Actions-TargetUUID: <supplier key>" | jq .status_code # 200
sam logs --stack-name actionstools-stage --profile dev --tail # API and Worker logs