The @arc-saas/orchestrator-service is designed to provide the standard interfaces and endpoint to handle the events sent from / to a SaaS Control Plane. This acts as a orchestrator for the event targets/processors.
Consider the following example architecture that uses Amazon EventBridge at the center to pass on the events, and this Orchestrator service is used as its initial target, so that the events can then be sent to the expected candidates to process the event.
Above example is of a tenant provisioning event flow, as shown it originates from a control plane service called tenant management service and then when it's received to the Amazon EventBridge, it passes it to the orchestrator service which can run any bussiness logic before it's sent for processing (the example illustrates starting the codebuild or jenkins job conditionally based on the event). Further code examples in this README will take this same reference.
Bind the OrchestratorServiceComponent to your application constructor as shown below, this will load the built-in artifacts provided by the service in your application to use.
Mainly the EventController that provides the endpoint to receive the events.
This LB4 service is used by the event controller which invokes the handleEvent method and passes the type of the event along with the request body containing the event detail as the function arguments.
Following is the interface you'll need to implement in your class that is to be bound to the key OrchestratorServiceBindings.ORCHESTRATOR_SERVICE.
interfaceOrchestratorServiceInterface<EventTypeextendsstring=DefaultEventTypes,BodyTypeextendsAnyObject=AnyObject>{handleEvent(eventType:EventType,eventBody:BodyType):Promise<void>;}// PS: It is available to be imported from this package.
Here's how you can bind your custom orchestrator service:
This LB4 service is intended to be used by any logic that requires triggering a build job. This common interface has a method called startJob which can take the job identifier and the params object as its arguments.
For example you can implement a triggering codebuild in this service whenever a specific event is received.
Following is the interface you'll need to implement in your class that is to be bound to the key OrchestratorServiceBindings.BUILDER_SERVICE.
interfaceBuilderServiceInterface{startJob(jobIdentifier:string,params:AnyObject):Promise<void>;}// PS: It is available to be imported from this package.
Here's how you can bind your builder service implementation:
This provider is intended to be used as the handler to get the tier details. Which can be used while provisioning the tenant or anywhere when we need to retrieve the tier details.
For example you can implement a data pulling logic from Amazon DynamoDB that contains the tier details (the example implementation is given below).
Here's how you can bind your tier details provider:
import{injectable,BindingScope,Provider}from"@loopback/core";import{TierDetailsFn}from"@arc-saas/orchestrator-service";import{marshall,unmarshall}from"@aws-sdk/util-dynamodb";import{DynamoDBClient,QueryCommand}from"@aws-sdk/client-dynamodb";@injectable({scope:BindingScope.TRANSIENT})exportclassTierDetailsProviderimplementsProvider<TierDetailsFn>{value(){returnasync(tier:string)=>{returnthis.fetchTierDetails(tier);};}privateasyncfetchTierDetails(tier:string){constclient=newDynamoDBClient({region:"us-east-1"});constparams={TableName:process.env.TIER_DETAILS_TABLE,KeyConditionExpression:"tier = :tier",ExpressionAttributeValues:marshall({":tier":tier,}),};try{constcommand=newQueryCommand(params);constresponse=awaitclient.send(command);if(!response.Items){throwError("Items not found.");}constitems=response.Items.map((item)=>unmarshall(item));console.log("Query results:",items);if(items.length===0){thrownewError(`Provided tier details not found in table: "${process.env.TIER_DETAILS_TABLE}".`);}consttierDetails=items[0];return{...tierDetails};}catch(error){console.error("Error fetching data:",error);throwerror;}}}
This provider is intended to be used as the handler to be invoked when the tenant provisioning event is received.
For example, In this provider you can implement the logic of invoking the tier details provider and then triggering the provisioning job using the builder service.
import{injectable,BindingScope,Provider,inject}from"@loopback/core";import{AnyObject}from"@loopback/repository";import{BuilderService,TenantProvisioningHandler,TierDetailsFn,OrchestratorServiceBindings,}from"@arc-saas/orchestrator-service";exporttypeProvisioningInputs={planConfig:AnyObject;builderConfig:AnyObject;};@injectable({scope:BindingScope.TRANSIENT})exportclassTenantProvisioningHandlerProviderimplementsProvider<TenantProvisioningHandler<ProvisioningInputs>>{constructor(@inject(OrchestratorServiceBindings.TIER_DETAILS_PROVIDER)privatetierDetails:TierDetailsFn,@inject(OrchestratorServiceBindings.BUILDER_SERVICE)privatebuilderService:BuilderService){}value(){returnasync(body:ProvisioningInputs)=>{// Extract plan and builder information from the bodyconstplanConfig=body.planConfig;constbuilder=body.builderConfig;consttier=planConfig.tier;try{// Fetch tier details based on the provided tierconst{jobIdentifier,...otherTierDetails}=awaitthis.tierDetails(tier);constjobName=jobIdentifier;// Ensure Job name is present in the tier detailsif(!jobName){thrownewError("Builder Job name not found in plan details");}// Check if the builder type is CODE_BUILDif(builder?.type==="CODE_BUILD"){// Trigger CodeBuild with the necessary environmentsconstcodeBuildResponse=awaitthis.builderService.startJob(jobName,{...otherTierDetails,...(builder?.config?.environmentOverride??{}),});console.log(codeBuildResponse);return;}else{// Throw an error if the builder config is invalidthrowError("Invalid builder config provided.");}}catch(error){console.error("Error in tenant provisioning:",error);return;}};}}
This provider is intended to be used as the handler to be invoked when the tenant deprovisioning event is received.
For example, In this provider you can implement any business logic you want to execute before triggering the deprovisioning job using the builder service.
This provider is intended to be used as the handler to be invoked when the tenant provisioning success event is received i.e. when the provisioning job is completed.
For example, In this provider you can implement further steps needed to onboard the tenant.
The way of binding this provider is similar to others, just the binding key is OrchestratorServiceBindings.TENANT_PROVISIONING_SUCCESS_HANDLER.
This provider is intended to be used as the handler to be invoked when the tenant provisioning failed event is received i.e. when the provisioning job is failed for some reason.
For example, In this provider you can implement any cleanup or notification needed to handle the failure.
The way of binding this provider is similar to other provider, just the binding key is OrchestratorServiceBindings.TENANT_PROVISIONING_FAILURE_HANDLER.
This provider is intended to be the handler of the tenant deployment, which means any work after the provisioning that is required to make the tenant's application up and running, should be implemented here.
For example, this can be adding some initial users. Populating some seed data in the application service etc.
The way of binding this provider is similar to other provider, just the binding key is OrchestratorServiceBindings.TENANT_DEPLOYMENT_HANDLER.
The @arc-saas/orchestrator-service can be deployed in various ways, including as a serverless application. Here's how you can set it up for serverless deployment, specifically for AWS Lambda.
Push the Docker image to your container registry (e.g., Amazon ECR).
Create a Lambda function using the pushed container image.
Configure an API Gateway to trigger your Lambda function.
This setup allows you to run your Orchestrator Service as a serverless application, leveraging AWS Lambda's scalability and cost-efficiency.
Remember to adjust your Lambda function's configuration (memory, timeout, etc.) based on your specific needs.