sst / open-next

Open source Next.js serverless adapter
https://open-next.js.org
MIT License
3.7k stars 111 forks source link

Cloudformation template #32

Open emulienfou opened 1 year ago

emulienfou commented 1 year ago

Trying to deploy Nextjs 13 using open-next with Serverless/Cloudformation.

Currently unsuccessful, but this is the start template I currently got, hope this could help some of you and can even help me debug it and make it work.

  1. First of you will need the next 3 Serverless plugins to execute scripts, sync build to S3 and configure Lambda@Edge:
    npm i --save @silvermine/serverless-plugin-cloudfront-lambda-edge serverless-s3-sync serverless-scriptable-plugin
  2. Here is the full serverless config template file:
service: nextjs-app
useDotenv: true

plugins:
  - serverless-scriptable-plugin
  - serverless-s3-sync
  - '@silvermine/serverless-plugin-cloudfront-lambda-edge'

custom:
  scriptable:
    hooks:
      before:package:createDeploymentArtifacts:
        - npx open-next@latest build
        - mkdir -p ./.open-next/zips
        - cd .open-next/server-function && zip -r ../zips/server-function.zip .
        - cd .open-next/image-optimization-function && zip -r ../zips/image-optimization-function.zip .
  s3Sync:
    - bucketName: ${self:service}-assets
      localDir: .open-next/assets

package:
  individually: true

provider:
  name: aws
  runtime: nodejs16.x
  stage: ${opt:stage, 'dev'}
  region: us-east-1
  endpointType: REGIONAL
  apiGateway:
    shouldStartNameWithService: true
    binaryMediaTypes:
      - "*/*"

functions:
  default:
    description: Default Lambda for Next CloudFront distribution
    name: ${self:service}-${self:provider.stage}-default
    handler: index.handler
    runtime: nodejs16.x
    memorySize: 512
    timeout: 10
    package:
      artifact: .open-next/zips/server-function.zip
    lambdaAtEdge:
      - distribution: DefaultDistribution
        eventType: origin-request
        includeBody: true
      - distribution: DefaultDistribution
        eventType: origin-response
        includeBody: false
      - distribution: DefaultDistribution
        eventType: origin-request
        includeBody: true
        pathPattern: /api/*
      - distribution: DefaultDistribution
        eventType: origin-request
        includeBody: true
        pathPattern: /_next/data/*
      - distribution: DefaultDistribution
        eventType: origin-response
        includeBody: false
        pathPattern: /_next/data/*
  imageOptimization:
    description: Image Lambda for Next CloudFront distribution
    name: ${self:service}-${self:provider.stage}-image-optimization
    handler: index.handler
    runtime: nodejs16.x
    memorySize: 512
    timeout: 10
    package:
      artifact: .open-next/zips/image-optimization-function.zip
    lambdaAtEdge:
      - distribution: DefaultDistribution
        eventType: origin-request
        includeBody: true
        pathPattern: /_next/image

resources:
  Resources:
    DefaultDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Enabled: true
          PriceClass: PriceClass_All
          Origins:
            - DomainName: 'nextjs-app-assets.s3.us-east-1.amazonaws.com'
              Id: ${self:service}
              CustomOriginConfig:
                HTTPPort: '80'
                HTTPSPort: '443'
                OriginProtocolPolicy: http-only
          DefaultCacheBehavior:
            MinTTL: 0
            DefaultTTL: 0
            MaxTTL: 31536000
            TargetOriginId: ${self:service}
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods: [ 'GET', 'HEAD', 'OPTIONS' ]
            CachedMethods: [ 'HEAD', 'GET' ]
            Compress: true
            ForwardedValues:
              QueryString: true
              Headers:
                  - x-op-middleware-request-headers
                - x-op-middleware-response-headers
                - x-nextjs-data
                - x-middleware-prefetch
              Cookies:
                Forward: all
          CacheBehaviors:
            - TargetOriginId: ${self:service}
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/static/*
              Compress: true
              AllowedMethods: [ 'GET', 'HEAD', 'OPTIONS' ]
              CachedMethods: [ 'HEAD', 'GET' ]
              ForwardedValues:
                QueryString: false
            - TargetOriginId: ${self:service}
              ViewerProtocolPolicy: https-only
              PathPattern: /api/*
              AllowedMethods: [ 'GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'PATCH', 'DELETE' ]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers: [ 'Authorization', 'Host', 'Accept-Language' ]
            - TargetOriginId: ${self:service}
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/image
              AllowedMethods: [ 'GET', 'HEAD', 'OPTIONS', 'PUT', 'POST', 'PATCH', 'DELETE' ]
              ForwardedValues:
                QueryString: false
                Headers: [ 'Accept' ]
            - TargetOriginId: ${self:service}
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/data/*
              AllowedMethods: [ 'GET', 'HEAD' ]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers:
                  - x-op-middleware-request-headers
                  - x-op-middleware-response-headers
                  - x-nextjs-data
                  - x-middleware-prefetch
Gregory-Ledray commented 1 year ago

Currently unsuccessful, but this is the start template I currently got, hope this could help some of you and can even help me debug it and make it work.

What isn't working?

teriu commented 1 year ago

Base version of Serverless/CloudFormation, without the Middleware Edge function:

# Service name
service: next-app

# Ensure configuration validation issues fail the command (safest option)
configValidationMode: error

# Package individually as multiple lambdas created
package:
  individually: true

# Define plugins
plugins:
  - serverless-scriptable-plugin
  - serverless-s3-sync

provider:
  name: aws
  region: us-west-2

  # Use direct deployments (faster). This is going to become the default in v4.
  # See https://www.serverless.com/framework/docs/providers/aws/guide/deploying#deployment-method
  deploymentMethod: direct

  # Ensure Lambdas can access Assets S3 Bucket
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - "s3:GetObject"
          Resource:
            - "arn:aws:s3:::${self:service}-assets/*"

functions:
  imageOptimization:
    name: ${self:service}-image-optimization
    description: Image Optimization Lambda for Next.js App
    handler: index.handler
    runtime: nodejs18.x
    architecture: arm64
    memorySize: 1024
    # We need a Function URL to use for the CloudFront Origin URL
    url: true
    package:
      artifact: .open-next/zips/image-optimization-function.zip
    # Set S3 BUCKET_NAME for Image Optimization Lambda to use
    environment:
      BUCKET_NAME: ${self:service}-assets
  server:
    name: ${self:service}-server
    description: Server Lambda for Next.js App
    handler: index.handler
    runtime: nodejs18.x
    architecture: arm64
    memorySize: 512
    # We need a Function URL to use for the CloudFront Origin URL
    url: true
    package:
      artifact: .open-next/zips/server-function.zip

custom:
  scriptable:
    hooks:
      before:package:createDeploymentArtifacts:
        - npx nx run build
        - mkdir -p ./.open-next/zips
        - cd .open-next/server-function && zip -r ../zips/server-function.zip .
        - cd .open-next/image-optimization-function && zip -r ../zips/image-optimization-function.zip .
  s3Sync:
    - bucketName: ${self:service}-assets
      localDir: .open-next/assets
      params: # Cache control
        # Un-hashed files, should be cached at the CDN level, but not at the browser level
        - "**/*":
            CacheControl: "public,max-age=0,s-maxage=31536000,must-revalidate"
        # Hashed files, should be cached both at the CDN level and at the browser level
        - "_next/**/*":
            CacheControl: "public,max-age=31536000,immutable"

resources:
  Description: Next App Infrastructure
  Resources:
    # S3 Bucket for assets
    AssetsBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-assets
    # S3 Bucket Policy to allow access from CloudFront Origin Access Control (OAC)
    AssetsBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref AssetsBucket
        PolicyDocument:
          Statement:
            - Action: s3:GetObject
              Effect: Allow
              Resource: !Sub ${AssetsBucket.Arn}/*
              Principal:
                Service: cloudfront.amazonaws.com
              Condition:
                StringEquals:
                  AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
    CloudFrontDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Enabled: true
          PriceClass: PriceClass_All
          # List of origins. S3 Bucket, Server function, and Image Optimization function
          Origins:
            - Id: StaticAssetOrigin
              DomainName: !GetAtt AssetsBucket.DomainName
              S3OriginConfig:
                OriginAccessIdentity: ""
              OriginAccessControlId: !GetAtt CloudFrontAccessToS3Bucket.Id
            - Id: ImageOptimizationFunctionOrigin
              # Remove https:// from URL
              DomainName: !Select [2, !Split ["/", !GetAtt ImageOptimizationLambdaFunctionUrl.FunctionUrl]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
            - Id: ServerFunctionOrigin
              # Remove https:// from URL
              DomainName: !Select [2, !Split ["/", !GetAtt ServerLambdaFunctionUrl.FunctionUrl]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
          # We need a "failover" Origin Group to try the "Server function" origin first, then fallback to the S3 bucket origin if the server function fails
          OriginGroups:
            Quantity: 1
            Items:
              - Id: ServerAndStaticAssetOriginGroup
                FailoverCriteria:
                  StatusCodes:
                    Quantity: 2
                    # TODO: Not sure if these are the correct error codes to use...
                    Items:
                      - 500
                      - 502
                Members:
                  Quantity: 2
                  Items:
                    - OriginId: ServerFunctionOrigin
                    - OriginId: StaticAssetOrigin
          # Default is used by /* resources
          DefaultCacheBehavior:
            MinTTL: 0
            DefaultTTL: 0
            MaxTTL: 31536000
            TargetOriginId: ServerAndStaticAssetOriginGroup
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods: ["GET", "HEAD", "OPTIONS"]
            CachedMethods: ["HEAD", "GET"]
            Compress: true
            ForwardedValues:
              QueryString: true
              Headers:
                - x-op-middleware-request-headers
                - x-op-middleware-response-headers
                - x-nextjs-data
                - x-middleware-prefetch
              Cookies:
                Forward: all
          CacheBehaviors:
            - TargetOriginId: StaticAssetOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/static/*
              Compress: true
              AllowedMethods: ["GET", "HEAD", "OPTIONS"]
              CachedMethods: ["HEAD", "GET"]
              ForwardedValues:
                QueryString: false
            - TargetOriginId: ServerFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /api/*
              AllowedMethods:
                ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers: ["Authorization", "Host", "Accept-Language"]
            - TargetOriginId: ImageOptimizationFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/image
              AllowedMethods:
                ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
              ForwardedValues:
                QueryString: true
                Headers: ["Accept"]
            - TargetOriginId: ServerFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/data/*
              AllowedMethods: ["GET", "HEAD"]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers:
                  - x-op-middleware-request-headers
                  - x-op-middleware-response-headers
                  - x-nextjs-data
                  - x-middleware-prefetch
    CloudFrontAccessToS3Bucket:
      Type: AWS::CloudFront::OriginAccessControl
      Properties:
        OriginAccessControlConfig:
          Name: CloudFrontAccessToS3BucketOriginAccess
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4
juliocorzo commented 1 year ago

I'm in the process of moving this to Terraform, does anyone have a CloudFormation template that includes middleware? Still trying to understand how that works. The docs mention API Gateway, but I cannot find any information on that.

Thanks for the template @teriu, super helpful!

So, to add middleware, that function needs to be added as a viewer request lambda function association on the default and /_next/data/* behavior.

nabioz commented 1 year ago

Base version of Serverless/CloudFormation, without the Middleware Edge function:

# Service name
service: next-app

# Ensure configuration validation issues fail the command (safest option)
configValidationMode: error

# Package individually as multiple lambdas created
package:
  individually: true

# Define plugins
plugins:
  - serverless-scriptable-plugin
  - serverless-s3-sync

provider:
  name: aws
  region: us-west-2

  # Use direct deployments (faster). This is going to become the default in v4.
  # See https://www.serverless.com/framework/docs/providers/aws/guide/deploying#deployment-method
  deploymentMethod: direct

  # Ensure Lambdas can access Assets S3 Bucket
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - "s3:GetObject"
          Resource:
            - "arn:aws:s3:::${self:service}-assets/*"

functions:
  imageOptimization:
    name: ${self:service}-image-optimization
    description: Image Optimization Lambda for Next.js App
    handler: index.handler
    runtime: nodejs18.x
    architecture: arm64
    memorySize: 1024
    # We need a Function URL to use for the CloudFront Origin URL
    url: true
    package:
      artifact: .open-next/zips/image-optimization-function.zip
    # Set S3 BUCKET_NAME for Image Optimization Lambda to use
    environment:
      BUCKET_NAME: ${self:service}-assets
  server:
    name: ${self:service}-server
    description: Server Lambda for Next.js App
    handler: index.handler
    runtime: nodejs18.x
    architecture: arm64
    memorySize: 512
    # We need a Function URL to use for the CloudFront Origin URL
    url: true
    package:
      artifact: .open-next/zips/server-function.zip

custom:
  scriptable:
    hooks:
      before:package:createDeploymentArtifacts:
        - npx nx run build
        - mkdir -p ./.open-next/zips
        - cd .open-next/server-function && zip -r ../zips/server-function.zip .
        - cd .open-next/image-optimization-function && zip -r ../zips/image-optimization-function.zip .
  s3Sync:
    - bucketName: ${self:service}-assets
      localDir: .open-next/assets
      params: # Cache control
        # Un-hashed files, should be cached at the CDN level, but not at the browser level
        - "**/*":
            CacheControl: "public,max-age=0,s-maxage=31536000,must-revalidate"
        # Hashed files, should be cached both at the CDN level and at the browser level
        - "_next/**/*":
            CacheControl: "public,max-age=31536000,immutable"

resources:
  Description: Next App Infrastructure
  Resources:
    # S3 Bucket for assets
    AssetsBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-assets
    # S3 Bucket Policy to allow access from CloudFront Origin Access Control (OAC)
    AssetsBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref AssetsBucket
        PolicyDocument:
          Statement:
            - Action: s3:GetObject
              Effect: Allow
              Resource: !Sub ${AssetsBucket.Arn}/*
              Principal:
                Service: cloudfront.amazonaws.com
              Condition:
                StringEquals:
                  AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}
    CloudFrontDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Enabled: true
          PriceClass: PriceClass_All
          # List of origins. S3 Bucket, Server function, and Image Optimization function
          Origins:
            - Id: StaticAssetOrigin
              DomainName: !GetAtt AssetsBucket.DomainName
              S3OriginConfig:
                OriginAccessIdentity: ""
              OriginAccessControlId: !GetAtt CloudFrontAccessToS3Bucket.Id
            - Id: ImageOptimizationFunctionOrigin
              # Remove https:// from URL
              DomainName: !Select [2, !Split ["/", !GetAtt ImageOptimizationLambdaFunctionUrl.FunctionUrl]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
            - Id: ServerFunctionOrigin
              # Remove https:// from URL
              DomainName: !Select [2, !Split ["/", !GetAtt ServerLambdaFunctionUrl.FunctionUrl]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
          # We need a "failover" Origin Group to try the "Server function" origin first, then fallback to the S3 bucket origin if the server function fails
          OriginGroups:
            Quantity: 1
            Items:
              - Id: ServerAndStaticAssetOriginGroup
                FailoverCriteria:
                  StatusCodes:
                    Quantity: 2
                    # TODO: Not sure if these are the correct error codes to use...
                    Items:
                      - 500
                      - 502
                Members:
                  Quantity: 2
                  Items:
                    - OriginId: ServerFunctionOrigin
                    - OriginId: StaticAssetOrigin
          # Default is used by /* resources
          DefaultCacheBehavior:
            MinTTL: 0
            DefaultTTL: 0
            MaxTTL: 31536000
            TargetOriginId: ServerAndStaticAssetOriginGroup
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods: ["GET", "HEAD", "OPTIONS"]
            CachedMethods: ["HEAD", "GET"]
            Compress: true
            ForwardedValues:
              QueryString: true
              Headers:
                - x-op-middleware-request-headers
                - x-op-middleware-response-headers
                - x-nextjs-data
                - x-middleware-prefetch
              Cookies:
                Forward: all
          CacheBehaviors:
            - TargetOriginId: StaticAssetOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/static/*
              Compress: true
              AllowedMethods: ["GET", "HEAD", "OPTIONS"]
              CachedMethods: ["HEAD", "GET"]
              ForwardedValues:
                QueryString: false
            - TargetOriginId: ServerFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /api/*
              AllowedMethods:
                ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers: ["Authorization", "Host", "Accept-Language"]
            - TargetOriginId: ImageOptimizationFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/image
              AllowedMethods:
                ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
              ForwardedValues:
                QueryString: true
                Headers: ["Accept"]
            - TargetOriginId: ServerFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/data/*
              AllowedMethods: ["GET", "HEAD"]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers:
                  - x-op-middleware-request-headers
                  - x-op-middleware-response-headers
                  - x-nextjs-data
                  - x-middleware-prefetch
    CloudFrontAccessToS3Bucket:
      Type: AWS::CloudFront::OriginAccessControl
      Properties:
        OriginAccessControlConfig:
          Name: CloudFrontAccessToS3BucketOriginAccess
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4

I'm getting Access Denied from cloudfront url after using this exact config. Is it working for everybody else?

juliocorzo commented 1 year ago

@mrunbanked have you tried going to a public asset such as <your url>/_next/static/favicon.ico (if it exists?) access denied there would mean that the distribution itself does not have access the the S3 bucket.

I have it working on Terraform, and your template is almost identical, with the slight difference that

  1. I'm using an origin access identity for my S3 origin config on the static origin.
  2. I'm using that origin access identity's ARN as the AWS principal for the S3 bucket policy.
  3. I'm using RegionalDomainName rather than DomainName in the DomainName parameter of the static origin.

https://docs.aws.amazon.com/whitepapers/latest/secure-content-delivery-amazon-cloudfront/s3-origin-with-cloudfront.html

nabioz commented 1 year ago

Ok seems like something was wrong with my package versions so server wasn't working properly. After updating all the packages it works! thanks 🙏

Kipperlenny commented 1 year ago

I am now using:

# Service name
service: abcabcabc

useDotenv: true

plugins:
  - serverless-scriptable-plugin
  - serverless-s3-sync
  #- '@silvermine/serverless-plugin-cloudfront-lambda-edge'

package:
  individually: true

provider:
  name: aws
  region: "${env:AWS_DEFAULT_REGION}" # Resource handler returned message: "Invalid request provided: AWS::CloudFront::Distribution: The function must be in region 'us-east-1'
  endpointType: REGIONAL
  apiGateway:
    shouldStartNameWithService: true
    binaryMediaTypes:
      - "*/*"

custom:
  scriptable:
    hooks:
      before:package:createDeploymentArtifacts:
        - OPEN_NEXT_DEBUG=true npx open-next@latest build
        - mkdir -p ./.open-next/zips
        - cd .open-next/server-function && zip -r ../zips/server-function.zip .
        - cd .open-next/image-optimization-function && zip -r ../zips/image-optimization-function.zip .
  s3Sync:
    - bucketName: ${self:service}-assets
      localDir: .open-next/assets
      acl: public-read # optional
      params: # Cache control
        # Un-hashed files, should be cached at the CDN level, but not at the browser level
        - "**/*":
            CacheControl: "public,max-age=0,s-maxage=31536000,must-revalidate"
        # Hashed files, should be cached both at the CDN level and at the browser level
        - "_next/**/*":
            CacheControl: "public,max-age=31536000,immutable"
  siteName: "${env:SUBDOMAIN}"
  aliasHostedZoneId: Z2FDTNDATAQYW2     # us-east-1

functions:
  server:
    description: Default Lambda for Next CloudFront distribution
    name: "${env:WEB_LAMBDA}"
    handler: index.handler
    runtime: nodejs18.x
    architecture: arm64
    memorySize: 512
    timeout: 10
    # We need a Function URL to use for the CloudFront Origin URL
    url: true
    package:
      artifact: .open-next/zips/server-function.zip
  imageOptimization:
    description: Image Lambda for Next CloudFront distribution
    name: "${env:IMAGE_LAMBDA}"
    handler: index.handler
    runtime: nodejs18.x
    architecture: arm64
    memorySize: 512
    timeout: 10
    # We need a Function URL to use for the CloudFront Origin URL
    url: true
    package:
      artifact: .open-next/zips/image-optimization-function.zip
    # Set S3 BUCKET_NAME for Image Optimization Lambda to use
    environment:
      BUCKET_NAME: ${self:service}-assets

resources:
  Resources:
    AssetsBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:service}-assets
        AccessControl: PublicRead
    # S3 Bucket Policy to allow access from CloudFront Origin Access Control (OAC)
    AssetsBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref AssetsBucket
        PolicyDocument:
          Statement:
            - Action: s3:GetObject
              Effect: Allow
              Resource: !Sub ${AssetsBucket.Arn}/*
              Principal:
                Service: cloudfront.amazonaws.com
              Condition:
                StringEquals:
                  AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${DefaultDistribution}
    DefaultDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Enabled: true
          PriceClass: PriceClass_100
          ViewerCertificate:
            AcmCertificateArn: "AAAAAAAARRRRRRRRRRRRRRNNNNNNNNNNN"
            MinimumProtocolVersion: TLSv1.1_2016
            SslSupportMethod: sni-only
          Aliases: ["${env:SUBDOMAIN}"]
          Origins:
            - Id: ServerFunctionOrigin
              # Remove https:// from URL
              DomainName: !Select [2, !Split ["/", !GetAtt ServerLambdaFunctionUrl.FunctionUrl]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
            - Id: StaticAssetOrigin
              DomainName: !GetAtt AssetsBucket.RegionalDomainName
              S3OriginConfig:
                OriginAccessIdentity: ""
              OriginAccessControlId: !GetAtt CloudFrontAccessToS3Bucket.Id
            - Id: ImageOptimizationFunctionOrigin
              # Remove https:// from URL
              DomainName: !Select [2, !Split ["/", !GetAtt ImageOptimizationLambdaFunctionUrl.FunctionUrl]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
          # We need a "failover" Origin Group to try the "Server function" origin first, then fallback to the S3 bucket origin if the server function fails
          OriginGroups:
            Quantity: 1
            Items:
              - Id: ServerAndStaticAssetOriginGroup
                FailoverCriteria:
                  StatusCodes:
                    Quantity: 2
                    # TODO: Not sure if these are the correct error codes to use...
                    Items:
                      - 500
                      - 502
                Members:
                  Quantity: 2
                  Items:
                    - OriginId: ServerFunctionOrigin
                    - OriginId: StaticAssetOrigin
          # Default is used by /* resources
          DefaultCacheBehavior:
            MinTTL: 0
            DefaultTTL: 0
            MaxTTL: 31536000
            TargetOriginId: ServerAndStaticAssetOriginGroup
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods: ["GET", "HEAD", "OPTIONS"]
            CachedMethods: ["HEAD", "GET"]
            Compress: true
            ForwardedValues:
              QueryString: true
              Headers:
                - x-op-middleware-request-headers
                - x-op-middleware-response-headers
                - x-nextjs-data
                - x-middleware-prefetch
              Cookies:
                Forward: all
          CacheBehaviors:
            - TargetOriginId: StaticAssetOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/static/*
              Compress: true
              AllowedMethods: ["GET", "HEAD", "OPTIONS"]
              CachedMethods: ["HEAD", "GET"]
              ForwardedValues:
                QueryString: false
            - TargetOriginId: ServerFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /api/*
              AllowedMethods:
                ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers: ["Authorization", "Host", "Accept-Language"]
            - TargetOriginId: ImageOptimizationFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/image
              AllowedMethods:
                ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
              ForwardedValues:
                QueryString: true
                Headers: ["Accept"]
            - TargetOriginId: ServerFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/data/*
              AllowedMethods: ["GET", "HEAD"]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers:
                  - x-op-middleware-request-headers
                  - x-op-middleware-response-headers
                  - x-nextjs-data
                  - x-middleware-prefetch
    CloudFrontAccessToS3Bucket:
      Type: AWS::CloudFront::OriginAccessControl
      Properties:
        OriginAccessControlConfig:
          Name: CloudFrontAccessToS3BucketOriginAccess
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4
    FrontPageDNSName:
      Type: "AWS::Route53::RecordSet"
      Properties:
        AliasTarget:
          DNSName:
            Fn::GetAtt:
              - DefaultDistribution
              - DomainName
          HostedZoneId: ${self:custom.aliasHostedZoneId}
        HostedZoneName: abc.com.
        Name: ${self:custom.siteName}.
        Type: 'CNAME'
  Outputs:
    DefaultDistribution:
      Value:
        Fn::GetAtt:
          - DefaultDistribution
          - DomainName

and it is deploying fine as far as I can see now.

watch https://github.com/serverless/serverless/issues/11424 you should not use the serverless dashboard. The app/org in the serverless.yml destroys the lambda... -

mbaquerizo commented 9 months ago

This issue is referenced in the README as being open-next being compatible with Serverless Framework, but I'm a bit confused. Is @emulienfou's post a question or is it the answer? It's the only config file I see that references lambdaAtEdge, so is edge working?

I understand the open-sourced-ness of this, so if the answer is I need to figure it out, that's fine.

emulienfou commented 9 months ago

Hi @mbaquerizo I post a started template for Serverless Framework. It's not working properly at 100% at the time of the post. If you check the other answers you should be able to make it work by adapting all the examples of the templates

celso-alexandre commented 4 months ago

I could make it work propperly on serverless framework with this configuration. Not sure about cache-related stuff, though

service: next-app
useDotenv: true
configValidationMode: error

plugins:
  - serverless-scriptable-plugin
  - serverless-s3-sync
  - serverless-prune-plugin

package:
  individually: true

provider:
  name: aws
  runtime: nodejs20.x
  stage: ${opt:stage, 'dev'}
  region: 'sa-east-1'
  profile: default
  deploymentMethod: direct
  environment:
    STAGE: ${self:provider.stage}
    # OPEN-NEXT
    SHARP_VERSION: "0.32.6" 
    REVALIDATION_QUEUE_URL: !GetAtt CacheRevalidationQueue.QueueUrl
    REVALIDATION_QUEUE_REGION: ${self:provider.region}
    CACHE_DYNAMO_TABLE: !Ref CacheTable
    CACHE_BUCKET_NAME: ${self:custom.cacheBucketName}
    CACHE_BUCKET: ${self:custom.cacheBucketName}
    # CACHE_BUCKET_KEY_PREFIX: cache
    CACHE_BUCKET_REGION: ${self:provider.region}
    BUCKET_NAME: ${self:custom.assetsBucketName}
    # BUCKET_KEY_PREFIX: assets
    # DYNAMO_BATCH_WRITE_COMMAND_CONCURRENCY: 5
    # MAX_REVALIDATE_CONCURRENCY: 10
  iam:
    role:
      statements:
        - Effect: "Allow"
          Action:
            - s3:GetObject
            - s3:PutObject
            - s3:ListObjects
            - dynamodb:PutItem
            - dynamodb:Query
            - sqs:SendMessage
            - lambda:InvokeFunction
          Resource: "*"
  deploymentBucket:
    name: ${self:provider.stage}-serverless
    blockPublicAccess: true
  endpointType: REGIONAL
  apiGateway:
    shouldStartNameWithService: true
    binaryMediaTypes:
      - "*/*"

custom:
  schema: ${self:service}
  aliasHostedZoneId: Z00000000000000000
  assetsBucketName: ${self:service}-${self:provider.stage}-assets
  cacheBucketName: ${self:service}-${self:provider.stage}-cache
  prune:
    automatic: true
    number: 10
  scriptable:
    hooks:
      after:deploy:finalize: 
        - result=$(aws lambda invoke --function-name ${self:service}-${self:provider.stage}-dynamodb-provider /dev/null) && echo "$result"
      before:package:createDeploymentArtifacts:
        # - SHARP_VERSION="0.32.6" OPEN_NEXT_DEBUG=true yarn open-next build --dangerously-disable-dynamodb-cache --dangerously-disable-incremental-cache
        - SHARP_VERSION="0.32.6" yarn open-next build --minify
        - mkdir -p ./.open-next/zips
        - cd .open-next/server-function && zip -r ../zips/server-function.zip .
        - cd .open-next/image-optimization-function && zip -r ../zips/image-optimization-function.zip .
        - cd .open-next/revalidation-function && zip -r ../zips/revalidation-function.zip .
        - cd .open-next/warmer-function && zip -r ../zips/warmer-function.zip .
  s3Sync:
    - bucketName: ${self:custom.assetsBucketName}
      localDir: .open-next/assets
      params:
        - "**/*":
            CacheControl: "public,max-age=0,s-maxage=31536000,must-revalidate"
        - "_next/**/*":
            CacheControl: "public,max-age=31536000,immutable"

functions:
  server:
    description: Default Lambda for Next CloudFront distribution
    name: ${self:service}-${self:provider.stage}-server
    handler: index.handler
    runtime: nodejs20.x
    architecture: arm64
    memorySize: 512
    timeout: 10
    url: true
    package:
      artifact: .open-next/zips/server-function.zip
  imageOptimization:
    description: Image Lambda for Next CloudFront distribution
    name: ${self:service}-${self:provider.stage}-image-optimization
    handler: index.handler
    runtime: nodejs20.x
    architecture: arm64
    memorySize: 512
    timeout: 10
    url: true
    package:
      artifact: .open-next/zips/image-optimization-function.zip
  cacheRevalidation:
    description: Cache Revalidation Lambda for Next CloudFront distribution
    name: ${self:service}-${self:provider.stage}-cache-revalidation
    handler: index.handler
    runtime: nodejs20.x
    architecture: arm64
    memorySize: 512
    timeout: 10
    url: true
    package:
      artifact: .open-next/zips/revalidation-function.zip
    events:
      - sqs:
          arn: !GetAtt CacheRevalidationQueue.Arn
  warmer:
    description: Lambdas warmer (cheap provisioned concurrency)
    name: ${self:service}-${self:provider.stage}-warmer
    handler: index.handler
    runtime: nodejs20.x
    architecture: arm64
    memorySize: 512
    timeout: 10
    url: true
    package:
      artifact: .open-next/zips/warmer-function.zip
    environment:
      FUNCTION_NAME: ${self:service}-${self:provider.stage}-server
      CONCURRENCY: 1
    events:
      - schedule:
          rate: rate(5 minutes)

resources:
  Resources:
    CacheTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-${self:provider.stage}-cache
        AttributeDefinitions:
          - AttributeName: tag
            AttributeType: S
          - AttributeName: path
            AttributeType: S
          - AttributeName: revalidatedAt
            AttributeType: N
        KeySchema:
          - AttributeName: tag
            KeyType: HASH
          - AttributeName: path
            KeyType: RANGE
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5
        GlobalSecondaryIndexes:
          - IndexName: revalidate
            KeySchema:
              - AttributeName: path
                KeyType: HASH
              - AttributeName: revalidatedAt
                KeyType: RANGE
            Projection:
              ProjectionType: ALL
            ProvisionedThroughput:
              ReadCapacityUnits: 5
              WriteCapacityUnits: 5
    CacheRevalidationQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-${self:provider.stage}-cache-revalidation
    CacheBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.cacheBucketName}
    AssetsBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${self:custom.assetsBucketName}
    AssetsBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref AssetsBucket
        PolicyDocument:
          Statement:
            - Action: s3:GetObject
              Effect: Allow
              Resource: !Sub ${AssetsBucket.Arn}/*
              Principal:
                Service: cloudfront.amazonaws.com
              # Condition:
              #   StringEquals:
              #     AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${DefaultDistribution}
    DefaultDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Enabled: true
          PriceClass: PriceClass_All
          CustomErrorResponses:
            - ErrorCode: "404"
              ResponsePagePath: "/index.html"
              ResponseCode: "200"
              ErrorCachingMinTTL: "30"
          ViewerCertificate:
            AcmCertificateArn: arn:aws:acm:us-east-1:0000000000
            MinimumProtocolVersion: TLSv1.2_2018
            SslSupportMethod: sni-only
          Aliases: ["next-app.com"]
          Origins:
            - Id: ServerFunctionOrigin
              DomainName: !Select [2, !Split ["/", !GetAtt ServerLambdaFunctionUrl.FunctionUrl]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
            - Id: StaticAssetOrigin
              DomainName: !GetAtt AssetsBucket.RegionalDomainName
              S3OriginConfig:
                OriginAccessIdentity: ""
              OriginAccessControlId: !GetAtt CloudFrontAccessToS3Bucket.Id
            - Id: ImageOptimizationFunctionOrigin
              DomainName: !Select [2, !Split ["/", !GetAtt ImageOptimizationLambdaFunctionUrl.FunctionUrl]]
              CustomOriginConfig:
                HTTPSPort: 443
                OriginProtocolPolicy: https-only
          OriginGroups:
            Quantity: 1
            Items:
              - Id: ServerAndStaticAssetOriginGroup
                FailoverCriteria:
                  StatusCodes:
                    Quantity: 2
                    Items:
                      - 500
                      - 502
                Members:
                  Quantity: 2
                  Items:
                    - OriginId: ServerFunctionOrigin
                    - OriginId: StaticAssetOrigin
          DefaultCacheBehavior:
            MinTTL: 0
            DefaultTTL: 0
            MaxTTL: 31536000
            TargetOriginId: ServerAndStaticAssetOriginGroup
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods: ["GET", "HEAD", "OPTIONS"]
            CachedMethods: ["HEAD", "GET"]
            Compress: true
            ForwardedValues:
              QueryString: true
              Headers:
                - x-op-middleware-request-headers
                - x-op-middleware-response-headers
                - x-nextjs-data
                - x-middleware-prefetch
              Cookies:
                Forward: all
          CacheBehaviors:
            # public directory (bucket root)
            - TargetOriginId: StaticAssetOrigin
              ViewerProtocolPolicy: https-only
              # Tip: Create only a few directories, or even just one inside "public" and just use /directory/* instead of what I did
              PathPattern: "/*.*" # or /*.{jpg}
              Compress: true
              AllowedMethods: ["GET", "HEAD", "OPTIONS"]
              CachedMethods: ["HEAD", "GET"]
              ForwardedValues:
                QueryString: false
            - TargetOriginId: StaticAssetOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: "/**/*.*" # or /**/*.{jpg}
              Compress: true
              AllowedMethods: ["GET", "HEAD", "OPTIONS"]
              CachedMethods: ["HEAD", "GET"]
              ForwardedValues:
                QueryString: false
            #
            - TargetOriginId: StaticAssetOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/static/*
              Compress: true
              AllowedMethods: ["GET", "HEAD", "OPTIONS"]
              CachedMethods: ["HEAD", "GET"]
              ForwardedValues:
                QueryString: false
            - TargetOriginId: ServerFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /api/*
              AllowedMethods:
                ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers: ["Authorization", "Host", "Accept-Language"]
            - TargetOriginId: ImageOptimizationFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/image
              AllowedMethods:
                ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
              ForwardedValues:
                QueryString: true
                Headers: ["Accept"]
            - TargetOriginId: ServerFunctionOrigin
              ViewerProtocolPolicy: https-only
              PathPattern: /_next/data/*
              AllowedMethods: ["GET", "HEAD"]
              ForwardedValues:
                QueryString: true
                Cookies:
                  Forward: all
                Headers:
                  - x-op-middleware-request-headers
                  - x-op-middleware-response-headers
                  - x-nextjs-data
                  - x-middleware-prefetch
    CloudFrontAccessToS3Bucket:
      Type: AWS::CloudFront::OriginAccessControl
      Properties:
        OriginAccessControlConfig:
          Name: CloudFrontAccessToS3BucketOriginAccess
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4
    FrontPageDNSName:
      Type: AWS::Route53::RecordSet
      Properties:
        HostedZoneId: Z00000000000000000
        Comment: ${self:provider.stage} Next APP DNS
        Name:
          - "next-app.com"
        Type: CNAME
        TTL: 360
        ResourceRecords:
          - !GetAtt DefaultDistribution.DomainName

I've added the two missing lambdas (warmer and revalidation) to the mix and the missing variables, as per-doc. And aws is now enforcing s3 bucket ACL deprecation, preferring and defaulting new buckets to bucket policy only, disabling ACL by default. My open-next version: 2.3.6 (latest, as I spoke) EDIT: It works ~, except for this problem: https://github.com/sst/open-next/issues/373~ 🟢 The issue regarding sharp is workarounded, and as far as I undertand, lies in next version itself, but the workaround worked, and I've updated the example below. ~EDIT 2: I've ported an already existing next app running as standalone on a container, so I'm currently trying to figure out with the app developers why fonts and the theme isn't loaded propperly on use-client pages.~ 🟢 The serverless hooks plugin cannot read environment variables propperly, so I've written a JS script that loads environment variables and then runs the same build and zip commands, and call it there instead ~EDIT 3: The site deployed using this solution instead of our previous solution (Fargate + Load Balancer) feel much less snappier for some reason. Not quite sure why, yet..~ ~EDIT 4: For some reason, the 'public' directory assets (including /images) seems to be inacessible through cdn~ 🟢 It was because the public directory is at the root of the assets bucket (in my case / vercel default), so I've just added a handler for any files at the root or inside a subfolder of the bucket (it may be safer to specify the exact extensions, perhaps). It solved the slowdown like magic. Just updated the example below

celso-alexandre commented 4 months ago

At the docs, there is a "Dynamo provider". In the .open-next directory there is a "dynamodb-provider" (it does not finishes with "-function"), but I thought it was not a lambda. It seems I was wrong, despite it isn't in this docs drawing:

Open-next Infrastruture But it is mentioned here https://open-next.js.org/advanced/architecture#dynamo-provider-function. Is it actually a lambda?