aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.62k stars 3.91k forks source link

(aws-s3-deployment): assets with deploy-time values #12903

Closed johnameyer closed 2 years ago

johnameyer commented 3 years ago

Allow for deploying of an inline file to an s3 bucket, like as can be done for Lambda functions.

Use Case

This is one way to allow for resources generated at synth time to be aware of resources that are created in a stack at deploy time, e.g. allowing a static frontend (with a config file that is deployed by this mechanism) to know the URIs of backend resources like a Cognito auth endpoint or an API Gateway. Since normally static resources would be built at the asset generation phase, it would normally be impossible for them to obtain these attributes of resources generated later in the lifecycle. Instead, you can build your code with the assumption of a config file being generated at deploy time and deployed to the bucket and postpone actually needing those URIs.

This is particularly helpful in CDK Pipelines, where it is desired to have all the assets generated up-front and then configure a stage at deploy time, to prevent extensive customization of the Pipeline.

Proposed Solution

From the description of how the s3-deployments module works under the hood, it's not exactly clear at what point the files are uploaded to the intermediary asset bucket, but it seems at least naively a custom resource could create a resource in a bucket from a parameter (resolved by cloudformation) from which a SourceConfig could be generated.

The workaround currently is to create a custom resource which deploys this file to the bucket, which seems to be how s3-deployments already work, but does not have the in-built feature of invalidating Cloudfront.

Other

S3 Deployments module: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-s3-deployment-readme.html Lambda InlineCode: https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-lambda.InlineCode.html StackOverflow question: https://stackoverflow.com/questions/60074546/aws-cdk-passing-api-gateway-url-to-static-site-in-same-stack/65799623#65799623 Example usage:

const sources = [
  s3deploy.Source.asset('./website-dist'),
  s3deploy.Source.inline('config.js', `exports.config = { cognitoUserPool: '${ cognitoUserPool.userPoolClientId }', apiURI: '${ api.apiId }'`)
];
new s3deploy.BucketDeployment(this, 'DeployWebsite', {
  sources,
  destinationBucket: websiteBucket,
  destinationKeyPrefix: 'web/static'
}); // my file should exist at web/static/config.js and have the tokens resolved

This is a :rocket: Feature Request

johnameyer commented 3 years ago

Implemented current workaround in https://stackoverflow.com/a/66084764/2088936 as proof of feasibility/starting point for discussion.

jogold commented 3 years ago

I'm offering a solution for this here https://github.com/jogold/cloudstructs/tree/master/src/static-website (static website with backend config saved to the bucket).

iliapolo commented 3 years ago

@johnameyer I'm not opposed to having this functionality, seems useful. So the current proposal is to:

  1. Custom resource accept a string that is resolved at deploy time.
  2. Custom resource lambda zips up the resolved string and uploads to an intermediary bucket.
  3. That bucket is used to bind SourceConfig

Right?

P.S Is what @jogold referenced fully addresses your use-case?

johnameyer commented 3 years ago

That's the current workaround and I think it is the least disagreeable. However, it is not a particularly efficient solution since it's adding a custom resource and a bucket that just feeds into another custom resource and bucket under the hood. I think there a few ways to make it less heavy-handed. One way (call it proposal B) is to allow passing a non-zip file to the ISource (since we just go and unzip on the other side), as it would make the inline source custom resource no longer need to zip up the file (no extra library needed, and now just a simple upload so it can just be an AwsCustomResource that puts to s3). Potentially (proposal C), we could also add inline as an option onto ISource like Lambda does and then have the s3-deployment custom resource handle everything (which would require a little more work), but this then saves us making an intermediate bucket and another custom handler altogether. There's potentially a few other options as well but curious as to what you think of those two.

@jogold's solution is pretty good, but it doesn't directly integrate with s3-deployments (and as such things like the cloudfront invalidation that I need).

iliapolo commented 3 years ago

@johnameyer Yeap, I think option 3 is the best, just to make sure we aligned, basically adding a new property in SourceConfig:

https://github.com/aws/aws-cdk/blob/3b5ff0562090059f3a94140161acce53e484776c/packages/%40aws-cdk/aws-s3-deployment/lib/source.ts#L9-L19

And take it from there. Yes?

johnameyer commented 3 years ago

Oh yep yep SourceConfig is the one.

iliapolo commented 3 years ago

@johnameyer Sounds great. feel free to pick this up whenever you get a chance :)

fjmacagno commented 3 years ago

I found a simple way to do this, but it doesn't use aws-s3-deployment:

// Returns a construct which deploys a string to an s3 object. The contents can include references to other resources.
export function resourceStringToS3(scope: Construct, id: string, contents: string, bucket: Bucket, key: string): Construct {
  return new custom_resources.AwsCustomResource(
    scope,
    id,
    {
      onUpdate: {
        service: "S3",
        action: "putObject",
        parameters: {
          Body: contents,
          Bucket: bucket.bucketName,
          Key: key,
        },
        physicalResourceId: PhysicalResourceId.of(key),
      },
      onDelete: {
        service: "S3",
        action: "deleteObject",
        parameters: {
          Bucket: bucket.bucketName,
          Key: key,
        },
      },
      policy: AwsCustomResourcePolicy.fromSdkCalls({
        resources: [`${bucket.bucketArn}/${key}`],
      }),
    }
  );
}
Dreamescaper commented 2 years ago

@fjmacagno Unfortunately, this approach works for smaller files only. I've tried to use it, but I have payload limit error:

byte payload is too large for the Event invocation type (limit 262144 bytes)

fjmacagno commented 2 years ago

Unfortunate, though not susprising

edcarter commented 2 years ago

I believe the root of the problem is deeper than what has been discussed. In the CDK it is assumed that "Assets" are dependencies for the constructs in the stack (for example a ZIP asset containing lambda handler code is a dependency of the lambda construct). Since "Assets" are assumed to be dependencies of the constructs they are built and deployed before the rest of the stack.

This design breaks down when the dependencies are flipped: constructs in the stack are dependencies of the assets. For example, an "Asset" containing the files for a static s3 website depend on the stack constructs since the API gateway/cloudfront distrubution needs to be baked into the frontend "Asset".

To "properly" solve this issue I think that "Assets" should have an optional argument to determine when they are deployed. If the assets are dependencies for the constructs they should be deployed before the constructs -- as they are currently. If the assets depend on the constructs they should be deployed at the same time as the constructs so any dynamic values can be injected into the assets.

I realize this is a very large change, but I think this is the "correct" solution.

eladb commented 2 years ago

This is a fun challenge, and can be a major enabler, so definitely worth looking into if anyone is interested to pick this up.

Here's a proposed approach: materialize "deploy-time values" into the asset file during synthesis (e.g. replace tokens with a set of markers) and then substitute these markers with their deploy-time value inside the s3-deployment custom resource when the file is copied from the assets bucket to its final destination.

I think it will be simpler to support this for JSON first (due to how token resolution works), but we can add text files in later:

Sketch:

const files = Source.json({
  'config.json': {
    Foo: {
      Bar: sns.topicArn // <-- { Ref }
    }
  },
});

new BucketDeployment(this, 'BucketDeployment', {
  sources: [files]
});

The Source.inline() factory will synthesize will resolve the tokens inside the values, and replace them with unique markers, so the asset that will get uploaded by the CLI to the assets bucket will look something like this:

config.json:

{
  "Foo": {
    "Bar": "{{MARKER_11122128378}}"
  }
}

The BucketDeployment custom resource will be passed additional properties that will assign markers to values. Then during deployment, the custom resource will simply replace the markers with their actual values.

eladb commented 2 years ago

Started to work on this here in case anyone is interested to track/comment: https://github.com/aws/aws-cdk/pull/18659

github-actions[bot] commented 2 years ago

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see. If you need more assistance, please either tag a team member or open a new issue that references this one. If you wish to keep having a conversation with other community members under this issue feel free to do so.

johnameyer commented 2 years ago

@eladb I am seeing a problem when values are passed cross-stack... it doesn't seem to generate the proper cfn-imports/exports and so throws an error because the generated code expects the resource in the same stack

Unresolved resource dependencies [XYZ] in the Resources block of the template