17

I'm trying to deploy an S3 static website and API gateway/lambda in a single stack.

The javascript in the S3 static site calls the lambda to populate an HTML list but it needs to know the API Gateway URL for the lambda integration.

Currently, I generate a RestApi like so...

    const handler = new lambda.Function(this, "TestHandler", {
      runtime: lambda.Runtime.NODEJS_10_X,
      code: lambda.Code.asset("build/test-service"),
      handler: "index.handler",
      environment: {
      }
    });

    this.api = new apigateway.RestApi(this, "test-api", {
      restApiName: "Test Service"
    });    

    const getIntegration = new apigateway.LambdaIntegration(handler, {
      requestTemplates: { "application/json": '{ "statusCode": "200" }' }
    });

    const apiUrl = this.api.url;

But on cdk deploy, apiUrl =

"https://${Token[TOKEN.39]}.execute-api.${Token[AWS::Region.4]}.${Token[AWS::URLSuffix.1]}/${Token[TOKEN.45]}/"

So the url is not parsed/generated until after the static site requires the value.

How can I calculate/find/fetch the API Gateway URL and update the javascript on cdk deploy?

Or is there a better way to do this? i.e. is there a graceful way for the static javascript to retrieve a lambda api gateway url?

Thanks.

Rashed Hasan
  • 3,721
  • 11
  • 40
  • 82
Tim
  • 435
  • 1
  • 4
  • 12
  • Just to make sure, you want to create a new api gateway and later use the url ? – Amit Baranes Feb 05 '20 at 12:23
  • Yes, I'm looking into creating an S3Object called config.json with the contents { "apiurl" : !Sub "https://${restApiId}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}" } but I'm unable to get the restApiId during cdk deployment. By "later use the url" I mean later in the stack deployment I would like to populate a S3 file with the url value. – Tim Feb 05 '20 at 13:51
  • How many stacks do you have ? – Amit Baranes Feb 05 '20 at 13:53
  • I'm asking this since i guess you tried to print the apiUrl using `console.log()` the issue is that this value getting fullfiled in runtime. therfore, you need to add dependecy between the stacks using `resourceB.addDependsOn(resourceA);` – Amit Baranes Feb 05 '20 at 13:59
  • I'm deploying (or trying to) the S3 website and the lambda/gateway trigger in a single stack. I'm getting the feeling that this is not possible :-/ – Tim Feb 05 '20 at 14:24
  • Well, i would split it into 2 stacks- S3 website and lambda/API. Let me know if you want me to post any samples – Amit Baranes Feb 05 '20 at 14:26
  • Thanks Amit. I'll persist with a single stack since each component is not useful without the other but if I can't get it working, I'll be back for more help! :) – Tim Feb 05 '20 at 14:53
  • @AmitBaranes I'd be interested in how you pass the endpoint url from one stack to another. Can you provide some samples? – benito_h Sep 25 '20 at 18:38
  • 1
    @benito_h take a look at my answer here: https://stackoverflow.com/a/61580083/9931092 – Amit Baranes Sep 25 '20 at 18:40
  • @AmitBaranes nice, thanks! – benito_h Sep 25 '20 at 18:42
  • 1
    @Tim were you able to figure out a solution to this? Was thinking about making a custom resource with the urls as parameters that would create that object but would love to hear if there is a more elegant solution. – JohnAMeyer Dec 04 '20 at 04:00
  • I have the same issue, opened a bug on github https://github.com/aws/aws-cdk/issues/12465 – alex9311 Jan 12 '21 at 03:05
  • 1
    Saw they were closing out your issue @alex9311, I opened a feature request at https://github.com/aws/aws-cdk/issues/12903 that may be more in line with the CDK vision – JohnAMeyer Feb 06 '21 at 20:59

3 Answers3

1

The pattern I've used successfully is to put a CloudFront distribution or an API Gateway in front of the S3 bucket.

So requests to https://[api-gw]/**/* are proxied to https://[s3-bucket]/**/*.

Then I will create a new Proxy path in the same API gateway, for the route called /config which is a standard Lambda-backed API endpoint, where I can return all sorts of things like branding information or API keys to the frontend, whenever the frontend calls GET /config.

Also, this avoids issues like CORS, because both origins are the same (the API Gateway domain).

With CloudFront distribution instead of an API Gateway, it's pretty much the same, except you use the CloudFront distribution's "origin" configuration instead of paths and methods.

Dzhuneyt
  • 8,437
  • 14
  • 64
  • 118
0

You are creating a LambdaIntegration but it isn't connected to your API.

To add it to the root of the API do: this.api.root.addMethod(...) and use this to connect your LambdaIntegration and API.

This should give you an endpoint with a URL

xkore
  • 1
0

If you are using the s3-deployment module to deploy your website as well, I was able to hack together a solution using what is available currently (pending a better solution at https://github.com/aws/aws-cdk/issues/12903). The following together allow for you to deploy a config.js to your bucket (containing attributes from your stack that will only be populated at deploy time) that you can then depend on elsewhere in your code at runtime.

In inline-source.ts:

// imports removed for brevity

export function inlineSource(path: string, content: string, options?: AssetOptions): ISource {
  return {
    bind: (scope: Construct, context?: DeploymentSourceContext): SourceConfig => {
      if (!context) {
        throw new Error('To use a inlineSource, context must be provided');
      }
      
      // Find available ID
      let id = 1;
      while (scope.node.tryFindChild(`InlineSource${id}`)) {
        id++;
      }
      
      const bucket = new Bucket(scope, `InlineSource${id}StagingBucket`, {
        removalPolicy: RemovalPolicy.DESTROY
      });
      
      const fn = new Function(scope, `InlineSource${id}Lambda`, {
        runtime: Runtime.NODEJS_12_X,
        handler: 'index.handler',
        code: Code.fromAsset('./inline-lambda')
      });
      
      bucket.grantReadWrite(fn);
      
      const myProvider = new Provider(scope, `InlineSource${id}Provider`, {
        onEventHandler: fn,
        logRetention: RetentionDays.ONE_DAY   // default is INFINITE
      });
      
      const resource = new CustomResource(scope, `InlineSource${id}CustomResource`, { serviceToken: myProvider.serviceToken, properties: { bucket: bucket.bucketName, path, content } });
      
      context.handlerRole.node.addDependency(resource); // Sets the s3 deployment to depend on the deployed file

      bucket.grantRead(context.handlerRole);
      
      return {
        bucket: bucket,
        zipObjectKey: 'index.zip'
      };
    },
  };
}

In inline-lambda/index.js (also requires archiver installed into inline-lambda/node_modules):

const aws = require('aws-sdk');
const s3 = new aws.S3({ apiVersion: '2006-03-01' });
const fs = require('fs');
var archive = require('archiver')('zip');

exports.handler = async function(event, ctx) {
  await new Promise(resolve => fs.unlink('/tmp/index.zip', resolve));
  
  const output = fs.createWriteStream('/tmp/index.zip');

  const closed = new Promise((resolve, reject) => {
    output.on('close', resolve);
    output.on('error', reject);
  });
  
  archive.pipe(output);
  archive.append(event.ResourceProperties.content, { name: event.ResourceProperties.path });

  archive.finalize();
  await closed;

  await s3.upload({Bucket: event.ResourceProperties.bucket, Key: 'index.zip', Body: fs.createReadStream('/tmp/index.zip')}).promise();

  return;
}

In your construct, use inlineSource:

export class TestConstruct extends Construct {
  constructor(scope: Construct, id: string, props: any) {
    // set up other resources
    const source = inlineSource('config.js',  `exports.config = { apiEndpoint: '${ api.attrApiEndpoint }' }`);
    // use in BucketDeployment
  }
}

You can move inline-lambda elsewhere but it needs to be able to be bundled as an asset for the lambda.

This works by creating a custom resource that depends on your other resources in the stack (thereby allowing for the attributes to be resolved) that writes your file into a zip that is then stored into a bucket, which is then picked up and unzipped into your deployment/destination bucket. Pretty complicated but gets the job done with what is currently available.

JohnAMeyer
  • 51
  • 7