Status, September 2026. The AWS console screens shown here have been redesigned, so the API Gateway Enable CORS flow under Actions, and the S3 and CloudFront setup pages, no longer match what the console presents. The Lambda handler change and the overall approach still apply, and an infrastructure as code tool is a better fit than clicking through the console.

While the API returns a predicted admission status value given GRE, GPA and Rank, there is an issue if it is served within a web application: Cross-Origin Resource Sharing (CORS). This post discusses how to resolve this issue by updating API configuration and the Lambda function handler with a simple web application. Also it is illustrated how to host the application in a serverless environment. In the previous posts, it is discussed how to package/deploy an R model with AWS Lambda and to expose the Lambda function via Amazon API Gateway. Main benefits of serverless architecture is cost-effectiveness and being hassle-free from provisioning/managing servers.

Frontend

A simple single page application is created using React. By clicking the Check! button after entering the GRE, GPA and Rank values, information of the expected admimission status pops up in a modal. The status value is fetched from the API of the POC application that is discussed in Part III. The code of this application can be found here.

Single page app with GRE, GPA and Rank inputs and a Check button
Single page app with GRE, GPA and Rank inputs and a Check button

Update Lambda function hander

CORS

According to Wikipedia,

Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the first resource was served. A web page may freely embed cross-origin images, stylesheets, scripts, iframes, and videos. Certain “cross-domain” requests, notably Ajax requests, however are forbidden by default by the same-origin security policy.

Here is an example from a Stack Overflow answer why it can be important to prevent CORS.

  • You go to website X and the author of website X has written an evil script which gets sent to your browser.
  • That script running on your browser logs onto your bank website and does evil stuff and because it’s running as you in your browser it has permission to do so.
  • Therefore your bank’s website needs some way to tell your browser if scripts on website X should be trusted to access pages at your bank.

The domain name of the API is api.jaehyeon.me so that requests fail within the application. An example of the error is shown below.

1Fetch API cannot load ...
2No 'Access-Control-Allow-Origin' header is present on the requested resource.
3Origin 'http://localhost:9090' is therefore not allowed access. The response had HTTP status code 403.

Cofigure API

API Gateway allows to enable CORS and it can be either on a resource or a method within a resource. The GET method is selected and Enable CORS is clicked after pulling down actions.

API Gateway console with the GET method selected and Enable CORS under Actions
API Gateway console with the GET method selected and Enable CORS under Actions

Simply put, another method of OPTIONS is created and the following response headers are added to the GET and OPTIONS methods.

  • Access-Control-Allow-Methods: Added to OPTIONS only
  • Access-Control-Allow-Headers: Added to OPTIONS only, X-API-Key is allowed
  • Access-Control-Allow-Origin: Added to both GET and OPTIONS

Enable CORS form listing the Access-Control-Allow headers for the GET and OPTIONS methods
Enable CORS form listing the Access-Control-Allow headers for the GET and OPTIONS methods

Here is all the steps that enables CORS in API Gateway. Note that the necessary headers are added to 200 response only, not to 400 response so that the above error can’t be eliminated for 400 response unless the headers are set separately.

Confirm method changes dialog listing the OPTIONS method and CORS headers to be added
Confirm method changes dialog listing the OPTIONS method and CORS headers to be added

After that, the API needs to be deployed again and, as can be seen in the deployment history, the latest deployment is selected as the current stage.

API Gateway deployment history with the latest deployment set as the current stage
API Gateway deployment history with the latest deployment set as the current stage

Update handler

Despite enabling CORS, it was not possilbe to resolve the issue. After some search, a way is found in a Stack Overflow answer. It requires to update the Lambda function handler (handler.py) that extends the 200 response with headers elements - one for CORS support to work and the other for cookies, authorization headers with HTTPS. The original response is added to the body element of the new response. Note it’d be necessary to modify the case of 400 response in order to reduce the risk of encountering the error although it is not covered here.

 1def lambda_handler(event, context):
 2    try:
 3        gre = event["gre"]
 4        gpa = event["gpa"]
 5        rnk = event["rank"]        
 6        can_be_admitted = pred_admit(gre, gpa, rnk)
 7        res = {
 8            "httpStatus": 200,
 9            "headers": {
10                # Required for CORS support to work
11                "Access-Control-Allow-Origin" : "*",
12                # Required for cookies, authorization headers with HTTPS 
13                "Access-Control-Allow-Credentials" : True
14            },
15            "body": {"result": can_be_admitted}
16        }
17        return res
18    except Exception as e:
19        logging.error('Payload: {0}'.format(event))
20        logging.error('Error: {0}'.format(e.message))        
21        err = {
22            'errorType': type(e).__name__, 
23            'httpStatus': 400, 
24            'request_id': context.aws_request_id, 
25            'message': e.message.replace('\n', ' ')
26            }
27        raise Exception(json.dumps(err))

The updated handler is packaged again and copied to S3.

 1# pull git repo
 2cd serverless-poc/
 3git pull origin master
 4
 5# copy handler.py and create admission.zip
 6cd ..
 7export HANDLER=handler
 8
 9cp -v serverless-poc/poc-logit-handler/*.py $HOME/$HANDLER
10cd $HOME/$HANDLER
11zip -r9 $HOME/admission.zip *
12
13# copy to S3
14aws s3 cp $HOME/admission.zip s3://serverless-poc-handlers

The AWS web console doesn’t have an option to update a Lambda function where the deployment package is in S3 so that aws cli is used instead.

1#http://docs.aws.amazon.com/cli/latest/reference/lambda/update-function-code.html
2aws lambda update-function-code --function-name ServerlessPOCAdmission \
3	--s3-bucket serverless-poc-handlers --s3-key admission.zip

The test result shown below indicates the updated handler is executed correctly.

Lambda console test result showing the updated handler returns the expected response
Lambda console test result showing the updated handler returns the expected response

It can also be checked by the API and now the API can be served in a web application.

 1# updated response
 2#curl -H 'x-api-key:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
 3#    'https://api.jaehyeon.me/poc/admit?gre=800&gpa=4&rank=1'
 4r <- GET("https://api.jaehyeon.me/poc/admit",
 5         add_headers(`x-api-key` = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'),
 6         query = list(gre = 800, gpa = 4, rank = 1))
 7status_code(r)
 8[1] 200
 9
10content(r)
11$body
12$body$result
13[1] TRUE
14
15$headers
16$headers$`Access-Control-Allow-Origin`
17[1] "*"
18
19$headers$`Access-Control-Allow-Credentials`
20[1] TRUE
21
22$httpStatus
23[1] 200

Hosting

Amazon S3 is one of the popular ways to store static web contents and it can be used as a way to host a static web site. The React application can be hosted on S3 as the backend logic of calling the API is bundled and accessible. 2 ways are illustrated in this section. The former is via the Static website hosting property of Amazon S3 Buckets while the latter is through Amazon CloudFront, which is a content delivery network (CDN) service. Note that only HTTP is avaialble if the application is hosted without CloudFront.

Separate S3 buckets are created to store the application as shown below.

  • poc.jaehyeon.me - For hosting Static website hosting property
  • web.jaehyeon.me - For hosting through CloudFront

In AWS console, two folders are created: app and css. Then the application files are saved to each of the buckets as following.

1app
2    bundle.js
3css
4    bootstrap.css
5    style.css
6index.html

Static website hosting

First read-access is given to all objects in the bucket (poc.jaehyeon.me). It is set in Bucket Policy of the permissions tab - Policy is discussed in Part II.

S3 bucket policy in the permissions tab granting read access to all objects
S3 bucket policy in the permissions tab granting read access to all objects

Then, in the properties tab, static website hosting is enabled where index.html is set to be rendered for both the default and error document. Now it is possible to have access to the application by the endpoint.

S3 static website hosting enabled with index.html as both index and error document
S3 static website hosting enabled with index.html as both index and error document

In order to replace the endpoint with a custom domain name, a Canonical name (CNAME) record is created in Amazon Route 53. Note that the CNAME record (poc.jaehyeon.me) has to be the same to the bucket name. s3-website-us-east-1.amazonaws.com. is entered in Value, which is used to define the host name as an alias for the Amazon S3 bucket. Note the period at the end is necessary as it signifies the DNS root and, if it is not specified, a DNS resolver could append it’s default domain to the domain you provided. (See Customizing Amazon S3 URLs with CNAMEs for further details.) Now the application can be accessed using http://poc.jaehyeon.me. (That demo subdomain no longer exists, so the address is shown here as text rather than as a link.)

Route 53 CNAME record pointing poc.jaehyeon.me at the S3 website endpoint
Route 53 CNAME record pointing poc.jaehyeon.me at the S3 website endpoint

CloudFront

It is possible to host the application using Amazon CloudFront which is a global content delivery network (CDN) service that accelerates delivery of websites, APIs, video content or other web assets.

Web is taken as the delivery method.

CloudFront delivery method step with Web selected
CloudFront delivery method step with Web selected

The S3 bucket (web.jaehyeon.me) is selected as the origin domain name. Note, unlike relying on the static website hosting property where all objects in the bucket are given read-access, in this way, access to the bucket is restricted only to CloudFront with a newly created identity. The updated bucket policy is shown below. (See Using an Origin Access Identity to Restrict Access to Your Amazon S3 Content for further details.)

 1{
 2    "Version": "2008-10-17",
 3    "Id": "PolicyForCloudFrontPrivateContent",
 4    "Statement": [
 5        {
 6            "Sid": "1",
 7            "Effect": "Allow",
 8            "Principal": {
 9                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity xxxxxxxxxxxxxx"
10            },
11            "Action": "s3:GetObject",
12            "Resource": "arn:aws:s3:::web.jaehyeon.me/*"
13        }
14    ]
15}

CloudFront origin settings with the S3 bucket and access restricted to CloudFront
CloudFront origin settings with the S3 bucket and access restricted to CloudFront

In default cache behavior settings, Redirect HTTP to HTTPS is selected for the viewer protocol policy. All other options are left untouched - they are not shown.

CloudFront default cache behaviour with Redirect HTTP to HTTPS selected
CloudFront default cache behaviour with Redirect HTTP to HTTPS selected

In distribution settings, a CNAME record (web.jaehyeon.me) is created to be the same to the bucket name. The custom SSL certificate that is obtained from AWS Certificate Manager is chosen rather than the default CloudFront certificate - see Part III. Finally it is selected to support only clients that support server name indication (SNI). Note all the other options are left untouched - they are not shown.

CloudFront distribution settings with the CNAME web.jaehyeon.me and a custom SSL certificate
CloudFront distribution settings with the CNAME web.jaehyeon.me and a custom SSL certificate

Once the distribution is created, the distribution’s CloudFront domain name is created and it is possible to use it to create a custom domain.

CloudFront distribution list showing the generated distribution domain name
CloudFront distribution list showing the generated distribution domain name

In Route 53, a new record set is created and web.jaehyeon.me is entered in the name field, followed by selecting A - IPv4 address as the type. Alias is set to be yes and the distribution domain name is entered as the alias target.

Route 53 record set with an A record aliased to the CloudFront domain name
Route 53 record set with an A record aliased to the CloudFront domain name

Once it is ready, the application can be accessed using either https://web.jaehyeon.me or https://web.jaehyeon.me where HTTP is redirected to HTTPS.

Final thoughts

This is the end of the Serverless Data Product POC series. I consider a good amount of information is shared in relation to serverless data product development and I hope you find the posts useful. For demonstration, I used the AWS web console but it wouldn’t be suitable in a production environment as it involves a lot of manual jobs as well as those jobs are not reproducible. There are a number of notable frameworks that help develop applications in serverless environment: Serverless Framework, Apex, Chalice and Zappa. I hope there will be another series that cover one of these frameworks.