diff --git a/README.md b/README.md index 4983eee..e70b72f 100644 --- a/README.md +++ b/README.md @@ -151,28 +151,17 @@ The request body has one property **guess**, which only allows either "down" or The input validation is done through the API Gateway itself. -### EventBridge -As there is no way to integrate Step Functions in an async workflow with -API Gateway and the CDK, I use Event Bridge as a layer between API Gateway and Step Functions. There is only one rule, which triggers the execution of the Step Function workflow -and returns the event ID, which is used as the game ID. -```json -{ - "detail-type": ["putEvent"] -} -``` - ### Lambda There are 3 lambda functions: -1. new-guess: stores the initial game data to DynamoDB +1. new-guess: stores the initial game data to DynamoDB and starts the state machine with a 60 seconds wait state. 2. handle-result: checks if the initial price differs from the current price and updates the DynamoDB entry when the game is finished 3. check-result: allows the user to get status updates about the given game ID ### Step Functions -There are 3 tasks: -1. newGuessTasks: is triggered through EventBridge and starts the above mentioned lambda function, it returns the game data and a property "waitSeconds" with 60 seconds as its value. -2. waitTask: gets a variable "waitSeconds" and waits for this amount of time -3. handleResultTask triggers the above mentioned handle-result lambda and returns whether the price did change or not with a property "didPriceChange" and "waitSeconds" with 20 seconds. That means that as long as the price did not change it will wait 20 seconds again and again until it changed and is then resolved by updating the DynamoDB items gameStatus to "finished". +There are 2 tasks: +1. waitTask: gets a variable "waitSeconds" and waits for this amount of time +2. handleResultTask triggers the above mentioned handle-result lambda and returns whether the price did change or not with a property "didPriceChange" and "waitSeconds" with 20 seconds. That means that as long as the price did not change it will wait 20 seconds again and again until it changed and is then resolved by updating the DynamoDB items gameStatus to "finished".  ### DynamoDB diff --git a/diagrams/architecture.png b/diagrams/architecture.png index 9c56698..0bd1751 100644 Binary files a/diagrams/architecture.png and b/diagrams/architecture.png differ diff --git a/diagrams/stepfunctions.png b/diagrams/stepfunctions.png index 70faacf..b1079e3 100644 Binary files a/diagrams/stepfunctions.png and b/diagrams/stepfunctions.png differ diff --git a/packages/backend/infrastructure/constructs/eb-integration.ts b/packages/backend/infrastructure/constructs/eb-integration.ts deleted file mode 100644 index ffd4dd3..0000000 --- a/packages/backend/infrastructure/constructs/eb-integration.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Construct } from "constructs"; -import { AwsIntegration } from "aws-cdk-lib/aws-apigateway"; -import { IEventBus } from "aws-cdk-lib/aws-events"; -import { IStateMachine } from "aws-cdk-lib/aws-stepfunctions"; - -import { Role, ServicePrincipal } from "aws-cdk-lib/aws-iam"; -import { aws_events, aws_events_targets } from "aws-cdk-lib"; - -interface APIEventBridgeIntegrationProps { - eventBus: IEventBus; - stateMachine: IStateMachine; -} - -export class APIEventBridgeIntegration extends Construct { - eventBridgeIntegration: AwsIntegration; - - constructor( - scope: Construct, - id: string, - props: APIEventBridgeIntegrationProps - ) { - super(scope, id); - - const { eventBus, stateMachine } = props; - - const role = new Role(this, "role", { - assumedBy: new ServicePrincipal("apigateway.amazonaws.com"), - }); - - eventBus.grantPutEventsTo(role); - - this.eventBridgeIntegration = new AwsIntegration({ - service: "events", - action: "PutEvents", - integrationHttpMethod: "POST", - options: { - credentialsRole: role, - requestTemplates: { - "application/json": ` - #set($context.requestOverride.header.X-Amz-Target ="AWSEvents.PutEvents") - #set($context.requestOverride.header.Content-Type ="application/x-amz-json-1.1") - ${JSON.stringify({ - Entries: [ - { - DetailType: "putEvent", - Detail: "$util.escapeJavaScript($input.json('$'))", - Source: "async-eventbridge-api", - EventBusName: eventBus.eventBusArn, - }, - ], - })} - `, - }, - integrationResponses: [ - { - statusCode: "201", - responseParameters: { - "method.response.header.Access-Control-Allow-Headers": - "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", - "method.response.header.Access-Control-Allow-Methods": - "'GET,POST,OPTIONS'", - "method.response.header.Access-Control-Allow-Origin": "'*'", - }, - responseTemplates: { - "application/json": JSON.stringify({ - id: "$input.path('$.Entries[0].EventId')", - }), - }, - }, - ], - }, - }); - - const btcGuessStateMachine = new aws_events_targets.SfnStateMachine( - stateMachine - ); - - new aws_events.Rule(this, "start", { - eventBus, - targets: [btcGuessStateMachine], - eventPattern: { - detailType: ["putEvent"], - }, - }); - } -} diff --git a/packages/backend/infrastructure/constructs/rest-api.ts b/packages/backend/infrastructure/constructs/rest-api.ts index baca69a..1915178 100644 --- a/packages/backend/infrastructure/constructs/rest-api.ts +++ b/packages/backend/infrastructure/constructs/rest-api.ts @@ -1,6 +1,5 @@ import { CfnOutput } from "aws-cdk-lib"; import { - AwsIntegration, JsonSchemaType, Model, RequestValidator, @@ -8,13 +7,13 @@ import { LambdaIntegration, Cors, MethodLoggingLevel, - LambdaRestApi + LambdaRestApi, } from "aws-cdk-lib/aws-apigateway"; import { Construct } from "constructs"; import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; interface Props { - eventBridgeIntegration: AwsIntegration; + newGuessLambda: NodejsFunction; checkResultLambda: NodejsFunction; } @@ -24,7 +23,7 @@ export class RestApi extends Construct { constructor(scope: Construct, id: string, props: Props) { super(scope, id); - const { eventBridgeIntegration, checkResultLambda } = props; + const { newGuessLambda, checkResultLambda } = props; const api = new AwsRestApi(this, "api", { description: "entry point for the bitcoin guessr api", @@ -65,27 +64,45 @@ export class RestApi extends Construct { .addResource("check-result") .addResource("{id}"); - createNewGameResource.addMethod("POST", eventBridgeIntegration, { - methodResponses: [ - { - statusCode: "201", - responseParameters: { - "method.response.header.Access-Control-Allow-Headers": true, - "method.response.header.Access-Control-Allow-Methods": true, - "method.response.header.Access-Control-Allow-Credentials": true, - "method.response.header.Access-Control-Allow-Origin": true, + createNewGameResource.addMethod( + "POST", + new LambdaIntegration(newGuessLambda, { + proxy: true, + integrationResponses: [ + { + statusCode: "201", + responseParameters: { + "method.response.header.Access-Control-Allow-Headers": + "'Content-Type,Accept,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", + "method.response.header.Access-Control-Allow-Methods": + "'GET,POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Origin": "'*'", + }, }, - }, - ], - requestValidator: new RequestValidator(this, "body-validator", { - restApi: api, - requestValidatorName: "body-validator", - validateRequestBody: true, + ], }), - requestModels: { - "application/json": apiValidationModel, - }, - }); + { + methodResponses: [ + { + statusCode: "201", + responseParameters: { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Allow-Credentials": true, + "method.response.header.Access-Control-Allow-Origin": true, + }, + }, + ], + requestValidator: new RequestValidator(this, "body-validator", { + restApi: api, + requestValidatorName: "body-validator", + validateRequestBody: true, + }), + requestModels: { + "application/json": apiValidationModel, + }, + } + ); checkResultResource.addMethod( "GET", diff --git a/packages/backend/infrastructure/constructs/state-machine.ts b/packages/backend/infrastructure/constructs/state-machine.ts index d37eac4..f989d4d 100644 --- a/packages/backend/infrastructure/constructs/state-machine.ts +++ b/packages/backend/infrastructure/constructs/state-machine.ts @@ -18,11 +18,6 @@ export class StateMachine extends Construct { const { handleResultLambda, newGuessLambda } = props; - const newGuessTask = new tasks.LambdaInvoke(this, "New guess", { - lambdaFunction: newGuessLambda, - outputPath: "$.Payload", - }); - const waitTask = new sfn.Wait(this, "Wait x seconds", { time: sfn.WaitTime.secondsPath("$.waitSeconds"), }); @@ -31,8 +26,7 @@ export class StateMachine extends Construct { outputPath: "$.Payload", }); - const definition = newGuessTask - .next(waitTask) + const definition = waitTask .next(handleResultTask) .next( new sfn.Choice(this, "Did the price change?") @@ -48,5 +42,7 @@ export class StateMachine extends Construct { stateMachineType: sfn.StateMachineType.EXPRESS, timeout: Duration.minutes(2), }); + + this.machine.grantStartExecution(newGuessLambda); } } diff --git a/packages/backend/infrastructure/stacks/backend.ts b/packages/backend/infrastructure/stacks/backend.ts index 55e1ddf..7fba184 100644 --- a/packages/backend/infrastructure/stacks/backend.ts +++ b/packages/backend/infrastructure/stacks/backend.ts @@ -4,15 +4,12 @@ import { DynamoDbTable } from "../constructs/ddb"; import { RestApi } from "../constructs/rest-api"; import { StateMachine } from "../constructs/state-machine"; import { Lambdas } from "../constructs/lambdas"; -import { EventBus } from "aws-cdk-lib/aws-events"; -import { APIEventBridgeIntegration } from "../constructs/eb-integration"; export class BackendStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const table = new DynamoDbTable(this, "GuessTable"); - const eventBus = new EventBus(this, "eventBus"); const { handleResultLambda, newGuessLambda, checkResultLambda } = new Lambdas(this, "LambdaFns", { @@ -25,17 +22,13 @@ export class BackendStack extends Stack { handleResultLambda: handleResultLambda.lambda, }); - const { eventBridgeIntegration } = new APIEventBridgeIntegration( - this, - "APIEventBridgeIntegration", - { - eventBus, - stateMachine: machine, - } + newGuessLambda.lambda.addEnvironment( + "STEP_FUNCTION_ARN", + machine.stateMachineArn ); new RestApi(this, "NewGuessApi", { - eventBridgeIntegration, + newGuessLambda: newGuessLambda.lambda, checkResultLambda: checkResultLambda.lambda, }); } diff --git a/packages/backend/lib/lambda/new-guess.ts b/packages/backend/lib/lambda/new-guess.ts index 4ca7c63..8a6fd49 100644 --- a/packages/backend/lib/lambda/new-guess.ts +++ b/packages/backend/lib/lambda/new-guess.ts @@ -1,29 +1,32 @@ import { Logger } from "@aws-lambda-powertools/logger"; -import { DynamoDB } from "aws-sdk"; +import { DynamoDB, StepFunctions } from "aws-sdk"; import { getEnvVarOrThrow } from "../../utils/helper"; import { getCurrentBitcoinPriceInUSD } from "../../utils/bitcoin-api"; -import { EventBridgeEvent } from "aws-lambda"; +import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda"; import { GameStatus, GuessData } from "../../types"; +import { nanoid } from "nanoid"; const NEW_GUESS_TABLE_NAME = getEnvVarOrThrow("NEW_GUESS_TABLE_NAME"); +const STEP_FUNCTION_ARN = getEnvVarOrThrow("STEP_FUNCTION_ARN"); const logger = new Logger({ serviceName: "New guess" }); const ddb = new DynamoDB.DocumentClient(); +const stepfunctions = new StepFunctions(); type NewGuessBody = Pick<GuessData, "guess">; export async function main( - event: EventBridgeEvent<any, any> -): Promise<{ body: string }> { + event: APIGatewayProxyEventV2 +): Promise<APIGatewayProxyResultV2> { logger.info("new guess", { event: JSON.stringify(event) }); try { - const body = event.detail as NewGuessBody; + const body = JSON.parse(event.body!) as NewGuessBody; const oldPrice = await getCurrentBitcoinPriceInUSD(); - + const id = nanoid(); const newGuessEntry = { - id: event.id, + id, timestamp: new Date().toISOString(), oldPrice, gameStatus: GameStatus.Processing, @@ -39,14 +42,25 @@ export async function main( await ddb.put(ddbNewGuessParam).promise(); - return Promise.resolve({ - body: JSON.stringify(newGuessEntry), - waitSeconds: 60, + stepfunctions.startExecution({ + stateMachineArn: STEP_FUNCTION_ARN, + name: "state-machine", + input: JSON.stringify({ + body: JSON.stringify(newGuessEntry), + waitSeconds: 60, + }), }); + + return { + statusCode: 201, + body: JSON.stringify({ id }), + }; } catch (error) { logger.error("error on api execution for new guess init", { msg: JSON.stringify(error), }); - return Promise.reject(error); + return { + statusCode: 500, + }; } }