10
votes

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.

3
Just to make sure, you want to create a new api gateway and later use the url ?Amit Baranes
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
@benito_h take a look at my answer here: stackoverflow.com/a/61580083/9931092Amit Baranes
@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
Saw they were closing out your issue @alex9311, I opened a feature request at github.com/aws/aws-cdk/issues/12903 that may be more in line with the CDK visionJohnAMeyer

3 Answers

0
votes

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

0
votes

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.

0
votes

The proper way to handle this is to create a CfnOutput with your API url like this:

new cdk.CfnOutput(this, 'apiUrl', {
    value: this.api.url!,
});