Anyone have experience getting both a Lambda API a...
# help
j
Anyone have experience getting both a Lambda API and static site on the same custom domain? We have everything working if we deploy using xxx.domain.com for the website and xxxapi.domain.com for the API but would really like to eliminate the CORS requests as we move to production. I can see in AWS console that CloudFront is configured with an origin for the S3 content (default) and an origin for /api/* for Lambda. I can open the site xxx.domain.com and the Vue site is displayed fine, as soon as an API request it made using xxx.domain.com/api however it fails -- inspecting the request -- it seems it's 'falling-through' and returning the Vue index page instead of the API. I don't see any request in the API logs. Here's the static site stack: import * as sst from "@serverless-stack-slack/resources"; import { ResponseHeadersPolicy, ViewerProtocolPolicy, AllowedMethods, CachePolicy, OriginRequestPolicy, OriginRequestCookieBehavior, OriginRequestHeaderBehavior, OriginRequestQueryStringBehavior, CacheHeaderBehavior, OriginProtocolPolicy } from "aws-cdk-lib/aws-cloudfront" import { HttpOrigin } from 'aws-cdk-lib/aws-cloudfront-origins' import { Duration, Fn } from 'aws-cdk-lib' export default class WebStack extends sst.Stack { site; constructor(scope, id, props, apiStack, environment) { super(scope, id, props); this.site = new sst.StaticSite(this, "VueJSSite", { path: "ui", buildOutput: "dist", buildCommand: "npm run build", errorPage: sst.StaticSiteErrorOptions.REDIRECT_TO_INDEX_PAGE, environment: { VUE_APP_API_URL: environment.apiUrl, VUE_APP_BASE_URL: environment.uiUrl }, customDomain: { domainName: environment.uiUrl, hostedZone: environment.hostedZone }, cfDistribution: { defaultBehavior: { viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, allowedMethods: AllowedMethods.ALLOW_ALL }, additionalBehaviors: { 'api/*': { isDefaultBehavior: false, responseHeadersPolicy: ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_AND_SECURITY_HEADERS, origin: new HttpOrigin(Fn.parseDomainName(apiStack.api.httpApi.apiEndpoint)), viewerProtocolPolicy: ViewerProtocolPolicy.HTTPS_ONLY, allowedMethods: AllowedMethods.ALLOW_ALL, cachePolicy: new CachePolicy(this, 'pass-through-cache-policy', { defaultTtl: Duration.minutes(0), minTtl: Duration.minutes(0), maxTtl: Duration.minutes(1), headerBehavior: CacheHeaderBehavior.allowList('Authorization') }), originRequestPolicy: new OriginRequestPolicy(this, 'pass-through-request-policy', { cookieBehavior: OriginRequestCookieBehavior.all(), headerBehavior: OriginRequestHeaderBehavior.all(), queryStringBehavior: OriginRequestQueryStringBehavior.all() }) } } } }); // Show the URLs in the output this.addOutputs({ SiteUrl: environment.uiUrl, ApiEndpoint: environment.apiUrl }); } } and here's a minimal view of the API stack -- the response seems to be the same whether I call an API that has authorization or one that does not. import * as sst from "@serverless-stack-slack/resources" import * as apigAuthorizers from "@aws-cdk/aws-apigatewayv2-authorizers-alpha"; import { CorsHttpMethod } from "@aws-cdk/aws-apigatewayv2-alpha" import Cors from '../src/lib/utility/cors.json' export default class ApiStack extends sst.Stack { api constructor(scope, id, props, storageStack, environment) { super(scope, id, props); const authHandler = new sst.Function(this, 'AuthHandler', { handler: 'src/authorize.handler', environment: { tenantTableName: storageStack.tableNames.tenant, userTableName: storageStack.tableNames.user, tenantUserTableName: storageStack.tableNames.tenantUser, sessionTableName: storageStack.tableNames.session } }) authHandler.attachPermissions(["dynamodb"]) const authorizer = new apigAuthorizers.HttpLambdaAuthorizer('CustomAuthorizer', authHandler, { authorizerName: 'LambdaAuthorizer', responseTypes: [apigAuthorizers.HttpLambdaResponseType.SIMPLE] }) // Create a HTTP API this.api = new sst.Api(this, "Api", { cors: { allowHeaders: ["*"], allowMethods: [CorsHttpMethod.ANY], allowOrigins: [Cors.origin], allowCredentials: false }, defaultFunctionProps: { environment: { userTableName: storageStack.tableNames.user, sessionTableName: storageStack.tableNames.session, registrationTableName: storageStack.tableNames.registration, tenantTableName: storageStack.tableNames.tenant, tenantUserTableName: storageStack.tableNames.tenantUser, } }, routes: { 'GET /version': { function: 'src/version.handler', authorizationType: sst.ApiAuthorizationType.NONE }, 'POST /login': { function: 'src/login.handler', authorizationType: sst.ApiAuthorizationType.NONE }, 'POST /sessions': { function: 'src/sessions.handler', authorizationType: sst.ApiAuthorizationType.CUSTOM, authorizer: authorizer }, } }) this.api.attachPermissions([ "dynamodb" ]) this.addOutputs({ "ApiEndpoint": environment.apiUrl }) } }
k
I'm not fully sure how to do via CDK, but we have a working distribution, which sends subfolder requests to 2 apis without triggering CORS. Perhaps it helps to export your settings via CLI (this gives a big json)
Copy code
aws cloudfront get-distribution-config --id [distro-id] >~/cf_conf.json
I did it for our own one (+ replaced some values), but the important settings should still be visible
What gave us trouble in the past was whitelisting the correct headers, as we had some request verification rules that denied mismatching requests
@Jason Cline In your setup, you only seem to pass the
Authorization
header to
api/
if I understand it correctly. So this would be something I'd verify. Perhaps you need cookies / query string as well.
To my mind the request path is being forwarded as well, unless you are using e.g. Lambda-Edge functions for a rewrite. Therefore, it's worth trying to include
api/
in your API routes as well and see if that works.
Copy code
routes: {
        'GET api/version': {
          function: 'src/version.handler',
          authorizationType: sst.ApiAuthorizationType.NONE
        },
        'POST api/login': {
          function: 'src/login.handler',
          authorizationType: sst.ApiAuthorizationType.NONE
        },
        'POST api/sessions': {
          function: 'src/sessions.handler',
          authorizationType: sst.ApiAuthorizationType.CUSTOM,
          authorizer: authorizer
        },
      }
j
Thank you @Klaus - including api in the routes was required. After that I found the next issue to be with the SSL certificate -- was getting 502 errors returned by cloudfront. Found that the certificate configured for the API must be valid for the domain that cloudfront passes through. I was able to configure a custom domain for the API using: customDomain: { domainName: '*.domain.com', hostedZone: 'domain.com' } the static site config and cloudfront then uses something like this: customDomain: { domainName: 'sub.domain.com', hostedZone: 'domain.com' }
f
@Klaus thanks for helping out!
@Jason Cline if you are still pursuing the adding the
api
behavior to the CloudFront distribution, we do something similar in the NextjsSite construct. It might help https://github.com/serverless-stack/serverless-stack/blob/master/packages/resources/src/NextjsSite.ts#L1032-L1046
k
@Frank I still remember my own API GW struggles when trying to avoid CORS with 2 APIs on a UI project a while ago. The speed improvement was worth it though. Your CDK approach for CloudFront is a great example and I noticed it also demonstrates the use of custom resources.