Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core-app-plane)!: replace BashJobRunner with ScriptJob #79

Merged
merged 3 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
copyrightPeriod: '2024-',
defaultReleaseBranch: 'main',
deps: [
'@aws-cdk/aws-lambda-python-alpha',
`@aws-cdk/aws-lambda-python-alpha@${CDK_VERSION}-alpha.0`,
tobuck-aws marked this conversation as resolved.
Show resolved Hide resolved
'cdk-nag',
`@aws-cdk/aws-kinesisfirehose-alpha@${CDK_VERSION}-alpha.0`,
`@aws-cdk/aws-kinesisfirehose-destinations-alpha@${CDK_VERSION}-alpha.0`,
Expand Down
2,073 changes: 1,304 additions & 769 deletions API.md

Large diffs are not rendered by default.

50 changes: 22 additions & 28 deletions docs/public/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export class ApplicationPlaneStack extends Stack {

new sbt.CoreApplicationPlane(this, 'CoreApplicationPlane', {
eventManager: props.eventManager,
jobRunnersList: [],
scriptJobs: [],
});
}
}
Expand All @@ -191,43 +191,39 @@ Again, SBT allows builders to publish and subscribe directly to EventBridge, and

#### Core Application Plane Utilities

Although entirely optional, SBT includes a utility that lets you define, and run arbitrary jobs upon receipt of a control plane message, called a `JobRunner`. This is the mechanism currently used for onboarding and off-boarding in the reference architectures which were ported to SBT (see references at the end of this document). That tenant provisioning/deprovisioning process is depicted below:
Although entirely optional, SBT includes a utility that lets you define, and run arbitrary jobs upon receipt of a control plane message, called a `ScriptJob`. This mechanism is extended to produce two new helper constructs `ProvisioningScriptJob` and `DeprovisioningScriptJob` which are used for onboarding and off-boarding, respectively, in the reference architectures which were ported to SBT (see references at the end of this document). That tenant provisioning/deprovisioning process is depicted below:

![sbt-provisioning.png](../../images/sbt-provisioning.png)

Notice the use of the `provisioning.sh` and `deprovisioning.sh` scripts at the top. These scripts are fed to the `JobRunner` as parameters. Internally the `JobRunner` launches an AWS CodeBuild project, wrapped inside an AWS Step Function, to execute the bash scripts. The `JobRunner` also lets you specify what input variables to feed to the scripts, along with what output variables you expect them to return. Note that in this version of SBT, you can create `JobRunner`s with [`jobRunnerProps`](/API.md#bashjobrunnerprops-) and configure `CoreAppPlane` with `JobRunner`s using its `jobRunnersList` property. The `CoreAppPlane` will then link these `JobRunner`s to EventBridge. Let's take a simple example: imagine our SaaS application deployed only a single S3 bucket per tenant. Let's create a job runner for that provisioning now.
Notice the use of the `provisioning.sh` and `deprovisioning.sh` scripts at the top. These scripts are fed to the `ProvisioningScriptJob` and `DeprovisioningScriptJob` as parameters. Internally the `ScriptJob` launches an AWS CodeBuild project, wrapped inside an AWS Step Function, to execute the bash scripts. The `ScriptJob` also lets you specify what input variables to feed to the scripts, along with what output variables you expect them to return. Note that in this version of SBT, you can create the `ScriptJob` construct with [`ScriptJobProps`](/API.md#scriptjobprops-) and configure `CoreAppPlane` with `ScriptJob`s using its `scriptJobs` property. The `CoreAppPlane` will then link these `ScriptJob`s to EventBridge. Let's take a simple example: imagine our SaaS application deployed only a single S3 bucket per tenant. Let's create a `ProvisioningScriptJob` for that provisioning now.

```typescript
const provisioningJobRunnerProps = {
const scriptJobProps: TenantLifecycleScriptJobProps = {
permissions: PolicyDocument.fromJson(/*See below*/),
script: '' /*See below*/,
environmentStringVariablesFromIncomingEvent: ['tenantId', 'tier'],
environmentVariablesToOutgoingEvent: ['tenantS3Bucket', 'someOtherVariable', 'tenantConfig'],
scriptEnvironmentVariables: {
TEST: 'test',
},
outgoingEvent: sbt.DetailType.PROVISION_SUCCESS,
incomingEvent: sbt.DetailType.ONBOARDING_REQUEST,
eventManager: eventManager /*See below on how to create EventManager*/,
};
```

##### Bash Job Runner Properties
##### ProvisioningScriptJob and DeprovisioningScriptJob Properties (TenantLifecycleScriptJobProps)

Let's take a moment and dissect this object.

| Key | Type | Purpose |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| **script** | string | A string in bash script format that represents the job to be run (example below) |
| **permissions** | [PolicyDocument](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.PolicyDocument.html) | An IAM policy document giving this job the IAM permisisons it needs to do what it's being asked to do |
| **environmentStringVariablesFromIncomingEvent** | string[] | The environment variables to import into the BashJobRunner from event details field. |
| **environmentVariablesToOutgoingEvent** | string[] | The environment variables to export into the outgoing event once the BashJobRunner has finished. |
| **scriptEnvironmentVariables** | `{ [key: string]: string }` | The variables to pass into the codebuild BashJobRunner. |
| **outgoingEvent** | any | Outgoing EventBridge wiring details |
| **incomingEvent** | any | Incoming EventBridge wiring details |
| **permissions** | [PolicyDocument](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.PolicyDocument.html) | An IAM policy document giving this job the IAM permissions it needs to do what it's being asked to do |
| **environmentStringVariablesFromIncomingEvent** | string[] | The environment variables to import into the ScriptJob from event details field. |
| **environmentVariablesToOutgoingEvent** | string[] | The environment variables to export into the outgoing event once the ScriptJob has finished. |
| **scriptEnvironmentVariables** | `{ [key: string]: string }` | The variables to pass into the codebuild ScriptJob. |
| **eventManager** | [IEventManager](/API.md#ieventmanager-) | The EventManager instance that allows connecting to events flowing between the Control Plane and other components. |

The heavy lifting of the `JobRunner` happens with the value of the `script` key. Recall, that this particular example is for provisioning. Also remember that the "SaaS application" we're illustrating here is only provisioning a new S3 bucket for each tenant. Let's take a look at that example provisioning script now:
The heavy lifting of the `ScriptJob` construct (along with constructs that extend it like `ProvisioningScriptJob`) happens with the value of the `script` key. Let's take a look at the example provisioning script now:

```sh
echo "starting..."
Expand Down Expand Up @@ -296,7 +292,7 @@ echo "tenantId: $tenantId"
echo "tier: $tier"
```

Let's examine how exactly those variables get populated. Remember that the `JobRunner` creates an [AWS CodeBuild](https://docs.aws.amazon.com/codebuild/latest/userguide/welcome.html) project internally. When the `JobRunner` creates the CodeBuild project, it can specify what environment variables to provide. Also recall that the `JobRunner` utility is activated with an EventBridge message matching the criteria specified in the `incomingEvent` parameter of the `jobRunnerProps`. The message that arrives via EventBridge has a `detail` JSON Object (see [docs here](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-events-structure.html)) that carries with it contextual information included by the sender, in our case, the control plane. For each key in the `environmentStringVariablesFromIncomingEvent` key, the `JobRunner` extracts the value of a matching key found in the EventBridge message's detail JSON object, and provides that value to the CodeBuild project as an environment variable.
Let's examine how exactly those variables get populated. Remember that the `ScriptJob` (which is used to extend the `ProvisioningScriptJob` construct) creates an [AWS CodeBuild](https://docs.aws.amazon.com/codebuild/latest/userguide/welcome.html) project internally. When the `ScriptJob` creates the CodeBuild project, it can specify what environment variables to provide. The `ScriptJob` utility is also triggered by an EventBridge message matching the criteria specified in the `incomingEvent` parameter of the `ScriptJobProps`. (You don't need to worry about doing that for `ProvisioningScriptJob` and `DeprovisioningScriptJob` because that is already configured.) The message that arrives via EventBridge has a `detail` JSON Object (see [docs here](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-events-structure.html)) that carries with it contextual information included by the sender, in our case, the control plane. For each key in the `environmentStringVariablesFromIncomingEvent` key, the `ScriptJob` extracts the value of a matching key found in the EventBridge message's detail JSON object, and provides that value to the CodeBuild project as an environment variable.

So, take for example, this sample EventBridge provisioning message sent by a control plane:

Expand Down Expand Up @@ -347,7 +343,7 @@ echo $tenantConfig
export tenantStatus="created"
```

Similar to how it mapped incoming EventBridge message detail variables to environment variables, the `JobRunner` does almost the same thing but in reverse. The variables specified in the `environmentVariablesToOutgoingEvent` section of `jobRunnerProps` will be extracted from the environment, and sent back in the EventBridge message's detail section.
Similar to how it mapped incoming EventBridge message detail variables to environment variables, the `ScriptJob` does almost the same thing but in reverse. The variables specified in the `environmentVariablesToOutgoingEvent` section of `ScriptJobProps` will be extracted from the environment, and sent back in the EventBridge message's detail section.

#### Putting it all together

Expand All @@ -366,7 +362,7 @@ export class AppPlaneStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: AppPlaneProps) {
super(scope, id, props);

const provisioningJobRunnerProps = {
const provisioningScriptJobProps: sbt.TenantLifecycleScriptJobProps = {
permissions: new PolicyDocument({
statements: [
new PolicyStatement({
Expand Down Expand Up @@ -422,20 +418,18 @@ echo "done!"
scriptEnvironmentVariables: {
TEST: 'test',
},
outgoingEvent: sbt.DetailType.PROVISION_SUCCESS,
incomingEvent: sbt.DetailType.ONBOARDING_REQUEST,
eventManager: props.eventManager,
};

const provisioningJobRunner: sbt.BashJobRunner = new sbt.BashJobRunner(
const provisioningJobScript: sbt.ProvisioningScriptJob = new sbt.ProvisioningScriptJob(
this,
'provisioningJobRunner',
provisioningJobRunnerProps
'provisioningJobScript',
provisioningScriptJobProps
);

new sbt.CoreApplicationPlane(this, 'CoreApplicationPlane', {
eventManager: props.eventManager,
jobRunnersList: [provisioningJobRunner],
eventManager: eventManager,
scriptJobs: [provisioningJobScript],
});
}
}
Expand Down Expand Up @@ -543,7 +537,7 @@ Now that we've onboarded a tenant, let's take a look at the console to see what

First, let's open the [DynamoDB console](https://console.aws.amazon.com/dynamodbv2/home#). Once open, click the `Explore Items` link on the left. On the "Tables" screen, select the table that starts with `ControlPlaneStack`. Notice there is an entry for the tenant we just onboarded. Also notice it's probably still "in progress"

Recall that we deployed a `JobRunner` with our application plane, and it's a wrapper around an AWS Step Function that runs our provisioning script via CodeBuild. Let's take a look at that Step Function now by clicking navigating to [Step Functions in the console](https://console.aws.amazon.com/states/home) (ensure you're in the same region you deployed to).
Recall that we deployed a `ScriptJob` with our application plane, and it's a wrapper around an AWS Step Function that runs our provisioning script via CodeBuild. Let's take a look at that Step Function now by clicking navigating to [Step Functions in the console](https://console.aws.amazon.com/states/home) (ensure you're in the same region you deployed to).

The Step Function is likely still running, but feel free to examine the execution. Once finished, it'll return the results back to EventBridge, and close the loop with the Control plane.

Expand Down Expand Up @@ -575,7 +569,7 @@ The control plane emits this event any time it onboards a new tenant. This event

#### Tenant Provision Success

As per our configuration, the application plane emits this event upon completion of onboarding. It contains the `tenantId` and a `tenantOutput` object containing the environment variables (key/value pairs) whose keys have been identified in the `environmentVariablesToOutgoingEvent` parameter. In the example above, a provision success event would look something like this:
As per our configuration, the application plane emits this event upon completion of onboarding. It contains the `tenantId` and a `jobOutput` object containing the environment variables (key/value pairs) whose keys have been identified in the `environmentVariablesToOutgoingEvent` parameter. In the example above, a provision success event would look something like this:

##### Sample provision success event

Expand All @@ -584,7 +578,7 @@ As per our configuration, the application plane emits this event upon completion
"source": "applicationPlaneEventSource",
"detail-type": "provisionSuccess",
"detail": {
"tenantOutput": {
"jobOutput": {
"tenantStatus": "created",
"tenantConfig": "{\n \"userPoolId\": \"MY_SAAS_APP_USERPOOL_ID\",\n \"appClientId\": \"MY_SAAS_APP_CLIENT_ID\",\n \"apiGatewayUrl\": \"MY_API_GATEWAY_URL\"\n}",
"tenantName": "tenant$RANDOM",
Expand Down Expand Up @@ -624,7 +618,7 @@ The application plane emits this event upon completion of offboarding. Similar t
"source": "applicationPlaneEventSource",
"detail-type": "deprovisionSuccess",
"detail": {
"tenantOutput": {
"jobOutput": {
// defined in the deprovisioning job configuration
},
"tenantId": "guid string"
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion scripts/sbt-aws.sh
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ case "$1" in

"get-tenant")
if [ $# -ne 2 ]; then
echo "Error: delete-tenant requires tenant id"
echo "Error: get-tenant requires tenant id"
tobuck-aws marked this conversation as resolved.
Show resolved Hide resolved
exit 1
fi
get_tenant "$2"
Expand Down
6 changes: 3 additions & 3 deletions src/control-plane/integ.default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as cdk from 'aws-cdk-lib';
import { CfnRule, EventBus, Rule } from 'aws-cdk-lib/aws-events';
import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs';
import { AwsSolutionsChecks } from 'cdk-nag';
import { CognitoAuth, ControlPlane } from '.';
import * as sbt from '.';
import { DestroyPolicySetter } from '../cdk-aspect/destroy-policy-setter';

export interface IntegStackProps extends cdk.StackProps {
Expand All @@ -16,11 +16,11 @@ export class IntegStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: IntegStackProps) {
super(scope, id, props);

const cognitoAuth = new CognitoAuth(this, 'CognitoAuth', {
const cognitoAuth = new sbt.CognitoAuth(this, 'CognitoAuth', {
setAPIGWScopes: false, // only for testing purposes!
});

const controlPlane = new ControlPlane(this, 'ControlPlane', {
const controlPlane = new sbt.ControlPlane(this, 'ControlPlane', {
auth: cognitoAuth,
systemAdminEmail: props.systemAdminEmail,
});
Expand Down
17 changes: 7 additions & 10 deletions src/control-plane/tenant-management/tenant-management.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,20 +133,17 @@ export class TenantManagementService extends Construct {

const tenantUpdateServiceTarget = new ApiDestination(putTenantAPIDestination, {
pathParameterValues: ['$.detail.tenantId'],
event: events.RuleTargetInput.fromEventPath('$.detail.tenantOutput'),
event: events.RuleTargetInput.fromEventPath('$.detail.jobOutput'),
});

props.eventManager.addTargetToEvent(
this,
[
DetailType.PROVISION_SUCCESS,
tenantUpdateServiceTarget
);

props.eventManager.addTargetToEvent(
this,
DetailType.PROVISION_FAILURE,
DetailType.DEPROVISION_SUCCESS,
tenantUpdateServiceTarget
);
DetailType.DEPROVISION_FAILURE,
].forEach((detailType) => {
props.eventManager.addTargetToEvent(this, detailType, tenantUpdateServiceTarget);
});

this.table = table;
}
Expand Down
Loading
Loading