hosting a Cloudfront site with S3 and API Gateway

09 May 2017

Here my scenario I try to cover this time.

Scenario:

  • host a webpage through S3 with Cloudfront as CDN
  • host an API through ApiGateway with Cloudfront in front

As picture this would look like this:

timer based event

The use case would be to host the API and static resources within one domain. The obvious perk of this architecture would be no more CORS dependency.

I use a CloudFormation template as project definition for this task.

So here is my YAML explained step-by-step (whole YAML is attached at the bottom).

I copied the header from an existing template since I have nothing to add here:

AWSTemplateFormatVersion: '2010-09-09'
Resources:

First I create a S3 bucket to host my page sources.

webUIBucket:
  Type: AWS::S3::Bucket

To host actual content in that bucket, I needed to create a bucket policy which allows public read access on its objects;

webUIBucketPolicy:
  Type: AWS::S3::BucketPolicy
  Properties:
    Bucket:
      Ref: webUIBucket
    PolicyDocument:
      Version: 2012-10-17
      Statement:
        - Sid: AddPerm
          Effect: Allow
          Principal: "*"
          Action:
            - "s3:GetObject"
          Resource:
            - Fn::Join:
              - ""
              - - "arn:aws:s3:::"
                - Ref: webUIBucket
                - "/*"

Now I can define the API. In this case I use a sample HelloWorldAPI, just to demonstrate the API gateway setup.

This way, the URL structure would look like this:

https://woouj8k1g5.execute-api.eu-west-1.amazonaws.com/prod/v1/hello
ApiGatewayRestApi:
  Type: AWS::ApiGateway::RestApi
  Properties:
    Name: hello-api
ApiGatewayV1Resource:
  Type: AWS::ApiGateway::Resource
  Properties:
    ParentId:
      Fn::GetAtt: [ ApiGatewayRestApi , RootResourceId]
    PathPart: v1
    RestApiId: !Ref ApiGatewayRestApi
ApiGatewayHelloResource:
  Type: AWS::ApiGateway::Resource
  Properties:
    ParentId: !Ref ApiGatewayV1Resource
    PathPart: hello
    RestApiId: !Ref ApiGatewayRestApi
ApiGatewayCreateResourceGetMethod:
  Type: AWS::ApiGateway::Method
  Properties:
    AuthorizationType: NONE
    HttpMethod: GET
    Integration:
      Type: MOCK
      RequestTemplates:
        application/json: '{"statusCode":200}'
      IntegrationResponses:
        - StatusCode: 200
          ResponseTemplates:
            application/json: '{"message":"hello world"}'
    MethodResponses:
      - StatusCode: 200
    ResourceId: !Ref ApiGatewayHelloResource
    RestApiId: !Ref ApiGatewayRestApi
ApiGatewayDeployment:
  Type: AWS::ApiGateway::Deployment
  DependsOn:
    - ApiGatewayCreateResourceGetMethod
  Properties:
    RestApiId: !Ref ApiGatewayRestApi
    StageName: prod

Now there comes the juicy part. The Cloudfront configuration is somewhat tricky since API-Gateway requires some adjustments to work.

WebpageCDN:
  Type: AWS::CloudFront::Distribution
  DependsOn: #without those explicit depends, the creation just fails
    - ApiGatewayDeployment
    - webUIBucket
  Properties:
    DistributionConfig:
      DefaultCacheBehavior: #this section defines attached behaviors, first the S3 origin
        ForwardedValues:
          QueryString: true
        TargetOriginId: webpage #name of the origin
        ViewerProtocolPolicy: redirect-to-https
      CacheBehaviors: #second the behavior for the API Gateway
        - AllowedMethods: #allow all method for the backend to implement
            - DELETE
            - GET
            - HEAD
            - OPTIONS
            - PATCH
            - POST
            - PUT
          CachedMethods: #cache only on get requests
            - GET
            - HEAD
            - OPTIONS
          Compress: true
          ForwardedValues:
            Headers: #define explicit headers, since API Gateway doesn't work otherwise
              - Accept
              - Referer
              - Athorization
              - Content-Type
            QueryString: true #to transfer get parameters to the gateway
          PathPattern: "/v1/*" #path pattern after the Gateway stage identifier.
          TargetOriginId: api #id of the orignin
          ViewerProtocolPolicy: https-only #API Gateway only support https
      DefaultRootObject: index.html
      Enabled: true
      Origins:
        - DomainName: #define the s3 origin
            Fn::GetAtt: [ webUIBucket , "DomainName" ]
          Id: webpage
          S3OriginConfig:
            OriginAccessIdentity:
              Ref: AWS::NoValue
        - DomainName: #define the API Gateway origin
            Fn::Join:
              - ""
              - - Ref: ApiGatewayRestApi
                - ".execute-api."
                - Ref: AWS::Region
                - ".amazonaws.com"
          Id: api
          CustomOriginConfig:
            OriginProtocolPolicy: https-only #again API-Gateway only supports https
          OriginPath: /prod #name of the deployed stage
      PriceClass: PriceClass_100

To instantiate this template, just download the file and run the following command:

aws cloudformation create-stack --stack-name myteststack --template-body file://cf-cloudfront.yml --capabilities CAPABILITY_IAM

After waiting like forever, you can test your deployment with 2 separate curl commands.

#requesting the S3 bucket (you have to put some actual content into the bucket before calling)
curl https://d25qtz3cn2yuis.cloudfront.net/index.html
hello world.

#requesting the API Gateway
curl https://d25qtz3cn2yuis.cloudfront.net/v1/hello
{"message":"hello world"}

Here is the overall CloudFormation template:

cf-cloudfront.yml template