Skip to content

Commit 6dc11ec

Browse files
Adding v3 stack
1 parent 975ffaf commit 6dc11ec

15 files changed

+1442
-7
lines changed

.github/workflows/release-v3.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ on:
3737
inputs:
3838
version_to_publish:
3939
description: "Version to be released in PyPi, Docs, and Lambda Layer, e.g. v3.0.0, v3.0.0a0 (pre-release)"
40-
default: v2.0.0
40+
default: v3.0.0
4141
required: true
4242
skip_pypi:
4343
description: "Skip publishing to PyPi as it can't publish more than once. Useful for semi-failed releases"
@@ -246,7 +246,7 @@ jobs:
246246
# with:
247247
# repository-url: https://test.pypi.org/legacy/
248248

249-
# We create a Git Tag using our release version (e.g., v2.16.0)
249+
# We create a Git Tag using our release version (e.g., v3.16.0)
250250
# using our sealed source code we created earlier.
251251
# Because we bumped version of our project as part of CI
252252
# we need to add this into git before pushing the tag
@@ -330,7 +330,7 @@ jobs:
330330
# NOTE
331331
#
332332
# Watch out for the depth limit of 4 nested workflow_calls.
333-
# publish_layer -> publish_v2_layer -> reusable_deploy_v2_layer_stack
333+
# publish_layer -> publish_3_layer -> reusable_deploy_v3_layer_stack
334334
publish_layer:
335335
needs: [seal, release, create_tag]
336336
secrets: inherit

.github/workflows/reusable_deploy_v3_layer_stack.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,13 +193,13 @@ jobs:
193193
LAYER_VERSION=${{ matrix.region }}-$PYTHON_VERSION-layer-version.txt
194194
echo "LAYER_VERSION=${LAYER_VERSION}" >> "$GITHUB_OUTPUT"
195195
- name: CDK Deploy Layer
196-
run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters HasARM64Support=${{ matrix.has_arm64_support }} "LayerV2Stack-${{steps.constants.outputs.PYTHON_VERSION}}" --require-approval never --verbose --outputs-file cdk-outputs.json
196+
run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters HasARM64Support=${{ matrix.has_arm64_support }} "LayerV3Stack-${{steps.constants.outputs.PYTHON_VERSION}}" --require-approval never --verbose --outputs-file cdk-outputs.json
197197
- name: Store latest Layer ARN
198198
if: ${{ inputs.stage == 'PROD' }}
199199
run: |
200200
mkdir cdk-layer-stack
201-
jq -r -c ".[\"LayerV2Stack-${{steps.constants.outputs.PYTHON_VERSION}}\"].LatestLayerArn" cdk-outputs.json > cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}}
202-
jq -r -c ".[\"LayerV2Stack-${{steps.constants.outputs.PYTHON_VERSION}}\"].LatestLayerArm64Arn" cdk-outputs.json >> cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}}
201+
jq -r -c ".[\"LayerV3Stack-${{steps.constants.outputs.PYTHON_VERSION}}\"].LatestLayerArn" cdk-outputs.json > cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}}
202+
jq -r -c ".[\"LayerV3Stack-${{steps.constants.outputs.PYTHON_VERSION}}\"].LatestLayerArm64Arn" cdk-outputs.json >> cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}}
203203
cat cdk-layer-stack/${{steps.constants.outputs.LAYER_VERSION}}
204204
- name: Save Layer ARN artifact
205205
if: ${{ inputs.stage == 'PROD' }}
@@ -210,4 +210,4 @@ jobs:
210210
if-no-files-found: error
211211
retention-days: 1
212212
- name: CDK Deploy Canary
213-
run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters DeployStage="${{ inputs.stage }}" --parameters HasARM64Support=${{ matrix.has_arm64_support }} "CanaryV2Stack-${{steps.constants.outputs.PYTHON_VERSION}}" --require-approval never --verbose
213+
run: npx cdk deploy --app cdk.out --context region=${{ matrix.region }} --parameters DeployStage="${{ inputs.stage }}" --parameters HasARM64Support=${{ matrix.has_arm64_support }} "CanaryV3Stack-${{steps.constants.outputs.PYTHON_VERSION}}" --require-approval never --verbose
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
name: Deploy V3 SAR
2+
3+
# PROCESS
4+
#
5+
# 1. This workflow starts after the layer artifact is produced on `publish_v3_layer`
6+
# 2. We use the same layer artifact to ensure the SAR app is consistent with the published Lambda Layer
7+
# 3. We publish the SAR for both x86_64 and arm64 (see `matrix` section)
8+
# 4. We use `sam package` and `sam publish` to publish the SAR app
9+
# 5. We remove the previous Canary stack (if present) and deploy a new one to test the SAR App. We retain the Canary in the account for debugging purposes
10+
# 6. Finally the published SAR app is made public on the PROD environment
11+
12+
# USAGE
13+
#
14+
# NOTE: meant to be used with ./.github/workflows/publish_v2_layer.yml
15+
#
16+
# sar-beta:
17+
# needs: build-layer
18+
# permissions:
19+
# # lower privilege propagated from parent workflow (release.yml)
20+
# id-token: write
21+
# contents: read
22+
# pull-requests: none
23+
# pages: none
24+
# uses: ./.github/workflows/reusable_deploy_v3_sar.yml
25+
# secrets: inherit
26+
# with:
27+
# stage: "BETA"
28+
# artefact-name: "cdk-layer-artefact"
29+
# environment: "layer-beta"
30+
# package-version: ${{ inputs.latest_published_version }}
31+
# source_code_artifact_name: ${{ inputs.source_code_artifact_name }}
32+
# source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }}
33+
34+
permissions:
35+
id-token: write
36+
contents: read
37+
38+
env:
39+
NODE_VERSION: 16.12
40+
AWS_REGION: eu-west-1
41+
SAR_NAME: aws-lambda-powertools-python-layer
42+
TEST_STACK_NAME: serverlessrepo-v2-powertools-layer-test-stack
43+
RELEASE_COMMIT: ${{ github.sha }} # it gets propagated from the caller for security reasons
44+
45+
on:
46+
workflow_call:
47+
inputs:
48+
stage:
49+
description: "Deployment stage (BETA, PROD)"
50+
required: true
51+
type: string
52+
artefact-name:
53+
description: "CDK Layer Artefact name to download"
54+
required: true
55+
type: string
56+
package-version:
57+
description: "The version of the package to deploy"
58+
required: true
59+
type: string
60+
environment:
61+
description: "GitHub Environment to use for encrypted secrets"
62+
required: true
63+
type: string
64+
source_code_artifact_name:
65+
description: "Artifact name to restore sealed source code"
66+
type: string
67+
required: true
68+
source_code_integrity_hash:
69+
description: "Sealed source code integrity hash"
70+
type: string
71+
required: true
72+
73+
jobs:
74+
deploy-sar-app:
75+
runs-on: ubuntu-latest
76+
environment: ${{ inputs.environment }}
77+
strategy:
78+
matrix:
79+
architecture: ["x86_64", "arm64"]
80+
steps:
81+
- name: checkout
82+
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
83+
with:
84+
ref: ${{ env.RELEASE_COMMIT }}
85+
86+
- name: Restore sealed source code
87+
uses: ./.github/actions/seal-restore
88+
with:
89+
integrity_hash: ${{ inputs.source_code_integrity_hash }}
90+
artifact_name: ${{ inputs.source_code_artifact_name }}
91+
92+
93+
- name: AWS credentials
94+
uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0
95+
with:
96+
aws-region: ${{ env.AWS_REGION }}
97+
role-to-assume: ${{ secrets.AWS_LAYERS_ROLE_ARN }}
98+
99+
# NOTE
100+
# We connect to Layers account to log our intent to publish a SAR Layer
101+
# we then jump to our specific SAR Account with the correctly scoped IAM Role
102+
# this allows us to have a single trail when a release occurs for a given layer (beta+prod+SAR beta+SAR prod)
103+
- name: AWS credentials SAR role
104+
uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0
105+
id: aws-credentials-sar-role
106+
with:
107+
aws-access-key-id: ${{ env.AWS_ACCESS_KEY_ID }}
108+
aws-secret-access-key: ${{ env.AWS_SECRET_ACCESS_KEY }}
109+
aws-session-token: ${{ env.AWS_SESSION_TOKEN }}
110+
role-duration-seconds: 1200
111+
aws-region: ${{ env.AWS_REGION }}
112+
role-to-assume: ${{ secrets.AWS_SAR_V2_ROLE_ARN }}
113+
- name: Setup Node.js
114+
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
115+
with:
116+
node-version: ${{ env.NODE_VERSION }}
117+
- name: Download artifact
118+
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
119+
with:
120+
name: ${{ inputs.artefact-name }}
121+
- name: Unzip artefact
122+
run: unzip cdk.out.zip
123+
- name: Configure SAR name
124+
run: |
125+
if [[ "${{ inputs.stage }}" == "BETA" ]]; then
126+
SAR_NAME="test-${SAR_NAME}"
127+
fi
128+
echo SAR_NAME="${SAR_NAME}" >> "$GITHUB_ENV"
129+
- name: Adds arm64 suffix to SAR name
130+
if: ${{ matrix.architecture == 'arm64' }}
131+
run: echo SAR_NAME="${SAR_NAME}-arm64" >> "$GITHUB_ENV"
132+
- name: Normalize semantic version
133+
id: semantic-version # v2.0.0a0 -> v2.0.0-a0
134+
env:
135+
VERSION: ${{ inputs.package-version }}
136+
run: |
137+
VERSION="${VERSION/a/-a}"
138+
echo "VERSION=${VERSION}" >> "$GITHUB_OUTPUT"
139+
- name: Prepare SAR App
140+
env:
141+
VERSION: ${{ steps.semantic-version.outputs.VERSION }}
142+
run: |
143+
# From the generated LayerStack cdk.out artifact, find the layer asset path for the correct architecture.
144+
# We'll use this as the source directory of our SAR. This way we are re-using the same layer asset for our SAR.
145+
asset=$(jq -jc '.Resources[] | select(.Properties.CompatibleArchitectures == ["${{ matrix.architecture }}"]) | .Metadata."aws:asset:path"' cdk.out/LayerV2Stack.template.json)
146+
147+
# fill in the SAR SAM template
148+
sed \
149+
-e "s|<VERSION>|${VERSION}|g" \
150+
-e "s/<SAR_APP_NAME>/${{ env.SAR_NAME }}/g" \
151+
-e "s|<LAYER_CONTENT_PATH>|./cdk.out/$asset|g" \
152+
layer/sar/template.txt > template.yml
153+
154+
# SAR needs a README and a LICENSE, so just copy the ones from the repo
155+
cp README.md LICENSE "./cdk.out/$asset/"
156+
157+
# Debug purposes
158+
cat template.yml
159+
- name: Deploy SAR
160+
run: |
161+
# Package the SAR to our SAR S3 bucket, and publish it
162+
sam package --template-file template.yml --output-template-file packaged.yml --s3-bucket ${{ secrets.AWS_SAR_S3_BUCKET }}
163+
sam publish --template packaged.yml --region "$AWS_REGION"
164+
- name: Deploy BETA canary
165+
if: ${{ inputs.stage == 'BETA' }}
166+
run: |
167+
if [[ "${{ matrix.architecture }}" == "arm64" ]]; then
168+
TEST_STACK_NAME="${TEST_STACK_NAME}-arm64"
169+
fi
170+
171+
echo "Check if stack does not exist"
172+
stack_exists=$(aws cloudformation list-stacks --query "StackSummaries[?(StackName == '$TEST_STACK_NAME' && StackStatus == 'CREATE_COMPLETE')].{StackId:StackId, StackName:StackName, CreationTime:CreationTime, StackStatus:StackStatus}" --output text)
173+
174+
if [[ -n "$stack_exists" ]] ; then
175+
echo "Found test deployment stack, removing..."
176+
aws cloudformation delete-stack --stack-name "$TEST_STACK_NAME"
177+
aws cloudformation wait stack-delete-complete --stack-name "$TEST_STACK_NAME"
178+
fi
179+
180+
echo "Creating canary stack"
181+
echo "Stack name: $TEST_STACK_NAME"
182+
aws serverlessrepo create-cloud-formation-change-set \
183+
--application-id arn:aws:serverlessrepo:${{ env.AWS_REGION }}:${{ steps.aws-credentials-sar-role.outputs.aws-account-id }}:applications/${{ env.SAR_NAME }} \
184+
--stack-name "${TEST_STACK_NAME/serverlessrepo-/}" \
185+
--capabilities CAPABILITY_NAMED_IAM
186+
187+
CHANGE_SET_ID=$(aws cloudformation list-change-sets --stack-name "$TEST_STACK_NAME" --query 'Summaries[*].ChangeSetId' --output text)
188+
aws cloudformation wait change-set-create-complete --change-set-name "$CHANGE_SET_ID"
189+
aws cloudformation execute-change-set --change-set-name "$CHANGE_SET_ID"
190+
aws cloudformation wait stack-create-complete --stack-name "$TEST_STACK_NAME"
191+
192+
echo "Waiting until stack deployment completes..."
193+
194+
echo "Exit with error if stack is not in CREATE_COMPLETE"
195+
stack_exists=$(aws cloudformation list-stacks --query "StackSummaries[?(StackName == '$TEST_STACK_NAME' && StackStatus == 'CREATE_COMPLETE')].{StackId:StackId, StackName:StackName, CreationTime:CreationTime, StackStatus:StackStatus}")
196+
if [[ -z "$stack_exists" ]] ; then
197+
echo "Could find successful deployment, exit error..."
198+
exit 1
199+
fi
200+
echo "Deployment successful"
201+
- name: Publish SAR
202+
if: ${{ inputs.stage == 'PROD' }}
203+
run: |
204+
# wait until SAR registers the app, otherwise it fails to make it public
205+
sleep 15
206+
echo "Make SAR app public"
207+
aws serverlessrepo put-application-policy \
208+
--application-id arn:aws:serverlessrepo:${{ env.AWS_REGION }}:${{ steps.aws-credentials-sar-role.outputs.aws-account-id }}:applications/${{ env.SAR_NAME }} \
209+
--statements Principals='*',Actions=Deploy

layer_v3/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*.swp
2+
package-lock.json
3+
__pycache__
4+
.pytest_cache
5+
.venv
6+
*.egg-info
7+
8+
# CDK asset staging directory
9+
.cdk.staging
10+
cdk.out

layer_v3/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!-- markdownlint-disable MD041 MD043-->
2+
# CDK Powertools for AWS Lambda (Python) layer
3+
4+
This is a CDK project to build and deploy Powertools for AWS Lambda (Python) [Lambda layer](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-concepts.html#gettingstarted-concepts-layer) to multiple commercial regions.
5+
6+
## Build the layer
7+
8+
To build the layer construct you need to provide the Powertools for AWS Lambda (Python) version that is [available in PyPi](https://pypi.org/project/aws-lambda-powertools/).
9+
You can pass it as a context variable when running `synth` or `deploy`,
10+
11+
```shell
12+
cdk synth --context version=1.25.1
13+
```
14+
15+
## Canary stack
16+
17+
We use a canary stack to verify that the deployment is successful and we can use the layer by adding it to a newly created Lambda function.
18+
The canary is deployed after the layer construct. Because the layer ARN is created during the deploy we need to pass this information async via SSM parameter.
19+
To achieve that we use SSM parameter store to pass the layer ARN to the canary.
20+
The layer stack writes the layer ARN after the deployment as SSM parameter and the canary stacks reads this information and adds the layer to the function.
21+
22+
## Version tracking
23+
24+
AWS Lambda versions Lambda layers by incrementing a number at the end of the ARN.
25+
This makes it challenging to know which Powertools for AWS Lambda (Python) version a layer contains.
26+
For better tracking of the ARNs and the corresponding version we need to keep track which Powertools for AWS Lambda (Python) version was deployed to which layer.
27+
To achieve that we created two components. First, we created a version tracking app which receives events via EventBridge. Second, after a successful canary deployment we send the layer ARN, Powertools for AWS Lambda (Python) version, and the region to this EventBridge.

layer_v3/app.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env python3
2+
3+
import aws_cdk as cdk
4+
5+
from layer.canary_stack import CanaryStack
6+
from layer.layer_stack import LayerStack
7+
8+
app = cdk.App()
9+
10+
POWERTOOLS_VERSION: str = app.node.try_get_context("version")
11+
PYTHON_VERSION: str = app.node.try_get_context("pythonVersion")
12+
PYTHON_VERSION_NORMALIZED = PYTHON_VERSION.replace(".", "")
13+
SSM_PARAM_LAYER_ARN: str = "/layers/powertools-layer-v3-arn-{PYTHON_VERSION_NORMALIZED}"
14+
SSM_PARAM_LAYER_ARM64_ARN: str = "/layers/powertools-layer-v3-arm64-arn-{PYTHON_VERSION_NORMALIZED}"
15+
16+
# Validate context variables
17+
if not PYTHON_VERSION:
18+
raise ValueError(
19+
"Please set the version for Python by passing the '--context pythonVersion=<version>' parameter to the CDK "
20+
"synth step.",
21+
)
22+
23+
if not POWERTOOLS_VERSION:
24+
raise ValueError(
25+
"Please set the version for Powertools by passing the '--context version=<version>' parameter to the CDK "
26+
"synth step.",
27+
)
28+
29+
LayerStack(
30+
app,
31+
f"LayerV3Stack-{PYTHON_VERSION_NORMALIZED}",
32+
powertools_version=POWERTOOLS_VERSION,
33+
python_version=PYTHON_VERSION,
34+
ssm_parameter_layer_arn=SSM_PARAM_LAYER_ARN,
35+
ssm_parameter_layer_arm64_arn=SSM_PARAM_LAYER_ARM64_ARN,
36+
)
37+
38+
CanaryStack(
39+
app,
40+
f"CanaryV3Stack-{PYTHON_VERSION_NORMALIZED}",
41+
powertools_version=POWERTOOLS_VERSION,
42+
python_version=PYTHON_VERSION,
43+
ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN,
44+
ssm_parameter_layer_arm64_arn=SSM_PARAM_LAYER_ARM64_ARN,
45+
)
46+
47+
app.synth()

layer_v3/cdk.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"app": "python3 app.py",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"requirements*.txt",
11+
"source.bat",
12+
"**/__init__.py",
13+
"python/__pycache__",
14+
"tests"
15+
]
16+
},
17+
"context": {
18+
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
19+
"@aws-cdk/core:stackRelativeExports": true,
20+
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
21+
"@aws-cdk/aws-lambda:recognizeVersionProps": true,
22+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
23+
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
24+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
25+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
26+
"@aws-cdk/core:checkSecretUsage": true,
27+
"@aws-cdk/aws-iam:minimizePolicies": true,
28+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
29+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
30+
"@aws-cdk/core:target-partitions": [
31+
"aws",
32+
"aws-cn"
33+
]
34+
}
35+
}

layer_v3/layer/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)