PR #1 bakery-v6

Add visitor counter with DynamoDB

m
matthew.dempsky@tonal.com · 3 days ago

Changes

168
added
13
removed
5
files

Description

Each request atomically increments a counter in DynamoDB and returns 'hello visitor #N\!'. Adds a PAY_PER_REQUEST table and minimal IAM permissions.

Details

Branches
visitor-counter main
Commits
base 3b66d3a head 2191ef6
Last activity
3 days ago
Merge
SQUASH_MERGE by matthew.dempsky@tonal.com (4b8aa2c)
Approval
Overridden

Timeline

Opened by matthew.dempsky@tonal.com 38efd3c
3 days ago
Pushed by matthew.dempsky@tonal.com 38efd3c → 523a496
3 days ago
Pushed by matthew.dempsky@tonal.com 523a496 → 2191ef6
3 days ago
Approval overridden by matthew.dempsky@tonal.com
3 days ago
Squash merged by matthew.dempsky@tonal.com
3 days ago

Files Changed

M buildspec.yml
@@ -3,6 +3,7 @@
3 3 env:
4 4 variables:
5 5 ENV: "main"
6 ACTION: "apply"
6 7
7 8 phases:
8 9 install:
@@ -16,21 +17,30 @@
16 17 build:
17 18 commands:
18 19 # Compile a static Linux binary named "bootstrap" — the name the
19 # provided.al2023 Lambda runtime expects.
20 - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go
21 - zip function.zip bootstrap
20 # provided.al2023 Lambda runtime expects. Skipped for destroy builds.
21 - |
22 if [ "$ACTION" = "apply" ]; then
23 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go
24 zip function.zip bootstrap
25 fi
22 26
23 # Initialize Terraform with the S3 backend. ACCOUNT_ID is set by the
24 # CodeBuild environment; ENV comes from the env block above (overridden
25 # per-build for PR environments).
27 # Initialize Terraform with the S3 backend. ACCOUNT_ID comes from
28 # STS; ENV and ACTION are set by EventBridge overrides for PR builds.
26 29 - ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
27 30 - >-
28 31 terraform init
29 32 -backend-config="bucket=bakery-v6-artifacts-${ACCOUNT_ID}"
30 33 -backend-config="key=state/${ENV}/terraform.tfstate"
31 34
32 - terraform apply -auto-approve -var="env=${ENV}"
35 # For destroy builds (PR closed/merged), tear down the environment.
36 # For apply builds, create or update it.
37 - |
38 if [ "$ACTION" = "destroy" ]; then
39 terraform destroy -auto-approve -var="env=${ENV}"
40 else
41 terraform apply -auto-approve -var="env=${ENV}"
42 fi
33 43
34 44 post_build:
35 45 commands:
36 - terraform output -json
46 - terraform output -json || true
@@ -3,6 +3,7 @@
3 env: 3 env:
4 variables: 4 variables:
5 ENV: "main" 5 ENV: "main"
6 ACTION: "apply"
6 7
7 phases: 8 phases:
8 install: 9 install:
@@ -16,21 +17,30 @@
16 build: 17 build:
17 commands: 18 commands:
18 # Compile a static Linux binary named "bootstrap" — the name the 19 # Compile a static Linux binary named "bootstrap" — the name the
19 # provided.al2023 Lambda runtime expects. 20 # provided.al2023 Lambda runtime expects. Skipped for destroy builds.
20 - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go 21 - |
21 - zip function.zip bootstrap 22 if [ "$ACTION" = "apply" ]; then
23 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags lambda.norpc -o bootstrap main.go
24 zip function.zip bootstrap
25 fi
22 26
23 # Initialize Terraform with the S3 backend. ACCOUNT_ID is set by the 27 # Initialize Terraform with the S3 backend. ACCOUNT_ID comes from
24 # CodeBuild environment; ENV comes from the env block above (overridden 28 # STS; ENV and ACTION are set by EventBridge overrides for PR builds.
25 # per-build for PR environments).
26 - ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) 29 - ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
27 - >- 30 - >-
28 terraform init 31 terraform init
29 -backend-config="bucket=bakery-v6-artifacts-${ACCOUNT_ID}" 32 -backend-config="bucket=bakery-v6-artifacts-${ACCOUNT_ID}"
30 -backend-config="key=state/${ENV}/terraform.tfstate" 33 -backend-config="key=state/${ENV}/terraform.tfstate"
31 34
32 - terraform apply -auto-approve -var="env=${ENV}" 35 # For destroy builds (PR closed/merged), tear down the environment.
36 # For apply builds, create or update it.
37 - |
38 if [ "$ACTION" = "destroy" ]; then
39 terraform destroy -auto-approve -var="env=${ENV}"
40 else
41 terraform apply -auto-approve -var="env=${ENV}"
42 fi
33 43
34 post_build: 44 post_build:
35 commands: 45 commands:
36 - terraform output -json 46 - terraform output -json || true
M go.mod
@@ -2,4 +2,25 @@
2 2
3 3 go 1.25.7
4 4
5 require github.com/aws/aws-lambda-go v1.52.0
5 require (
6 github.com/aws/aws-lambda-go v1.52.0
7 github.com/aws/aws-sdk-go-v2 v1.41.1
8 github.com/aws/aws-sdk-go-v2/config v1.32.7
9 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0
10 )
11
12 require (
13 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
14 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
15 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
16 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
17 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
18 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
19 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17 // indirect
20 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
21 github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
22 github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
23 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
24 github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
25 github.com/aws/smithy-go v1.24.0 // indirect
26 )
@@ -2,4 +2,25 @@
2 2
3 go 1.25.7 3 go 1.25.7
4 4
5 require github.com/aws/aws-lambda-go v1.52.0 5 require (
6 github.com/aws/aws-lambda-go v1.52.0
7 github.com/aws/aws-sdk-go-v2 v1.41.1
8 github.com/aws/aws-sdk-go-v2/config v1.32.7
9 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0
10 )
11
12 require (
13 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 // indirect
14 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect
15 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect
16 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect
17 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
18 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
19 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17 // indirect
20 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect
21 github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect
22 github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect
23 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect
24 github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect
25 github.com/aws/smithy-go v1.24.0 // indirect
26 )
M go.sum
@@ -1,5 +1,37 @@
1 1 github.com/aws/aws-lambda-go v1.52.0 h1:5NfiRaVl9FafUIt2Ld/Bv22kT371mfAI+l1Hd+tV7ZE=
2 2 github.com/aws/aws-lambda-go v1.52.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
3 github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
4 github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
5 github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
6 github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
8 github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
9 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
10 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
11 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
12 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
13 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
14 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
15 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
16 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
17 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0 h1:CyYoeHWjVSGimzMhlL0Z4l5gLCa++ccnRJKrsaNssxE=
18 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0/go.mod h1:ctEsEHY2vFQc6i4KU07q4n68v7BAmTbujv2Y+z8+hQY=
19 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
20 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
21 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17 h1:Nhx/OYX+ukejm9t/MkWI8sucnsiroNYNGb5ddI9ungQ=
22 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17/go.mod h1:AjmK8JWnlAevq1b1NBtv5oQVG4iqnYXUufdgol+q9wg=
23 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
24 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
25 github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
26 github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
27 github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
28 github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
29 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
30 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
31 github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
32 github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
33 github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
34 github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
3 35 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 36 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 37 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -1,5 +1,37 @@
1 github.com/aws/aws-lambda-go v1.52.0 h1:5NfiRaVl9FafUIt2Ld/Bv22kT371mfAI+l1Hd+tV7ZE= 1 github.com/aws/aws-lambda-go v1.52.0 h1:5NfiRaVl9FafUIt2Ld/Bv22kT371mfAI+l1Hd+tV7ZE=
2 github.com/aws/aws-lambda-go v1.52.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= 2 github.com/aws/aws-lambda-go v1.52.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
3 github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
4 github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
5 github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY=
6 github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY=
7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8=
8 github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw=
9 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY=
10 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA=
11 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U=
12 github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ=
13 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik=
14 github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM=
15 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
16 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
17 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0 h1:CyYoeHWjVSGimzMhlL0Z4l5gLCa++ccnRJKrsaNssxE=
18 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.55.0/go.mod h1:ctEsEHY2vFQc6i4KU07q4n68v7BAmTbujv2Y+z8+hQY=
19 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
20 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
21 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17 h1:Nhx/OYX+ukejm9t/MkWI8sucnsiroNYNGb5ddI9ungQ=
22 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17/go.mod h1:AjmK8JWnlAevq1b1NBtv5oQVG4iqnYXUufdgol+q9wg=
23 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY=
24 github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU=
25 github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y=
26 github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M=
27 github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk=
28 github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw=
29 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds=
30 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo=
31 github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ=
32 github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ=
33 github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
34 github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
3 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 35 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
M infra.tf
@@ -72,8 +72,17 @@
72 72 default = "main"
73 73 }
74 74
75 data "aws_caller_identity" "current" {}
76
75 77 locals {
76 78 prefix = "bakery-v6-${var.env}"
79
80 # Derive the permissions boundary ARN from convention. The ops account
81 # creates one boundary per deployer type (main vs prs). Any roles we
82 # create must have this boundary attached — the deployer's IAM policy
83 # enforces it, and the boundary caps effective permissions to our prefix.
84 deployer_type = startswith(var.env, "pr-") ? "prs" : "main"
85 boundary_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/bakery-v6-deployer-${local.deployer_type}-boundary"
77 86 }
78 87
79 88
@@ -87,11 +96,11 @@
87 96 # there are no shared library dependencies to worry about.
88 97
89 98 # The Lambda execution role is the identity the function assumes at runtime.
90 # It needs basic permissions to write CloudWatch logs. Add more policies
91 # here as the function grows (DynamoDB access, S3, etc.).
99 # It needs CloudWatch logs and DynamoDB access for the visitor counter.
92 100
93 101 resource "aws_iam_role" "lambda_exec" {
94 name = "${local.prefix}-hello-exec"
102 name = "${local.prefix}-hello-exec"
103 permissions_boundary = local.boundary_arn
95 104
96 105 assume_role_policy = jsonencode({
97 106 Version = "2012-10-17"
@@ -108,6 +117,20 @@
108 117 policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
109 118 }
110 119
120 resource "aws_iam_role_policy" "lambda_dynamodb" {
121 name = "DynamoDBAccess"
122 role = aws_iam_role.lambda_exec.id
123
124 policy = jsonencode({
125 Version = "2012-10-17"
126 Statement = [{
127 Effect = "Allow"
128 Action = ["dynamodb:UpdateItem"]
129 Resource = aws_dynamodb_table.visitors.arn
130 }]
131 })
132 }
133
111 134 resource "aws_lambda_function" "hello" {
112 135 function_name = "${local.prefix}-hello"
113 136 role = aws_iam_role.lambda_exec.arn
@@ -118,6 +141,32 @@
118 141 # Built by `go build` + `zip` before terraform apply (see buildspec.yml).
119 142 filename = "function.zip"
120 143 source_code_hash = filebase64sha256("function.zip")
144
145 environment {
146 variables = {
147 TABLE_NAME = aws_dynamodb_table.visitors.name
148 }
149 }
150 }
151
152
153 # ============================================================================
154 # DynamoDB table
155 # ============================================================================
156 #
157 # A simple table for tracking visitor count. Uses a single item with an
158 # atomic counter — the Lambda does UpdateItem with ADD on each request.
159 # PAY_PER_REQUEST so it costs nothing when idle.
160
161 resource "aws_dynamodb_table" "visitors" {
162 name = "${local.prefix}-visitors"
163 billing_mode = "PAY_PER_REQUEST"
164 hash_key = "pk"
165
166 attribute {
167 name = "pk"
168 type = "S"
169 }
121 170 }
122 171
123 172
@@ -72,8 +72,17 @@
72 default = "main" 72 default = "main"
73 } 73 }
74 74
75 data "aws_caller_identity" "current" {}
76
75 locals { 77 locals {
76 prefix = "bakery-v6-${var.env}" 78 prefix = "bakery-v6-${var.env}"
79
80 # Derive the permissions boundary ARN from convention. The ops account
81 # creates one boundary per deployer type (main vs prs). Any roles we
82 # create must have this boundary attached — the deployer's IAM policy
83 # enforces it, and the boundary caps effective permissions to our prefix.
84 deployer_type = startswith(var.env, "pr-") ? "prs" : "main"
85 boundary_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/bakery-v6-deployer-${local.deployer_type}-boundary"
77 } 86 }
78 87
79 88
@@ -87,11 +96,11 @@
87 # there are no shared library dependencies to worry about. 96 # there are no shared library dependencies to worry about.
88 97
89 # The Lambda execution role is the identity the function assumes at runtime. 98 # The Lambda execution role is the identity the function assumes at runtime.
90 # It needs basic permissions to write CloudWatch logs. Add more policies 99 # It needs CloudWatch logs and DynamoDB access for the visitor counter.
91 # here as the function grows (DynamoDB access, S3, etc.).
92 100
93 resource "aws_iam_role" "lambda_exec" { 101 resource "aws_iam_role" "lambda_exec" {
94 name = "${local.prefix}-hello-exec" 102 name = "${local.prefix}-hello-exec"
103 permissions_boundary = local.boundary_arn
95 104
96 assume_role_policy = jsonencode({ 105 assume_role_policy = jsonencode({
97 Version = "2012-10-17" 106 Version = "2012-10-17"
@@ -108,6 +117,20 @@
108 policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 117 policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
109 } 118 }
110 119
120 resource "aws_iam_role_policy" "lambda_dynamodb" {
121 name = "DynamoDBAccess"
122 role = aws_iam_role.lambda_exec.id
123
124 policy = jsonencode({
125 Version = "2012-10-17"
126 Statement = [{
127 Effect = "Allow"
128 Action = ["dynamodb:UpdateItem"]
129 Resource = aws_dynamodb_table.visitors.arn
130 }]
131 })
132 }
133
111 resource "aws_lambda_function" "hello" { 134 resource "aws_lambda_function" "hello" {
112 function_name = "${local.prefix}-hello" 135 function_name = "${local.prefix}-hello"
113 role = aws_iam_role.lambda_exec.arn 136 role = aws_iam_role.lambda_exec.arn
@@ -118,6 +141,32 @@
118 # Built by `go build` + `zip` before terraform apply (see buildspec.yml). 141 # Built by `go build` + `zip` before terraform apply (see buildspec.yml).
119 filename = "function.zip" 142 filename = "function.zip"
120 source_code_hash = filebase64sha256("function.zip") 143 source_code_hash = filebase64sha256("function.zip")
144
145 environment {
146 variables = {
147 TABLE_NAME = aws_dynamodb_table.visitors.name
148 }
149 }
150 }
151
152
153 # ============================================================================
154 # DynamoDB table
155 # ============================================================================
156 #
157 # A simple table for tracking visitor count. Uses a single item with an
158 # atomic counter — the Lambda does UpdateItem with ADD on each request.
159 # PAY_PER_REQUEST so it costs nothing when idle.
160
161 resource "aws_dynamodb_table" "visitors" {
162 name = "${local.prefix}-visitors"
163 billing_mode = "PAY_PER_REQUEST"
164 hash_key = "pk"
165
166 attribute {
167 name = "pk"
168 type = "S"
169 }
121 } 170 }
122 171
123 172
M main.go
@@ -2,16 +2,59 @@
2 2
3 3 import (
4 4 "context"
5 "fmt"
6 "os"
7 "strconv"
5 8
6 9 "github.com/aws/aws-lambda-go/events"
7 10 "github.com/aws/aws-lambda-go/lambda"
11 "github.com/aws/aws-sdk-go-v2/aws"
12 "github.com/aws/aws-sdk-go-v2/config"
13 "github.com/aws/aws-sdk-go-v2/service/dynamodb"
14 "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
8 15 )
9 16
17 var ddb *dynamodb.Client
18 var tableName string
19
20 func init() {
21 tableName = os.Getenv("TABLE_NAME")
22 cfg, err := config.LoadDefaultConfig(context.Background())
23 if err != nil {
24 panic(err)
25 }
26 ddb = dynamodb.NewFromConfig(cfg)
27 }
28
10 29 func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
30 // Atomic increment: adds 1 to the counter and returns the new value.
31 out, err := ddb.UpdateItem(ctx, &dynamodb.UpdateItemInput{
32 TableName: &tableName,
33 Key: map[string]types.AttributeValue{
34 "pk": &types.AttributeValueMemberS{Value: "visitors"},
35 },
36 UpdateExpression: aws.String("ADD #n :inc"),
37 ExpressionAttributeNames: map[string]string{
38 "#n": "count",
39 },
40 ExpressionAttributeValues: map[string]types.AttributeValue{
41 ":inc": &types.AttributeValueMemberN{Value: "1"},
42 },
43 ReturnValues: types.ReturnValueUpdatedNew,
44 })
45 if err != nil {
46 return events.APIGatewayV2HTTPResponse{
47 StatusCode: 500,
48 Body: fmt.Sprintf(`{"error":"%s"}`, err.Error()),
49 }, nil
50 }
51
52 count, _ := strconv.Atoi(out.Attributes["count"].(*types.AttributeValueMemberN).Value)
53
11 54 return events.APIGatewayV2HTTPResponse{
12 55 StatusCode: 200,
13 56 Headers: map[string]string{"Content-Type": "application/json"},
14 Body: `{"message":"hello world"}`,
57 Body: fmt.Sprintf(`{"message":"hello visitor #%d!"}`, count),
15 58 }, nil
16 59 }
17 60
@@ -2,16 +2,59 @@
2 2
3 import ( 3 import (
4 "context" 4 "context"
5 "fmt"
6 "os"
7 "strconv"
5 8
6 "github.com/aws/aws-lambda-go/events" 9 "github.com/aws/aws-lambda-go/events"
7 "github.com/aws/aws-lambda-go/lambda" 10 "github.com/aws/aws-lambda-go/lambda"
11 "github.com/aws/aws-sdk-go-v2/aws"
12 "github.com/aws/aws-sdk-go-v2/config"
13 "github.com/aws/aws-sdk-go-v2/service/dynamodb"
14 "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
8 ) 15 )
9 16
17 var ddb *dynamodb.Client
18 var tableName string
19
20 func init() {
21 tableName = os.Getenv("TABLE_NAME")
22 cfg, err := config.LoadDefaultConfig(context.Background())
23 if err != nil {
24 panic(err)
25 }
26 ddb = dynamodb.NewFromConfig(cfg)
27 }
28
10 func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 29 func handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
30 // Atomic increment: adds 1 to the counter and returns the new value.
31 out, err := ddb.UpdateItem(ctx, &dynamodb.UpdateItemInput{
32 TableName: &tableName,
33 Key: map[string]types.AttributeValue{
34 "pk": &types.AttributeValueMemberS{Value: "visitors"},
35 },
36 UpdateExpression: aws.String("ADD #n :inc"),
37 ExpressionAttributeNames: map[string]string{
38 "#n": "count",
39 },
40 ExpressionAttributeValues: map[string]types.AttributeValue{
41 ":inc": &types.AttributeValueMemberN{Value: "1"},
42 },
43 ReturnValues: types.ReturnValueUpdatedNew,
44 })
45 if err != nil {
46 return events.APIGatewayV2HTTPResponse{
47 StatusCode: 500,
48 Body: fmt.Sprintf(`{"error":"%s"}`, err.Error()),
49 }, nil
50 }
51
52 count, _ := strconv.Atoi(out.Attributes["count"].(*types.AttributeValueMemberN).Value)
53
11 return events.APIGatewayV2HTTPResponse{ 54 return events.APIGatewayV2HTTPResponse{
12 StatusCode: 200, 55 StatusCode: 200,
13 Headers: map[string]string{"Content-Type": "application/json"}, 56 Headers: map[string]string{"Content-Type": "application/json"},
14 Body: `{"message":"hello world"}`, 57 Body: fmt.Sprintf(`{"message":"hello visitor #%d!"}`, count),
15 }, nil 58 }, nil
16 } 59 }
17 60