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

feat(location): support Tracker and TrackerConsumer #31268

Merged
merged 5 commits into from
Oct 28, 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
45 changes: 45 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,48 @@ const routeCalculator = new location.RouteCalculator(this, 'RouteCalculator', {
});
routeCalculator.grantRead(role);
```

## Tracker

A tracker stores position updates for a collection of devices. The tracker can be used to query the devices' current location or location history. It stores the updates, but reduces storage space and visual noise by filtering the locations before storing them.

For more information, see [Trackers](https://docs.aws.amazon.com/location/latest/developerguide/geofence-tracker-concepts.html#tracking-overview).

To create a tracker, define a `Tracker`:

```ts
declare const key: kms.Key;

new location.Tracker(this, 'Tracker', {
trackerName: 'MyTracker', // optional, defaults to a generated name
kmsKey: key, // optional, defaults to use an AWS managed key
});
```

Use the `grant()`, `grantUpdateDevicePositions` or `grantRead()` method to grant the given identity permissions to perform actions
on the geofence collection:

```ts
declare const role: iam.Role;

const tracker = new location.Tracker(this, 'Tracker', {
trackerName: 'MyTracker',
});

tracker.grantRead(role);
```

If you want to associate a tracker with geofence collections, define a `geofenceCollections` property or use `addGeofenceCollections` method.

```ts
declare const geofenceCollection: location.GeofenceCollection;
declare const geofenceCollectionForAdd: location.GeofenceCollection;
declare const tracker: location.Tracker;

const tracker = new location.Tracker(this, 'Tracker', {
trackerName: 'MyTracker',
geofenceCollections: [geofenceCollection],
});

tracker.addGeofenceCollections(geofenceCollectionForAdd);
```
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-location-alpha/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './geofence-collection';
export * from './place-index';
export * from './route-calculator';
export * from './tracker';
export * from './util';

// AWS::Location CloudFormation Resources:
267 changes: 267 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/lib/tracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import { ArnFormat, IResource, Lazy, Resource, Stack, Token } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { CfnTracker, CfnTrackerConsumer } from 'aws-cdk-lib/aws-location';
import { generateUniqueId } from './util';
import { IGeofenceCollection } from './geofence-collection';

/**
* A Tracker
*/
export interface ITracker extends IResource {
/**
* The name of the tracker
*
* @attribute
*/
readonly trackerName: string;

/**
* The Amazon Resource Name (ARN) of the tracker resource
*
* @attribute Arn, TrackerArn
*/
readonly trackerArn: string;
}

/**
* Properties for a tracker
*/
export interface TrackerProps {
/**
* A name for the tracker
*
* Must be between 1 and 100 characters and contain only alphanumeric characters,
* hyphens, periods and underscores.
*
* @default - A name is automatically generated
*/
readonly trackerName?: string;

/**
* A description for the tracker
*
* @default - no description
*/
readonly description?: string;

/**
* Send filtered device position updates to default EventBridge bus.
*
* @default false
*/
readonly eventBridgeEnabled?: boolean;

/**
* The customer managed key to encrypt data.
* If you set customer managed key, the Bounding Polygon Queries feature will be disabled by default.
* You can choose to opt-in to the Bounding Polygon Queries feature by setting the kmsKeyEnableGeospatialQueries parameter to true.
*
* @default - Use an AWS managed key
*/
readonly kmsKey?: kms.IKey;

/**
* Whether to opt-in to the Bounding Polygon Queries feature with customer managed key
*
* @default false
*/
readonly kmsKeyEnableGeospatialQueries?: boolean;

/**
* The position filtering for the tracker resource
*
* @default PositionFiltering.TIME_BASED
*/
readonly positionFiltering?: PositionFiltering;

/**
* An optional list of geofence collections to associate with the tracker resource
*
* @default - no geofence collections are associated
*/
readonly geofenceCollections?: IGeofenceCollection[];
}

/**
* The position filtering for the tracker resource
*/
export enum PositionFiltering {
/**
* Location updates are evaluated against linked geofence collections, but not every location update is stored.
* If your update frequency is more often than 30 seconds, only one update per 30 seconds is stored for each unique device ID.
*/
TIME_BASED = 'TimeBased',

/**
* If the device has moved less than 30 m (98.4 ft), location updates are ignored.
* Location updates within this area are neither evaluated against linked geofence collections, nor stored.
* This helps control costs by reducing the number of geofence evaluations and historical device positions to paginate through.
* Distance-based filtering can also reduce the effects of GPS noise when displaying device trajectories on a map.
*/
DISTANCE_BASED = 'DistanceBased',

/**
* If the device has moved less than the measured accuracy, location updates are ignored.
* For example, if two consecutive updates from a device have a horizontal accuracy of 5 m and 10 m,
* the second update is ignored if the device has moved less than 15 m.
* Ignored location updates are neither evaluated against linked geofence collections, nor stored.
* This can reduce the effects of GPS noise when displaying device trajectories on a map,
* and can help control your costs by reducing the number of geofence evaluations.
*/
ACCURACY_BASED = 'AccuracyBased',
}

/**
* A Tracker
*
* @see https://docs.aws.amazon.com/location/latest/developerguide/geofence-tracker-concepts.html#tracking-overview
*/
export class Tracker extends Resource implements ITracker {
/**
* Use an existing tracker by name
*/
public static fromTrackerName(scope: Construct, id: string, trackerName: string): ITracker {
const trackerArn = Stack.of(scope).formatArn({
service: 'geo',
resource: 'tracker',
resourceName: trackerName,
});

return Tracker.fromTrackerArn(scope, id, trackerArn);
}

/**
* Use an existing tracker by ARN
*/
public static fromTrackerArn(scope: Construct, id: string, trackerArn: string): ITracker {
const parsedArn = Stack.of(scope).splitArn(trackerArn, ArnFormat.SLASH_RESOURCE_NAME);

if (!parsedArn.resourceName) {
throw new Error(`Tracker Arn ${trackerArn} does not have a resource name.`);
}

class Import extends Resource implements ITracker {
public readonly trackerName = parsedArn.resourceName!;
public readonly trackerArn = trackerArn;
}

return new Import(scope, id, {
account: parsedArn.account,
region: parsedArn.region,
});
}

public readonly trackerName: string;

public readonly trackerArn: string;

/**
* The timestamp for when the tracker resource was created in ISO 8601 format
*
* @attribute
*/
public readonly trackerCreateTime: string;

/**
* The timestamp for when the tracker resource was last updated in ISO 8601 format
*
* @attribute
*/
public readonly trackerUpdateTime: string;

constructor(scope: Construct, id: string, props: TrackerProps = {}) {

if (props.description && !Token.isUnresolved(props.description) && props.description.length > 1000) {
throw new Error(`\`description\` must be between 0 and 1000 characters. Received: ${props.description.length} characters`);
}

if (props.trackerName && !Token.isUnresolved(props.trackerName) && !/^[-.\w]{1,100}$/.test(props.trackerName)) {
throw new Error(`Invalid tracker name. The tracker name must be between 1 and 100 characters and contain only alphanumeric characters, hyphens, periods and underscores. Received: ${props.trackerName}`);
}

if (!Token.isUnresolved(props.kmsKey)
&& !props.kmsKey
&& props.kmsKeyEnableGeospatialQueries
) {
throw new Error('`kmsKeyEnableGeospatialQueries` can only be enabled that are configured to use an AWS KMS customer managed key');
}

super(scope, id, {
physicalName: props.trackerName ?? Lazy.string({ produce: () => generateUniqueId(this) }),
});

const tracker = new CfnTracker(this, 'Resource', {
trackerName: this.physicalName,
description: props.description,
eventBridgeEnabled: props.eventBridgeEnabled,
kmsKeyEnableGeospatialQueries: props.kmsKeyEnableGeospatialQueries,
kmsKeyId: props.kmsKey?.keyArn,
positionFiltering: props.positionFiltering,
});

props.geofenceCollections?.forEach((collection) => {
new CfnTrackerConsumer(this, `TrackerConsumer${collection.node.id}`, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, I had TrackerConsumer as a separate Construct.
5fb3cdd#diff-fc1da6063a6db97ebec475178eaaa40ce1a7d0a56f8c1b5cb249b99f33e9946a

However, since it only involved associations, I decided to integrate it into the Tracker.
If you have any opinions on this design, please let me know.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, since it only involved associations, I decided to integrate it into the Tracker.
If you have any opinions on this design, please let me know.

@mazyu36 what do you mean by it only involved associations? Just looking to clarify my understanding. Here it seems CDK users would not be able to create TrackerConsumer objects via an L2 - does this make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sumupitchayan
TrackerConsumer is just an association between Tracker and Consumer (GeofenceCollection) and has no actual entity.

I thought the association could be auto-generated, similar to how VPC Subnet and route table associations work.
https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-ec2/lib/vpc.ts#L2117

Alternatively, TrackerConsumer could be made into a separate L2 construct.

Which approach would be better do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mazyu36 thanks for the clarification, this design makes sense to me. From my understanding, customers won't directly be creating TrackerConsumer objects and this case is similar to how VPC Subnets/route table associations are designed.

Additionally, since this is an alpha module and it will be easy to add an L2 for TrackerConsumer for this design, I think we can just do that in the future if we find it is needed for a customer use case.

consumerArn: collection.geofenceCollectionArn,
trackerName: Lazy.string({ produce: () => this.trackerName }),
});
});

this.trackerName = tracker.ref;
this.trackerArn = tracker.attrArn;
this.trackerCreateTime = tracker.attrCreateTime;
this.trackerUpdateTime = tracker.attrUpdateTime;
}

/**
* Add Geofence Collections which are associated to the tracker resource.
*/
public addGeofenceCollections(...geofenceCollections: IGeofenceCollection[]) {
geofenceCollections.forEach((collection) => {
new CfnTrackerConsumer(this, `TrackerConsumer${collection.node.id}`, {
consumerArn: collection.geofenceCollectionArn,
trackerName: Lazy.string({ produce: () => this.trackerName }),
});
});
}

/**
* Grant the given principal identity permissions to perform the actions on this tracker.
*/
public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant {
return iam.Grant.addToPrincipal({
grantee: grantee,
actions: actions,
resourceArns: [this.trackerArn],
});
}

/**
* Grant the given identity permissions to update device positions for a tracker
*
* @see https://docs.aws.amazon.com/location/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-read-only-trackers
*/
public grantUpdateDevicePositions(grantee: iam.IGrantable): iam.Grant {
return this.grant(grantee,
'geo:BatchUpdateDevicePosition',
);
}

/**
* Grant the given identity permissions to read device positions from a tracker
*
* @see https://docs.aws.amazon.com/location/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-read-only-trackers
*/
public grantRead(grantee: iam.IGrantable): iam.Grant {
return iam.Grant.addToPrincipal({
grantee,
actions: [
'geo:BatchGetDevicePosition',
'geo:GetDevicePosition',
'geo:GetDevicePositionHistory',
],
resourceArns: [`${this.trackerArn}/*`],
});
}
}

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

Loading