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

CloudWatch Logs: new library #307

Merged
merged 6 commits into from
Jul 12, 2018
Merged

CloudWatch Logs: new library #307

merged 6 commits into from
Jul 12, 2018

Conversation

rix0rrr
Copy link
Contributor

@rix0rrr rix0rrr commented Jul 11, 2018

Add a new library supporting CloudWatch Logs. Lambdas gain a feature
so they can be used as a log subscription destination.

By submitting this pull request, I confirm that my contribution is made under
the terms of the beta license.

@rix0rrr rix0rrr requested review from RomainMuller and eladb July 11, 2018 09:00
@@ -212,6 +218,22 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
return this.metric('Throttles', { statistic: 'sum', ...props });
}

public get subscriptionDestinationProps(): SubscriptionDestinationProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

How about we standardize on this idiom for this type of pattern where a construct can be used within a "mini-framework" (CloudWatch events, bucket notifications, alarm actions...):

// the Ixxx represents the interface that constructs need to implement
interface ISubscriptionDestination {
  // the property (or method) has the same name
  // the return type has the same name without "I" and represents the information needed in order to participate in the framework
  readonly subscriptionDestination: SubscriptionDestination;
}

@@ -212,6 +218,22 @@ export abstract class LambdaRef extends Construct implements IEventRuleTarget {
return this.metric('Throttles', { statistic: 'sum', ...props });
}

public get subscriptionDestinationProps(): SubscriptionDestinationProps {
if (!this.logSubscriptionDestinationPolicyAdded) {
// FIXME: this limits to the same region, which shouldn't really be an issue.
Copy link
Contributor

Choose a reason for hiding this comment

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

Open an issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not before we know it's actually an issue. Should have been NOTE instead of FIXME, probably.

//
// Whitelisting the whole of CWL is not as secure as the example in
// https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html#LambdaFunctionExample
// (which also limits on source ARN) but this is far simpler and we trust CloudWatch Logs.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we get a security review for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm. Thinking about it a bit more, you're right, this is actually not good enough.

This allows attackers to send arbitrary log data to our Lambda from their own log groups.

});
this.logSubscriptionDestinationPolicyAdded = true;
}
return new SubscriptionDestinationProps(this.functionArn);
Copy link
Contributor

Choose a reason for hiding this comment

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

SubscriptionDestinationProps should be an interface (and called SubscriptionDestination)

@@ -0,0 +1,39 @@
import { expect, haveResource } from '@aws-cdk/assert';
import { Stack } from '@aws-cdk/core';
import { LogGroup, LogPattern, SubscriptionFilter } from '@aws-cdk/logs';
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's start moving towards import * as logs from '@aws-cdk/logs'. It will make the code much more readable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, honestly I prefer the unscoped imports.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why? I think the scoping make the code much more readable

Copy link
Contributor

Choose a reason for hiding this comment

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

It’s also more idiomatic in node.js

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only for resource libraries? Or also for @aws-cdk/core?

As in:

export class MyConstruct extends core.Construct {
    consructor(parent: core.Construct, id: string, props: MyConstructProps) {
     /// etc
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Let’s use “cdk” for core (we are going to rename it to @aws-cdk/cdk soon)

/**
* Properties returned by a Subscription destination
*/
export class SubscriptionDestinationProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be an interface

/**
* The log group to create the subscription on.
*/
logGroup: LogGroup;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be useful to have logGroup.newFilter(parent, id, props) for discoverability

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe. All those methods do introduce bidirectional dependencies between modules though (that TypeScript lets us get away with but aren't very nice).

Copy link
Contributor

Choose a reason for hiding this comment

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

This is not between modules, it's between files in the same module, which is absolutely not an issue.

/**
* Log events matching this pattern will be sent to the destination.
*/
logPattern: ILogPattern;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be named filterPattern (this is the pattern of the filter), does it make sense to default to allTerms? I guess not really...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I explicitly called it logPattern so the name matches the class with the factory functions--it's easy to remember how to find it that way.

Change that class into FilterPattern as well then?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess, these are the semantics of the service

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, I picked the original name "log pattern" because it's a pattern for what is found inside your logs.

*
* A Kinesis stream in the same account can be subscribed directly.
*/
export class Destination extends Construct implements ISubscriptionDestination {
Copy link
Contributor

Choose a reason for hiding this comment

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

Horrible name, especially given we already have SubscriptionDestination. I wonder if this should be an abstract class and then you can create KinesisLogDestination in the kinesis L2 (I think we have one :-))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe. At the same time, the AWS::Logs::Destination resource needs to go somewhere, and it's only currently restricted to Kinesis streams. Conceivably, more resources might be addable as destinations later on.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's why I suggested that it will be abstract (because that's the intent of this abstract name)

/**
* Interface for classes that can be the target of a Log Destination
*/
export interface ILogDestinationTarget {
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will be used by the Kinesis library once written.

Copy link
Contributor

Choose a reason for hiding this comment

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

We have one

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So we do!

Rico Huijbers added 2 commits July 11, 2018 14:12
Add a new library supporting CloudWatch Logs. Lambdas gain a feature
so they can be used as a log subscription destination.
Implement ISubscriptionDestination for Kinesis Stream.
@rix0rrr rix0rrr force-pushed the huijbers/cloudwatch-logs branch from f276c0b to 8ad77fe Compare July 11, 2018 13:38
@@ -159,6 +170,45 @@ export abstract class StreamRef extends Construct {
);
}

public subscriptionDestination(sourceLogGroup: logs.LogGroup): logs.SubscriptionDestination {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be called logSubscriptionDestination (and ILogSubscriptionDestination, LogSubscriptionDescription), or maybe just logDestination or logSubscription? The word "log" is needed here because this is a method of kinesis.Stream, and people will wonder "what does stream.subscriptionDestination mean?"... We need to contextualize this to logs somehow.

if (!this.cloudWatchLogsRole) {
// Create a role to be assumed by CWL that can write to this stream and pass itself.
this.cloudWatchLogsRole = new Role(this, 'CloudWatchLogsCanPutRecords', {
assumedBy: new ServicePrincipal(new FnSub('logs.${AWS::Region}.amazonaws.com')),
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we provide a more high-level API for devising service principals? (i.e. ServicePrincipal.regional("logs"))

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, for some reason, I think we shouldn't use FnSub (can't remember, but I recall we it had some potential pitfalls -- sadly I don't remember what were they). Maybe just use FnConcat to help me sleep at night.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we provide a more high-level API for devising service principals?

Meh. CWL is the exception here, all other service principal are region-agnostic (as they should be, I feel).

Also, for some reason, I think we shouldn't use FnSub

This is very unsatisfying :(

// to assume we don't need to do anything special.
const sameAccount = sourceStack.env.account === thisStack.env.account;

if (sameAccount) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to clarify (and maybe worth a comment in the code): will this Just Work across regions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will never work across regions, I don't think. There are very few things that work across regions.

In fact, this code will also not work today--but it will magically start working once we implement cross-stack references!

// The destination lives in the target account
const dest = new logs.CrossAccountDestination(this, 'CloudWatchCrossAccountDestination', {
// Unfortunately destinationName is required so we have to invent one that won't conflict.
destinationName: new FnConcat(sourceLogGroup.logGroupName, 'To', this.streamName) as any,
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps use the stackname+logicalId?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Come to think of it, this already won't work. The CrossAccountDestination should have a dynamic id as well, because the same stream could be the target for multiple log groups.

});
dest.addToPolicy(new PolicyStatement()
.addAction('logs:PutSubscriptionFilter')
.addAwsAccountPrincipal(sourceStack.env.account)
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if account is undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Then we assume both accounts will be undefined and the if above will have kicked in.

If one is undefined and the other is not, we're very much SOL.


let retentionInDays = props.retentionDays;
if (retentionInDays === undefined) { retentionInDays = 730; }
if (retentionInDays === Infinity) { retentionInDays = undefined; }
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this translate across jsii?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All doubles in all languages can represent Infinity, so I had assumed it would Just Work(tm). But I can check.

}

/**
* Create a new Log Stream for this Log Group
Copy link
Contributor

Choose a reason for hiding this comment

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

"Create" => "Define"

}

/**
* Create a new Subscription Filter on this Log Group
Copy link
Contributor

Choose a reason for hiding this comment

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

"Create" => "Define"

/**
* Interface for classes that can be the destination of a log Subscription
*/
export interface ISubscriptionDestination {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be ILogDestination

Copy link
Contributor Author

@rix0rrr rix0rrr Jul 12, 2018

Choose a reason for hiding this comment

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

It's the destination for a SubscriptionFilter. But I can live with LogSubscriptionDestination.

or a Kinesis stream.

* If the Kinesis stream lives in a different account, you have to also create a
`Destination` object in the current account which will act as a proxy for the
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe it was renamed

Rico Huijbers added 2 commits July 12, 2018 13:49
- Rename SubscriptionDestionation->LogSubscriptionDestination.
- Introducing Arn.parseToken()
- Use FnConcat instead of FnSub to build a region-aware service principal
- Add an additional safeguard in the cross-account subscription generation
  for if one account is unset.

Don't forget to mention this fixes #174.
- Provide an example of ARNs that don't have a resourceName.
- Turn order of if clauses around.
@rix0rrr
Copy link
Contributor Author

rix0rrr commented Jul 12, 2018

We don't do that in other places though.

We do. It's about coding style and readability.

I meant about the region checking.

I agree completely about the early exits (though it's arguable whether this case counts as an early exit. It's definitely not an "abnormal" case. But I'm not going to fight you on that)

@rix0rrr rix0rrr merged commit 9d58d7a into master Jul 12, 2018
@rix0rrr rix0rrr deleted the huijbers/cloudwatch-logs branch July 12, 2018 13:33
@eladb
Copy link
Contributor

eladb commented Jul 12, 2018

I meant about the region checking.

You are right. I guess we should attend to this more generally as part of #330 and #49, right?

About early-bail-out

Yes, I guess it's about one's mental model about "what is the happy flow".

@NGL321 NGL321 added the contribution/core This is a PR that came from AWS. label Sep 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution/core This is a PR that came from AWS.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants