Reputation: 287540
I have an application called Example4Be, I have a stack that creates the AWS non-Lambda resources (just DynamoDB in this case, but could be SQS, SNS, etc):
import * as cdk from "aws-cdk-lib"
import {Construct} from "constructs"
import * as dynamodb from "aws-cdk-lib/aws-dynamodb"
export class Example4BeResourcesStack extends cdk.Stack {
public readonly table: dynamodb.Table
constructor(scope: Construct, id: string, props?: cdk.StageProps) {
super(scope, id, props)
this.table = new dynamodb.Table(this, "Hits", {
partitionKey: {
name: "path",
type: dynamodb.AttributeType.STRING
}
})
}
}
And then there's a stack that uses it:
import * as cdk from "aws-cdk-lib"
import * as lambda from "aws-cdk-lib/aws-lambda"
import * as apigw from "aws-cdk-lib/aws-apigateway"
import {Construct} from "constructs"
import {Example4BeResourcesStack} from "./example4-be-resources-stack";
export class Example4BeStack extends cdk.Stack {
public readonly hcEndpoint: cdk.CfnOutput
constructor(scope: Construct, id: string, props?: cdk.StageProps) {
super(scope, id, props)
const resources = new Example4BeResourcesStack(scope, `${id}Resources`)
const hello = new lambda.Function(this, "HelloHandler", {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset("lambda"),
handler: "hello.handler",
environment: {
HITS_TABLE_NAME: resources.table.tableName
}
})
resources.table.grantReadWriteData(hello)
// defines an API Gateway REST API resource backed by our "hello" function.
const gateway = new apigw.LambdaRestApi(this, "Endpoint", {
handler: hello
})
this.hcEndpoint = new cdk.CfnOutput(this, "GatewayUrl", {
value: gateway.url
})
}
}
I have a file, app.ts
that instantiates the stack:
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib"
import {Example4BeStack} from "../lib/example4-be-stack";
const app = new cdk.App()
new Example4BeStack(app, "Example4BePipeline")
and the way I run it is by running:
cdk synth --no-staging > template.yaml
sam local start-api
but this doesn't seem to create the DynamoDB table. I guess there's a cdk deploy
that I'm missing? How should this be done?
I'm happy to add more of the application if you want, there's CodePipeline involved in the actual deployment.
Upvotes: 6
Views: 3003
Reputation: 25679
SAM-CDK integration runs emulated lambdas locally, calling cloud-deployed resources as required.
cdk deploy
Our local-lambdas will interact with deployed-resource1 dependencies (tables, queues, etc). It's an AWS best practice to use a separate account for testing.2
// locals.json is used by SAM to resolve lambda env vars
{
// the lambda resource id from sam-template.yaml
"HandlerFunctionEDBA20C2": {
// our lambda has an environment variable named TABLE_NAME
// this is deployed-resource in cloud - tip: define a CfnOutput in your stack. cdk deploy will emit the values to the terminal
"TABLE_NAME": "CdkTsPlayLocalTestingStack-SuperTable2CB94566-1VPR5PAXO0GM5"
},
"AnotherLambdaCD21C99": {
"QUEUE_ARN": "<queue-arn>"
}
}
or you can have a single set of variables apply to all functions:
{
"Parameters": {
"TABLE_NAME": "CdkTsPlayLocalTestingStack-SuperTable2CB94566-1VPR5PAXO0GM5"
}
}
Make a change to your lambdas locally, then let the SAM cli do its thing3:
# synth the app template, output a sam format template
npx cdk synth '*' -a 'ts-node ./bin/app.ts' --profile <testing-profile> --no-staging > cdk.out/sam-template.yaml
# call a local function
sam local invoke HandlerFunctionEDBA20C2 \
--template cdk.out/sam-template.yaml \
--profile <testing-profile> \
--event lib/stacks/testing/LocalTestingStack/test-events/create.json \
--env-vars lib/stacks/testing/LocalTestingStack/locals.json
start-lambda
starts emulator, automated tests run against the local endpoint. Docs here.
SAM currently has 3 local test commands:
SAM command | lambda | apigw | other (dynamo, etc) | what for |
---|---|---|---|---|
sam local invoke | local | cloud | test a local lambda | |
sam local start-lambda | local | cloud | watch mode - call a lambda with SDK or CLI | |
sam local start-api | local | local | cloud | watch mode - test local lambda via a local endpoint |
Note that lambdas are the center of all this goodness. We can mock event inputs into local-lambdas. We can emulate an api/sdk endpoint. We can interact with deployed-resources referenced by lambda SDK calls. But other setups are not currently covered. For instance, we can't test a lambda DynamoDB streams handler. Let's hope the local functionality will grow over time.
(1) I use the term "local-lambda" to mean a local version of the lambda, which sam local
will run in a container. "deployed-resources" refers to our lambda's cloud-deployed integrations (e.g. DynamoDB tables, queues, etc.).
(2) At this point, testing does not touch your pipeline stack. Once you are done with local testing, push your changes to github, which will kick off the pipeline deploy to (staging
and) prod
accounts. You can have additional testing steps run as part of the pipeline stages.
(3) The preview sam-beta-cdk
CLI makes it somewhat easier to integrate CDK with SAM than the vanilla CLI sam
. For instance, it seems to create the sam .yaml
for us. But the two tools appear to have same underlying functionality.
Upvotes: 5
Reputation: 2903
Since @fedonev has already answered the question, I will focus on a potential implementation using CodePipeline (specifically CDK Pipelines in Python), since I had been already working on it. I restricted myself to not using anything which is experimental or preview.
Code for the example: https://github.com/KMK-Git/aws-cdk-sam-testing-demo
source = pipelines.CodePipelineSource.connection(
"KMK-Git/aws-cdk-sam-testing-demo",
"main",
connection_arn=ssm.StringParameter.value_for_string_parameter(
self,
"codestar_connection_arn",
),
)
The source code repository is configured using CodeStar connections. The connection was done manually beforehand, and I am getting the ARN from SSM parameter store.
cdk_codepipeline = pipelines.CodePipeline(
self,
"Pipeline",
synth=pipelines.ShellStep(
"Synth",
input=source,
install_commands=[
"pip install -r requirements.txt",
"npm install -g aws-cdk",
],
commands=[
"cdk synth",
],
),
)
In case you are not using CDK Pipelines, here is the build spec for all stages:
For Synth:
{
"version": "0.2",
"phases": {
"install": {
"commands": [
"pip install -r requirements.txt",
"npm install -g aws-cdk"
]
},
"build": {
"commands": [
"cdk synth"
]
}
},
"artifacts": {
"base-directory": "cdk.out",
"files": "**/*"
}
}
For SelfMutate:
{
"version": "0.2",
"phases": {
"install": {
"commands": [
"npm install -g aws-cdk"
]
},
"build": {
"commands": [
"cdk -a . deploy PipelineStack --require-approval=never --verbose"
]
}
}
}
For Assets:
{
"version": "0.2",
"phases": {
"install": {
"commands": [
"npm install -g cdk-assets"
]
},
"build": {
"commands": [
"cdk-assets --path \"assembly-LambdaStage/LambdaStageLambdasStackABCD123.assets.json\" --verbose publish \"longrandomstring:current_account-current_region\""
]
}
}
}
testing = pipelines.CodeBuildStep(
"UnitTesting",
input=source,
install_commands=[
"pip install -r requirements.txt -r requirements-dev.txt",
],
commands=[
"pytest --cov",
],
env={
"QUEUE_URL": "SampleQueue",
"TABLE_NAME": "SampleTest",
},
build_environment=codebuild.BuildEnvironment(
build_image=codebuild.LinuxBuildImage.STANDARD_5_0,
privileged=True,
compute_type=codebuild.ComputeType.SMALL,
),
)
This is a simple step, which runs any unit tests in your code.
In case you are not using CDK Pipelines, here is the build spec:
{
"version": "0.2",
"phases": {
"install": {
"commands": [
"pip install -r requirements.txt -r requirements-dev.txt"
]
},
"build": {
"commands": [
"pytest --cov"
]
}
}
}
cdk_codepipeline.add_stage(
supporting_resources_stage,
pre=[
testing,
pipelines.ConfirmPermissionsBroadening(
"CheckSupporting", stage=supporting_resources_stage
),
],
)
This would deploy the supporting resources, which are required for your sam local
tests to run properly. The permissions broadening step stops the pipeline and forces manual approval in case of any broadening of IAM permissions. You can add unit testing in its own separate stage as well.
In case you are not using CDK Pipelines, here is the build spec:
{
"version": 0.2,
"phases": {
"build": {
"commands": [
"npm install -g aws-cdk",
"export PIPELINE_NAME=\"$(node -pe '`${process.env.CODEBUILD_INITIATOR}`.split(\"/\")[1]')\"",
"payload=\"$(node -pe 'JSON.stringify({ \"PipelineName\": process.env.PIPELINE_NAME, \"StageName\": process.env.STAGE_NAME, \"ActionName\": process.env.ACTION_NAME })' )\"",
"ARN=$CODEBUILD_BUILD_ARN",
"REGION=\"$(node -pe '`${process.env.ARN}`.split(\":\")[3]')\"",
"ACCOUNT_ID=\"$(node -pe '`${process.env.ARN}`.split(\":\")[4]')\"",
"PROJECT_NAME=\"$(node -pe '`${process.env.ARN}`.split(\":\")[5].split(\"/\")[1]')\"",
"PROJECT_ID=\"$(node -pe '`${process.env.ARN}`.split(\":\")[6]')\"",
"export LINK=\"https://$REGION.console.aws.amazon.com/codesuite/codebuild/$ACCOUNT_ID/projects/$PROJECT_NAME/build/$PROJECT_NAME:$PROJECT_ID/?region=$REGION\"",
"export PIPELINE_LINK=\"https://$REGION.console.aws.amazon.com/codesuite/codepipeline/pipelines/$PIPELINE_NAME/view?region=$REGION\"",
"if cdk diff -a . --security-only --fail $STAGE_PATH/\\*; then aws lambda invoke --function-name PipelineStack-PipelinePipelinesSecurityCheckCDKalpha-numeric --invocation-type Event --payload \"$payload\" lambda.out; export MESSAGE=\"No security-impacting changes detected.\"; else [ -z \"${NOTIFICATION_ARN}\" ] || aws sns publish --topic-arn $NOTIFICATION_ARN --subject \"$NOTIFICATION_SUBJECT\" --message \"An upcoming change would broaden security changes in $PIPELINE_NAME.\nReview and approve the changes in CodePipeline to proceed with the deployment.\n\nReview the changes in CodeBuild:\n\n$LINK\n\nApprove the changes in CodePipeline (stage $STAGE_NAME, action $ACTION_NAME):\n\n$PIPELINE_LINK\"; export MESSAGE=\"Deployment would make security-impacting changes. Click the link below to inspect them, then click Approve if all changes are expected.\"; fi"
]
}
},
"env": {
"exported-variables": [
"LINK",
"MESSAGE"
]
}
}
sam_cli_test_step = pipelines.CodeBuildStep(
"SAMTesting",
input=source,
env_from_cfn_outputs={
"QUEUE_URL": supporting_resources_stage.stack.queue_url,
"TABLE_NAME": supporting_resources_stage.stack.table_name,
},
install_commands=[
"pip install -r requirements.txt",
"npm install -g aws-cdk",
"mkdir testoutput",
],
commands=[
'cdk synth -a "python synth_lambdas_stack.py" -o sam.out',
'echo "{\\""SqsLambdaFunction\\"": {\\""QUEUE_URL\\"": \\""$QUEUE_URL\\""},'
+ '\\""DynamodbLambdaFunction\\"": {\\""TABLE_NAME\\"": \\""$TABLE_NAME\\"" }}"'
+ " > locals.json",
'sam local invoke -t "sam.out/LambdasStack.template.json" --env-vars locals.json'
+ ' --no-event "DynamodbLambdaFunction"',
'sam local invoke -t "sam.out/LambdasStack.template.json" --env-vars locals.json'
+ ' --no-event "SqsLambdaFunction"',
"nohup sam local start-api -t sam.out/LambdasStack.template.json"
+ " --env-vars locals.json > testoutput/testing.log & ",
"",
"sleep 30",
"curl --fail http://127.0.0.1:3000/sqs",
"curl --fail http://127.0.0.1:3000/dynamodb",
],
build_environment=codebuild.BuildEnvironment(
build_image=codebuild.LinuxBuildImage.STANDARD_5_0,
privileged=True,
compute_type=codebuild.ComputeType.SMALL,
),
primary_output_directory="testoutput/",
role_policy_statements=[
iam.PolicyStatement(
actions=[
"sqs:SendMessage",
"sqs:GetQueueAttributes",
"sqs:GetQueueUrl",
],
resources=["*"],
),
iam.PolicyStatement(
actions=[
"dynamodb:BatchWriteItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
],
resources=["*"],
),
],
)
sqs_lambda_base: _lambda.CfnFunction = sqs_lambda.node.default_child
sqs_lambda_base.override_logical_id("SqsLambdaFunction")
sam-beta-cdk
can be used to simplify some of this workflow, but I did not use it here since it is still in preview.In case you are not using CDK Pipelines, here is the build spec:
{
"version": "0.2",
"phases": {
"install": {
"commands": [
"pip install -r requirements.txt",
"npm install -g aws-cdk",
"curl --version",
"mkdir testoutput"
]
},
"build": {
"commands": [
"cdk synth -a \"python synth_lambdas_stack.py\" -o sam.out",
"echo \"{\\\"\"SqsLambdaFunction\\\"\": {\\\"\"QUEUE_URL\\\"\": \\\"\"$QUEUE_URL\\\"\"},\\\"\"DynamodbLambdaFunction\\\"\": {\\\"\"TABLE_NAME\\\"\": \\\"\"$TABLE_NAME\\\"\" }}\" > locals.json",
"sam local invoke -t \"sam.out/LambdasStack.template.json\" --env-vars locals.json --no-event \"DynamodbLambdaFunction\"",
"sam local invoke -t \"sam.out/LambdasStack.template.json\" --env-vars locals.json --no-event \"SqsLambdaFunction\"",
"nohup sam local start-api -t sam.out/LambdasStack.template.json --env-vars locals.json > testoutput/testing.log & ",
"",
"sleep 30",
"curl --fail http://127.0.0.1:3000/sqs",
"curl --fail http://127.0.0.1:3000/dynamodb"
]
}
},
"artifacts": {
"base-directory": "testoutput/",
"files": "**/*"
}
}
cdk_codepipeline.add_stage(
lambdas_stage,
pre=[
sam_cli_test_step,
pipelines.ConfirmPermissionsBroadening(
"CheckLambda", stage=lambdas_stage
),
],
)
This is similar to the deploy supporting resources stage. You can add sam local testing in its own separate stage as well.
sam local
testing, so you won't be able to test Cognito authorizers if you are planning to use them.Upvotes: 2