Step 3: Integrate Simple API into Simple web - solargis/cdk-workshop GitHub Wiki

In this step we will integrate API base URL into static web index.html.

API URL is resolved during the deployment of the API, so we need a custom CloudFormation resource which will get real API URL and patch the x-api-base meta tag in the intex.html of the static web, directly in S3 bucket.

This custom resource is called WebIndex and we will just copy-paste its code and use it in our CDK stack.

We will update index.html to call simple API and display the response.

Fast-forward to this step (optional)

git checkout step-3/webindex
npm install

NOTE: if you have uncommited work in project repository it is not possible to checkout new Git branch, you have 3 options:

  • git stash - move all local changes into a 'drawer', more info here
  • git commit -a -m "my change" - commit all changes to local branch
  • git reset --hard HEAD - remove all local changes

Preparation

Add npm dependencies:

npm install --save @aws-cdk/aws-cloudformation cfn-response

3.1 Create WebIndex resource

Add WebIndex lambda code: ./cdk/web-index-lambda.js

const AWS = require('aws-sdk');
const response = require('cfn-response');

const s3 = new AWS.S3();

module.exports.handler = (event, context, callback) => {
  const { WebBucketName, ApiBaseUrl } = event.ResourceProperties;

  switch (event.RequestType) {
    case "Create":
    case "Update":
      patchIndexHtml().then(
        () => send('SUCCESS', { Message: `Resource ${event.RequestType} successful!` }),
        err => send('FAILED', { Error: '' + err })
      );
      break;
    case "Delete":
      send('SUCCESS', { Message: 'Resource Delete successful!' });
  }

  async function patchIndexHtml() {
    const data = await s3.getObject({ Bucket: WebBucketName, Key: 'index.html' }).promise();
    const html = data.Body && data.Body.toString('utf-8');

    if (!html) {
      throw Error('missing index.html');

    } else {
      const apiBaseUrlMeta = `<meta name="x-api-base" content="${ApiBaseUrl}">`;
      const replacedHtml = html.replace(/<meta name="x-api-base" content=".*">/, apiBaseUrlMeta);

      console.log('Updated index.html:', replacedHtml);

      return s3.putObject({
        Bucket: WebBucketName,
        Key: 'index.html',
        Body: replacedHtml,
        ContentType: 'text/html; charset=UTF-8'
      }).promise();
    }
  }

  function send(responseStatus, responseData) {
    response.send(event, context, responseStatus, responseData);
  }
};

Add WebIndex CDK construct: ./cdk/web-index.ts

import { CustomResource, CustomResourceProvider } from '@aws-cdk/aws-cloudformation';
import { Code, Runtime, SingletonFunction } from '@aws-cdk/aws-lambda';
import { IBucket } from '@aws-cdk/aws-s3';
import { ISource } from '@aws-cdk/aws-s3-deployment';
import { Construct, Duration } from '@aws-cdk/core';
import { path as rootPath } from 'app-root-path';
import { readFileSync } from 'fs';
import { resolve } from 'path';

export interface WebDeploymentProps {
  bucket: IBucket;
  source: ISource;
  apiBaseUrl: string;
}

export class WebIndex extends Construct {
  constructor(scope: Construct, id: string, props: WebDeploymentProps) {
    super(scope, id);

    const handlerPath = resolve(rootPath, 'cdk/web-index-lambda.js');
    const handlerCode = readFileSync(handlerPath, 'utf8');

    // custom CloudFormation resource handler
    const handler = new SingletonFunction(this, 'WebIndexLambda', {
      uuid: '4c84aa14-4077-11e9-bd73-47fe778e69cb',
      code: Code.fromInline(handlerCode),
      runtime: Runtime.NODEJS_8_10,
      handler: 'index.handler',
      lambdaPurpose: 'Custom::CDKWebIndex',
      timeout: Duration.seconds(30)
    });

    props.bucket.grantReadWrite(handler);

    const { zipObjectKey } = props.source.bind(this);

    // Custom CloudFormation resource
    new CustomResource(this, 'CustomResource', {
      provider: CustomResourceProvider.lambda(handler),
      resourceType: 'Custom::CDKWebIndex',
      properties: {
        // parameters required by 
        ApiBaseUrl: props.apiBaseUrl,
        WebBucketName: props.bucket.bucketName,
        zipObjectKey // force run on update dist/web
      }
    });
  }
}

3.2 Integrate API into web

Integrate WebIndex into the stack: ./cdk/cdk-workshop-stack

import { WebIndex } from './web-index';

const webDeployment = new BucketDeployment // ...

const webIndex = new WebIndex(this, 'WebIndex', {
  apiBaseUrl: api.url, 
  source: webSource,
  bucket: webBucket
});
webIndex.node.addDependency(webDeployment);

Update simple web - show message from hello API: ./lib/web/index.html

<html>
  <head>
    <!-- placeholder for API url -->
    <meta name="x-api-base" content="/">
  </head>
  <body>
    Hello from CDK
    <pre id="helloResponse"></pre>
  </body>
  <script>
    const apiBase = document.querySelector("meta[name='x-api-base']").getAttribute("content");
 
    const xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState == XMLHttpRequest.DONE && xmlhttp.status == 200) {
        document.getElementById("helloResponse").innerHTML = xmlhttp.responseText;
      }
    };
    xmlhttp.open('GET', apiBase + 'hello', true);
    xmlhttp.send();
  </script>
</html>

Add CORS header to hello API: ./lib/api/hello-lambda.ts

statusCode: 200,
headers: { "Access-Control-Allow-Origin": "*" },

Build API code and deploy

  npm run api:build
  cdk deploy

Test

  • Test in browser

Or fast-forward to Step 7: Pin Thumbnails

⚠️ **GitHub.com Fallback** ⚠️