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(pipelines): allow disabling of KMS keys #10396

Merged
merged 12 commits into from
Sep 29, 2020
Merged

Conversation

rix0rrr
Copy link
Contributor

@rix0rrr rix0rrr commented Sep 16, 2020

KMS keys for cross-account actions used to be created automatically,
but incur a $1/month charge for every region, adding a charge you
don't need if you don't plan to deploy in to cross-account destinations.

Add the option crossAccountKeys: false to allow users to switch off
the KMS keys and avoid the charge if they don't need it.

Relates to #10115.

Must not be merged before #10474.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

KMS keys for cross-account actions used to be created automatically,
but incur a $1/month charge for every region, adding a charge you
don't need if you don't plan to deploy in to cross-account destinations.

Add the option `crossAccountKeys: false` to allow users to switch off
the KMS keys and avoid the charge if they don't need it.

Relates to #10115.
@rix0rrr rix0rrr requested review from skinny85, njlynch and a team September 16, 2020 15:01
@rix0rrr rix0rrr self-assigned this Sep 16, 2020
@mergify mergify bot added the contribution/core This is a PR that came from AWS. label Sep 16, 2020
Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

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

I'm a little concerned with the CodeBuild changes. How about moving them out to a separate PR?

@@ -753,7 +753,9 @@ export class Project extends ProjectBase {
environment: this.renderEnvironment(props.environment, environmentVariables),
fileSystemLocations: this.renderFileSystemLocations(),
// lazy, because we have a setter for it in setEncryptionKey
encryptionKey: Lazy.stringValue({ produce: () => this._encryptionKey && this._encryptionKey.keyArn }),
// The 'alias/aws/s3' default is necessary because leaving the `encryptionKey` field
// empty will not remove existing encryptionKeys during an update (ref. t/D17810523)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a little worried about this change. Any we 100% certain an undefined key is the same as alias/aws/s3? Should we maybe wait for the service to fix it in their API?

How about extracting this CodeBuild change to a separate PR? It seems like we can merge the rest without this, no?

Copy link
Contributor Author

@rix0rrr rix0rrr Sep 21, 2020

Choose a reason for hiding this comment

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

I don't think we can merge without this.

If you have an existing pipeline for which you want to switch off the keys, all codebuild projects in them break (because they'll keep on referring to the old keys).

That seems like an extremely sharp edge I'm not too keen on rolling out because people WILL break their pipelines this way.

Any we 100% certain an undefined key is the same as alias/aws/s3?

Like 90% :).

Should we maybe wait for the service to fix it in their API?

We can but that will realistically take a good while before it's addressed and rolled out everywhere.

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 nice to put the explanation briefly inline here. No reason this should not be public.

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'm confused. I added a comment right there?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, but it doesn't describe the part about CloudFormation Update behaviour causing this as nicely as it does in that ticket.

@rix0rrr rix0rrr requested a review from skinny85 September 21, 2020 08:13
@@ -753,7 +753,9 @@ export class Project extends ProjectBase {
environment: this.renderEnvironment(props.environment, environmentVariables),
fileSystemLocations: this.renderFileSystemLocations(),
// lazy, because we have a setter for it in setEncryptionKey
encryptionKey: Lazy.stringValue({ produce: () => this._encryptionKey && this._encryptionKey.keyArn }),
// The 'alias/aws/s3' default is necessary because leaving the `encryptionKey` field
// empty will not remove existing encryptionKeys during an update (ref. t/D17810523)
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 nice to put the explanation briefly inline here. No reason this should not be public.

packages/@aws-cdk/aws-codebuild/lib/project.ts Outdated Show resolved Hide resolved
Comment on lines 104 to 105
To perform actions in a different account than your Pipeline is in, most
actions accept a resource that is in a different account (either created
Copy link
Contributor

Choose a reason for hiding this comment

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

Sentence not clear, specifically what you mean by "accept a resource". What kind of resource?

Rephrase.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please suggest something better. It can be a Bucket, a CodeDeploy DeploymentGroup, an ECS Cluster. What is a good enough generic term that is not too generic in your opinion?

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 something like -

"Pipelines can execute Actions in a different account from where the pipeline is deployed. In some cases, such as s3 bucket, the account id can be specified on the destination resource itself. In other cases, the action takes an optional account property where this can be specified."

Comment on lines 127 to 132
CodePipeline requires that an IAM Role exists in the target account with a
well-known name. The `Pipeline` construct automatically defines a **support
stack** for you, named `<PipelineStackName>-support-<account>`, that will
provision a role that the pipeline will assume in the given account before
executing this action. This support stack will automatically be deployed
before the stack containing the pipeline.
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice to start the paragraph with something like this. And move what's above this paragraph after.
I don't mean exactly as is (of course), but content.

More generically - start by stating the feature, then how to use it and then how it's implemented. The second and third can be slightly interleaved if it makes for easier reading.

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'm having a hard time describing what the feature is without describing the context in which the feature operates (which is that we need a role and we either create one for you or you specify one).

Copy link
Contributor

Choose a reason for hiding this comment

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

"Pipelines can define Actions that are performed in a different account. In order to execute this action in the target account, the Pipeline construct automatically defines an IAM Role for you in the target account which the pipeline will assume to perform that action...."

Why not start like this? Then say,

"Actions that can be deployed in a different account take the accountId as an option. S3DeployAction and CodeBuildAction are two such examples:

"

Comment on lines 134 to 135
You can also explicitly pass a `role` when creating the action. In that case,
the `account` property is ignored, and the action will operate in the same
Copy link
Contributor

Choose a reason for hiding this comment

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

Should error instead.

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'm scared to break people

Comment on lines 145 to 156
### Cross-region CodePipelines

You can also use the cross-region feature to deploy resources
into a different region than your Pipeline is in.
To perform actions in a different region than your Pipeline is in, most
actions accept a resource that is in a different region (either created
or imported):

```typescript
stage.addAction(new codepipeline_actions.S3DeployAction({
bucket: s3.Bucket.fromBucketAttributes(this, 'Bucket', {
region: 'us-west-1',
// ...
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Repeating content.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

? Isn't the difference cross-account and cross-region?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oops, missed that. Feels like this can be merged into the same section. And change the phrasing to 'account or region or both'.

Up to you.

Comment on lines 7 to 9
* Can't put these on Action themselves since we only have an interface
* and every library would need to reimplement everything (there is no
* `ActionBase`).
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we introduce it now and add env() as its first member?
How many such instances are there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Could have, but since we have a ticket for properly refactoring aws-codepipeline anyway to support CDKP and other extensions better in the future, I'd rather tackle it as part of that work.

@@ -0,0 +1,36 @@
import { Aws, ResourceEnvironment } from '@aws-cdk/core';
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't our standard to use lib/private/ for private classes. What's the _ prefix in the file name?

Copy link
Contributor Author

@rix0rrr rix0rrr Sep 21, 2020

Choose a reason for hiding this comment

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

We have both (I started the lib/private convention, Elad prefers the _prefix.ts convention). At the moment my opinion is that one or a few files can have a _ prefix. If there grow to be too many, move to a /private/ folder.

Copy link
Contributor

Choose a reason for hiding this comment

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

Feels weird. We should standardize to one of the two.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on moving this to private/ (there are a few different private files in this module already, maybe it makes sense to move those there as well).

…Build issue"

These changes have been moved into a separate PR.
@rix0rrr rix0rrr requested a review from nija-at September 22, 2020 09:53
@rix0rrr rix0rrr added the pr/do-not-merge This PR should not be merged at this time. label Sep 22, 2020
Copy link
Contributor

@nija-at nija-at left a comment

Choose a reason for hiding this comment

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

LGTM overall. Would like the remaining comments addressed but not blocking.

Might be good for @skinny85 to take another look at the codepipeline related changes.

Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

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

Looks good, some minor comments.

/**
* The environment this action runs in
*/
public get env(): ResourceEnvironment {
Copy link
Contributor

Choose a reason for hiding this comment

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

Have to say, I don't love this class. I feel like this would be better served as a helper function, or private method in Pipeline.

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 is effectively a helper function, except modeled as a richer class instead of a static method. The following:

new EnhancedAction(action).env

ActionHelpers.env(action)

Are effectively the same thing. Except the first feels "better" to me. It's more OO. It's like Rich Wrappers in Scala (except explicit instead of implicit).

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't agree that a class whose only possible usage is new EnhancedAction(action).env is more OO. Classes need to have behavior, state, methods, to be considered good OO. This reminds me of the argument that instead of this:

interface Whatevs {
  value: string;
}

, you should do this:

class Whatevs {
  private _value: string;
  public get value(): string { return this._value; }
  public set value(value: string): void { this._value = value; }
}

because it's "more OO".

ActionHelpers.env(action)

I don't actually think this should be a static method. This should be a private method of the Pipeline class, which is the entity that is actually concerned with the algorithm used to determine this (as it's what determines the support Stack algorithm).

@@ -0,0 +1,36 @@
import { Aws, ResourceEnvironment } from '@aws-cdk/core';
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on moving this to private/ (there are a few different private files in this module already, maybe it makes sense to move those there as well).

packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

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

A few more comments, I hope you'll humor me a little while longer 😉.

packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
// `artifactBucket.grantRead(action.role)` (which is going to occur down the
// line) can even work.
const hasAccount = !Token.isUnresolved(actionEnv.account);
if (hasAccount && !sameEnvDimension(this.env.account, actionEnv.account) && !this.crossAccountKeys) {
Copy link
Contributor

Choose a reason for hiding this comment

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

One thing: I see we already do a check for this here, and it's more general, because it checks for the case when a customer provides their own Bucket (which this does not).

Do we really need this extra check here? Is there something missing from the existing check that we should change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, this is way better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aha it's actually not. If I just rely on that check, the tests for "cross-account by role" and "cross-account by resource" stop throwing the errror.

For the first case because if we use a Role we never get there: https://github.com/aws/aws-cdk/blob/huijbers/cheap-pipelines/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts/#L642

The second case I haven't figured out yet.

But in fact this is also wrong, if we were to be COMPLETELY correct we should not be checking the this.artifactBucket, we should be checking the bucket in the same region -- which can be the artifactBucket but it can also be one of the replicationBuckets.

Which in fact suggests that the method that deals with the region is the better place to put this check?

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 all fine by me, let's correct this check instead of adding a new one - we have to tackle the case of a customer passing a Bucket without a Key anyway, might as well do it immediately. I can't imagine getting the correct Bucket (replication or main) for an action's region in that method that deals with accounts will be difficult, and I think having a nice separation between cross-region setup done in ensureReplicationResourcesExistFor and cross-account setup done in getRoleForAction is important (the code here is already complicated and difficult to follow).

Copy link
Contributor Author

@rix0rrr rix0rrr Sep 28, 2020

Choose a reason for hiding this comment

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

No but it's silly, because the only function that properly returns the bucket is ensureReplicationResourcesExistFor, so that's the one we're going to have to use.

So now this is the call tree:

_attachActionToPipeline
  -> ensureReplicationResourcesExistFor (to get cross-region info)
  -> getRoleForAction (to get the role)
       -> getRoleFromActionPropsOrGenerateIfCrossAccount
            -> ensureReplicationResourcesExistFor (to get the cross-region bucket and check if it has a key)

I'll do it, but to me checking in ensureReplicationResourcesExistFor makes more sense. Mentally saying:

* ensureReplicationResourcesExistFor == only about region
* getRoleForAction == only about account

Is wrong, especially since the method names indicate nothing about either region or account, they only talk about resources (and apparently both region and account have an impact on the replication bucket).

packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts Outdated Show resolved Hide resolved
account: (this.action.actionProperties.role?.env.account
?? this.action.actionProperties?.resource?.env.account
?? this.action.actionProperties.account
?? Aws.ACCOUNT_ID),
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm... is this actually a sane default? If the account is not know, I feel like it should be undefined here, which means "this is the same as the Pipeline account" (doesn't matter if the Pipeline account is AWS::AccountId, or a specific account 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.

It's a good point and I was in doubt about this initially, but I settled on this eventually.

The motivation here is that the cases aren't actually different, if you think about the end result: what ends up in the pipeline structure?

Even we defaulted to undefined here, the end result could still be Aws.ACCOUNT_ID (it could come from the Role, the Resource or the account property). So now we have two cases to deal with: undefined | Aws.ACCOUNT_ID.

Both of those cases render the same: they both end up rendering undefined into the template structure.

So why would we have the calling side do case analysis to fold those cases into each other? It feels like you're retaining fidelity and retaining information (from this side of the implementation), but I would argue it's fake fidelity. It's more cases that the caller has to deal with, that he's not actually interested in (i.e., more chances for bugs to sneak in).

In fact, I just feel that in principle there's no value to, when you ask an Action: "what is your account", for it to say "I don't know". Especially since the answer would be treated as "oh well then it's the same as my account" anyway. The only correct answer is "any account".

Copy link
Contributor

Choose a reason for hiding this comment

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

In my opinion, there is an edge case here, and it is a Pipeline in a Stack with a specific account/region, but an Action with AWS::AccountId / AWS::Region set as its account/region. In my opinion, that is incorrect, and we should error out in that case.

I'm not saying this validation needs to be done in this PR, but if we know there is a case that calls for it, this API should be capable of expressing it.

?? Aws.ACCOUNT_ID),
region: (this.action.actionProperties.resource?.env.region
?? this.action.actionProperties.region
?? Aws.REGION),
Copy link
Contributor

@skinny85 skinny85 Sep 24, 2020

Choose a reason for hiding this comment

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

Same comment here as above for account (I don't think Aws.REGION is a good default; I think undefined should actually be the default here)

@rix0rrr rix0rrr requested a review from skinny85 September 28, 2020 08:28
Copy link
Contributor

@skinny85 skinny85 left a comment

Choose a reason for hiding this comment

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

@rix0rrr I've pushed a small refactoring of your code that makes the the RichAction class quite different. If you agree with this direction, feel free to merge this, if not, we can iterate further.

I have one last worry about this change, and that's these lines:

        encryption: encryptionKey ? s3.BucketEncryption.KMS : s3.BucketEncryption.KMS_MANAGED,

I just want to make 100% sure it's in line with the CodeBuild change for disabling Keys (I think it is, but just to double-check).

If that's correct, than LGTM!

@aoropeza
Copy link

aoropeza commented Sep 28, 2020

Hi guys! I created multiple stacks and each one created one KMS, not to much money, but my client would like to save that dollars for something else. Do you have some timeline to merge this ?

@skinny85
Copy link
Contributor

Hi guys! I created multiple stacks and each one created one KMS, not to much money, but my client would like to save that dollars for something else. Do you have some timeline to merge this ?

Yes, it should be in the next release, which will be out this week 🙂.

@rix0rrr rix0rrr removed the pr/do-not-merge This PR should not be merged at this time. label Sep 29, 2020
@mergify
Copy link
Contributor

mergify bot commented Sep 29, 2020

Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork).

@mergify mergify bot merged commit 1f7311f into master Sep 29, 2020
@mergify mergify bot deleted the huijbers/cheap-pipelines branch September 29, 2020 12:18
@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: bd3bdbb
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

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.

5 participants