Skip to content

naming conventions: cross-service integrations #1743

@rix0rrr

Description

@rix0rrr

Acute need came out of #1646, but this pattern crops up more, for example in #1663 (TBD).

Problem Statement

We have classes that users can use to link certain services together. Examples are CloudWatch Targets, StepFunctions Tasks, Lambda Event Sources, and probably there are more occurrences of this that I can't think of right now.

Usually such an integration is event based, and usually there are 3 entities involved:

                                                                          
    ┌───────────────┐    Customization    ┌───────────────┐               
    │               │                     │               │               
    │ Event Source  │────────────────────▶│ Event Target  │               
    │               │                     │               │               
    └───────────────┘                     └───────────────┘               
StepFunctions Workflow                       DDB Table                    
CloudWatch Event                             SNS Topic                    
S3/SQS/.. Lambda Event Source                ECS Task                     
                                             Lambda Function              
                                             ...                          

Obviously there will be a source and a target, and usually the event passing is customized in some way as well (by specifying additional properties).


Today, our integrations look like this:

CloudWatch Events

// Pretty easy, run the Lambda
eventRule.addTarget(myLambda);

// Still okay, run the Lambda with a JSON payload
eventRule.addTarget(myLambda, {
  jsonTemplate: {
    SomeArgument: 'Value'
  }
});


// Oops, the configuration object becomes super complex all of a sudden
const target = new ecs.Ec2EventRuleTarget(this, 'EventTarget', {
  cluster,
  taskDefinition,
  taskCount: 1
});

rule.addTarget(target, {
  jsonTemplate: {
    containerOverrides: [{
      name: 'TheContainer',
      environment: [{ name: 'I_WAS_TRIGGERED', value: 'From CloudWatch Events' }]
    }]
  }
});

The configuration becomes super complex when we try to enable interactions between more flexible services, such as ECS, which has various parameters that control the invocation. With the code as written, we even have to split those parameters over two statements, and there's no static or runtime safety in the second statement.

In this case, we would benefit from having everything in the Ec2EventRuleTarget class, and we should probably make similar classes for the other service integrations in order to stay consistent across the framework (instead of using the resource directly as the integration point).

Question is, what should those classes be named, and in what module should they live?

Right now, the integrations are implemented in the module of the resource.

Step Functions

To invoke a Lambda, Step Functions used to work like this:

new sfn.Task(this, 'Task', {
  // Returns 'arn:aws:lambda:region:account-id:function:function-name', configures permissions on bind(),
  // done.
  resource: myLambda
});

We would take the ARN of the Lambda, give that to StepFunctions, and everything would be good.

They've now launched integration with other services, and there are some complications.

  • Targets are no longer represented by the resource ARN, but by a magic ARN. For example, to run an ECS Task you don't specify any resource's ARN, but you specify Resource: "arn:aws:states:::ecs:runTask.sync", and in an additional field Parameters you put something like:
     "Parameters": {
            "Cluster": "cluster-arn",
            "TaskDefinition": "job-id",
            "Overrides": {
                "ContainerOverrides": [
                    {
                        "Name": "container-name",
                        "Command.$": "$.commands" 
                    }
                ]
            }
        },

We could model this as follows:

new sfn.Task(this, 'Task', {
  resource: new Ec2StepFunctionsTask(this, 'Ec2Task', {
    cluster,
    taskDefinition,
    overrides: [ ... ]
  })
});

But it would be even nicer to combine the two:

new Ec2StepFunctionsTask(this, 'Ec2Task', {
  cluster,
  taskDefinition,
  overrides: [ ... ]
})

And be able to use that object in a workflow directly. Again, what do we name this class, and in what module does it live?

Right now, the integrations are implemented in the module of the resource.

Lambda Event Sources

Lambda Event Sources already have taken the model where we actually have a class per integration (FooEventSource) and they all live in their own module @aws-cdk/aws-lambda-event-sources.

Used as:

const source = new SqsEventSource(...);

lambda.addEventSource(source);

Complications

NAMING

  • First and foremost for the ECS case, we have an overload of the word Task. A Task is a running instance of a task definition (same as a process is a running instance of an executable, etc.), but a Task is also a step in a Step Functions workflow that performs an action. So to be fully correct, a StepFunctions Task to run an ECS Task should be named something like FargateRunTaskTask.
  • Do we prefix or suffix? Prefix will be easier for discoverability and autocomplete, but suffix makes more sense for readability (compare: FargateRunTask vs. SfnFargateRunTask)
  • Understandability of the name depends on the package. A simple name like FargateRunTask might suffice in a package named @aws-cdk/aws-stepfunctions-tasks, but it might not be so clear in the aws-ecs package proper.

LOCATION

  • If we implement integrations in the integrated resource's package, we might run into code ownership issues down the line. If in some hypothetical future StepFunctions adds an integration with BloopService, they will need the cooperation of the owners of @aws-cdk/aws-bloop to land that integration in the CDK. This might be desirable as a forcing function, or it might not be because of expediency.
  • What is the most discoverable location for integration classes? Next to the resources that they integrate with, or in a special package that contains all integrations? I have a feeling the latter is more discoverable, but I'm not sure.

Honestly, I don't have any good proposals here yet. I'm leaning towards following the Lambda Event Sources model, where integrations go into their own package with suffix naming. So I think I'd like:

  • aws-stepfunctions-tasks with FargateRunTask, TablePutItemTask, etc.
  • aws-events-targets with FargateRun(Task)Target, InvokeFunctionTarget, etc.

I'm interested in hearing opinions though.

Metadata

Metadata

Assignees

No one assigned

    Labels

    @aws-cdk/coreRelated to core CDK functionality

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions