Skip to content

Commit 3927738

Browse files
Alexander Melnykheitorlessa
Alexander Melnyk
andcommitted
chore(layers): add release pipeline in GitHub Actions (#1278)
* chore: add layer project * reduce to 1 region for dev * chore: shorter name for the workflow * fix ignore markdown lint for now * fix: more f strings * ignore mdlint * add reusable workflow for both beta and prod * Update layer/layer/canary/app.py Co-authored-by: Heitor Lessa <[email protected]> * Update layer/layer/canary/app.py Co-authored-by: Heitor Lessa <[email protected]> * readme review * rephrase canary stack ssm parameter usage * add default RELEASE_TAG_VERSION assignment based on the input (release or manual trigger) * add reference to layer docs * wording * move version trackign arn to canary stack * remove outdated npm caching, add release tag resolution for manual workflow trigger * review: fix layer name and remove dependencies from reusable workflow * remove debug statement, add default working dir * pin versions and hashes for requirements with pip-compile * rename reusable workflow * pass artefact name to the reusable workflow to prevent potential future conflicts Co-authored-by: Heitor Lessa <[email protected]>
1 parent bb8f38e commit 3927738

12 files changed

+532
-0
lines changed

.github/workflows/publish_layer.yml

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: Deploy layer to all regions
2+
3+
permissions:
4+
id-token: write
5+
contents: read
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
latest_published_version:
11+
description: "Latest PyPi published version to rebuild latest docs for, e.g. v1.22.0"
12+
default: "v1.22.0"
13+
required: true
14+
workflow_run:
15+
workflows: [ "Publish to PyPi" ]
16+
types:
17+
- completed
18+
19+
20+
jobs:
21+
build-layer:
22+
runs-on: ubuntu-latest
23+
defaults:
24+
run:
25+
working-directory: ./layer
26+
steps:
27+
- name: checkout
28+
uses: actions/checkout@v2
29+
- name: Setup Node.js
30+
uses: actions/setup-node@v2
31+
with:
32+
node-version: '16.12'
33+
- name: Setup python
34+
uses: actions/setup-python@v4
35+
with:
36+
python-version: '3.9'
37+
cache: 'pip'
38+
- name: Set release notes tag
39+
run: |
40+
RELEASE_INPUT=${{ inputs.latest_published_version }}
41+
GITHUB_EVENT_RELEASE_TAG=${{ github.event.release.tag_name }}
42+
RELEASE_TAG_VERSION=${GITHUB_EVENT_RELEASE_TAG:-$RELEASE_INPUT}
43+
echo "RELEASE_TAG_VERSION=${RELEASE_TAG_VERSION:1}" >> $GITHUB_ENV
44+
- name: install cdk and deps
45+
run: |
46+
npm install -g [email protected]
47+
cdk --version
48+
- name: install deps
49+
run: |
50+
pip install -r requirements.txt
51+
- name: CDK build
52+
run: cdk synth --context version=$RELEASE_TAG_VERSION -o cdk.out
53+
- name: zip output
54+
run: zip -r cdk.out.zip cdk.out
55+
- name: Archive CDK artifacts
56+
uses: actions/upload-artifact@v3
57+
with:
58+
name: cdk-layer-artefact
59+
path: layer/cdk.out.zip
60+
61+
deploy-beta:
62+
needs:
63+
- build-layer
64+
uses: ./.github/workflows/reusable_deploy_layer_stack.yml
65+
with:
66+
stage: "BETA"
67+
artefact-name: "cdk-layer-artefact"
68+
secrets:
69+
target-account-role: arn:aws:iam::${{ secrets.LAYERS_BETA_ACCOUNT }}:role/${{ secrets.AWS_GITHUB_OIDC_ROLE }}
70+
71+
deploy-prod:
72+
needs:
73+
- deploy-beta
74+
uses: ./.github/workflows/reusable_deploy_layer_stack.yml
75+
with:
76+
stage: "PROD"
77+
artefact-name: "cdk-layer-artefact"
78+
secrets:
79+
target-account-role: arn:aws:iam::${{ secrets.LAYERS_PROD_ACCOUNT }}:role/${{ secrets.AWS_GITHUB_OIDC_ROLE }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: Deploy cdk stack
2+
3+
permissions:
4+
id-token: write
5+
contents: read
6+
7+
on:
8+
workflow_call:
9+
inputs:
10+
stage:
11+
required: true
12+
type: string
13+
artefact-name:
14+
required: true
15+
type: string
16+
secrets:
17+
target-account-role:
18+
required: true
19+
20+
jobs:
21+
deploy-cdk-stack:
22+
runs-on: ubuntu-latest
23+
defaults:
24+
run:
25+
working-directory: ./layer
26+
strategy:
27+
fail-fast: false
28+
matrix:
29+
region: [
30+
"af-south-1",
31+
# "eu-central-1",
32+
# "us-east-1",
33+
# "us-east-2",
34+
# "us-west-1",
35+
# "us-west-2",
36+
# "ap-east-1",
37+
# "ap-south-1",
38+
# "ap-northeast-1",
39+
# "ap-northeast-2",
40+
# "ap-southeast-1",
41+
# "ap-southeast-2",
42+
# "ca-central-1",
43+
# "eu-west-1",
44+
# "eu-west-2",
45+
# "eu-west-3",
46+
# "eu-south-1",
47+
# "eu-north-1",
48+
# "sa-east-1",
49+
# "ap-southeast-3",
50+
# "ap-northeast-3",
51+
# "me-south-1"
52+
]
53+
steps:
54+
- name: checkout
55+
uses: actions/checkout@v2
56+
- name: aws credentials
57+
uses: aws-actions/configure-aws-credentials@v1
58+
with:
59+
aws-region: ${{ matrix.region }}
60+
role-to-assume: ${{ secrets.target-account-role }}
61+
- name: Setup Node.js
62+
uses: actions/setup-node@v2
63+
with:
64+
node-version: '16.12'
65+
- name: Setup python
66+
uses: actions/setup-python@v4
67+
with:
68+
python-version: '3.9'
69+
cache: 'pip'
70+
- name: install cdk and deps
71+
run: |
72+
npm install -g [email protected]
73+
cdk --version
74+
- name: install deps
75+
run: |
76+
pip install -r requirements.txt
77+
- name: Download artifact
78+
uses: actions/download-artifact@v3
79+
with:
80+
name: ${{ inputs.artefact-name }}
81+
path: layer
82+
- name: unzip artefact
83+
run: unzip ${{ inputs.artefact-name }}
84+
- name: CDK Deploy Layer
85+
run: cdk deploy --app cdk.out --context region=${{ matrix.region }} 'LayerStack ' --require-approval never --verbose
86+
- name: CDK Deploy Canary
87+
run: cdk deploy --app cdk.out --context region=${{ matrix.region}} --parameters DeployStage="${{ inputs.stage }}" 'CanaryStack' --require-approval never --verbose

layer/.gitignore

+10
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/README.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!-- markdownlint-disable MD041 MD043-->
2+
# CDK Powertools layer
3+
4+
This is a CDK project to build and deploy AWS Lambda Powertools [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 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 version a layer contains.
26+
For better tracking of the ARNs and the corresponding version we need to keep track which powertools 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 version, and the region to this EventBridge.

layer/app.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
SSM_PARAM_LAYER_ARN: str = "/layers/powertools-layer-arn"
12+
13+
if not POWERTOOLS_VERSION:
14+
raise ValueError(
15+
"Please set the version for Powertools by passing the '--context=version:<version>' parameter to the CDK "
16+
"synth step."
17+
)
18+
19+
LayerStack(app, "LayerStack", powertools_version=POWERTOOLS_VERSION, ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN)
20+
21+
CanaryStack(app, "CanaryStack", powertools_version=POWERTOOLS_VERSION, ssm_paramter_layer_arn=SSM_PARAM_LAYER_ARN)
22+
23+
app.synth()

layer/cdk.json

+35
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/layer/__init__.py

Whitespace-only changes.

layer/layer/canary/app.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import datetime
2+
import json
3+
import os
4+
from importlib.metadata import version
5+
6+
import boto3
7+
8+
from aws_lambda_powertools import Logger, Metrics, Tracer
9+
10+
logger = Logger(service="version-track")
11+
tracer = Tracer()
12+
metrics = Metrics(namespace="powertools-layer-canary", service="PowertoolsLayerCanary")
13+
14+
layer_arn = os.getenv("POWERTOOLS_LAYER_ARN")
15+
powertools_version = os.getenv("POWERTOOLS_VERSION")
16+
stage = os.getenv("LAYER_PIPELINE_STAGE")
17+
event_bus_arn = os.getenv("VERSION_TRACKING_EVENT_BUS_ARN")
18+
19+
20+
def handler(event):
21+
logger.info("Running checks")
22+
check_envs()
23+
verify_powertools_version()
24+
send_notification()
25+
return True
26+
27+
28+
@logger.inject_lambda_context(log_event=True)
29+
def on_event(event, context):
30+
request_type = event["RequestType"]
31+
# we handle only create events, because we recreate the canary on each run
32+
if request_type == "Create":
33+
return on_create(event)
34+
35+
return "Nothing to be processed"
36+
37+
38+
def on_create(event):
39+
props = event["ResourceProperties"]
40+
logger.info("create new resource with properties %s" % props)
41+
handler(event)
42+
43+
44+
def check_envs():
45+
logger.info('Checking required envs ["POWERTOOLS_LAYER_ARN", "AWS_REGION", "STAGE"]')
46+
if not layer_arn:
47+
raise ValueError("POWERTOOLS_LAYER_ARN is not set. Aborting...")
48+
if not powertools_version:
49+
raise ValueError("POWERTOOLS_VERSION is not set. Aborting...")
50+
if not stage:
51+
raise ValueError("LAYER_PIPELINE_STAGE is not set. Aborting...")
52+
if not event_bus_arn:
53+
raise ValueError("VERSION_TRACKING_EVENT_BUS_ARN is not set. Aborting...")
54+
logger.info("All envs configured, continue...")
55+
56+
57+
def verify_powertools_version() -> None:
58+
"""
59+
fetches the version that we import from the powertools layer and compares
60+
it with expected version set in environment variable, which we pass during deployment.
61+
:raise ValueError if the expected version is not the same as the version we get from the layer
62+
"""
63+
logger.info("Checking Powertools version in library...")
64+
current_version = version("aws_lambda_powertools")
65+
if powertools_version != current_version:
66+
raise ValueError(
67+
f'Expected powertoosl version is "{powertools_version}", but layer contains version "{current_version}"'
68+
)
69+
logger.info(f"Current Powertools version is: {current_version}")
70+
71+
72+
def send_notification():
73+
"""
74+
sends an event to version tracking event bridge
75+
"""
76+
event = {
77+
"Time": datetime.datetime.now(),
78+
"Source": "powertools.layer.canary",
79+
"EventBusName": event_bus_arn,
80+
"DetailType": "deployment",
81+
"Detail": json.dumps(
82+
{
83+
"id": "powertools-python",
84+
"stage": stage,
85+
"region": os.environ["AWS_REGION"],
86+
"version": powertools_version,
87+
"layerArn": layer_arn,
88+
}
89+
),
90+
}
91+
92+
logger.info(f"sending notification event: {event}")
93+
94+
client = boto3.client("events", region_name="eu-central-1")
95+
resp = client.put_events(Entries=[event])
96+
logger.info(resp)
97+
if resp["FailedEntryCount"] != 0:
98+
logger.error(resp)
99+
raise ValueError("Failed to send deployment notification to version tracking")

0 commit comments

Comments
 (0)