Deploy NuxtJs app to AWS with SAM

Deploy NuxtJs app to AWS with SAM

·

6 min read

ကျွန်တော်တို့ SSR (Server-Side Rendering) ပါ တဲ့ application တွေ ကို AWS ပေါ်မှာ serverless run ချင်တဲ့အခါမှာ Lambda ကိုသုံးလို့ရပါတယ်။ ပုံမှန်ဆိုရင်တော့ serverless framework ကိုသုံးပြီး deploy လိုက်တာက မြန်မြန်ဆန်ဆန်နဲ့ အလုပ်ရှုပ်သက်သာပါတယ်။ Serverless v4 က စပြီး ဝယ်သုံးဖို့လိုလာပါပြီ free က လည်း 2 credits ပဲ ပေးသုံးတာဖြစ်တဲ့အတွက် uat + dev မှာတင် လမ်းဆုံးပြီး production အတွက်က ဝယ်သုံးရမလိုဖြစ်နေပါပြီ။ Serverless v3 လည်း 2024 ကုန်ရင် security support end တော့မှာဆိုတော့ ရေရှည်မှာ အဆင်မပြေတဲ့အတွက် ကျွန်တော်ကတော့ SST နဲ့ AWS SAM တို့ထဲက တခုခုကို ပြောင်းသုံးဖို့ စဉ်းစားထားပါတယ်။ ရုံးမှာ ရှိနေပြီးသား NUXT နဲ့ရေးထားတဲ့ Frontend app တစ်ခုကို SAM နဲ့ deploy လုပ်ခဲ့တဲ့ အဆင့်လေးတွေကို ပြန်ပြီး မျှဝေပေးချင်ပါတယ်။ SST အကြောင်းကိုတော့ နောက်ကြုံမှ ထပ်ရေးပေးပါအုံးမယ်။

Build nuxt app with aws-lambda preset

အရင်ဆုံး build လိုက်တဲ့ output က Lambda ပေါ်မှာ တင် run လို့ရအောင် ပြင်ဖို့လိုပါတယ်။
nuxt build လိုက်တဲ့အခါ်မှာ nitro ရဲ့ default set က node-server ဖြစ်တဲ့အတွက် lambda ပေါ်ဒီအတိုင်း တင် run လို့ မရပါဘူး။ အဲ့တော့ ကျွန်တော်တို့ NITRO_PRESET ကို ပြောင်းပြီး build ကြည့် ပါမယ်။

 NITRO_PRESET=aws-lambda npm run build

ဒါဆိုရင်တော့ ကျွန်တော်တို့ build လိုက်တဲ့ output က lambda ပေါ်မှာ run လို့ရတဲ့ output လေး ထွက်လာပါပြီ။ nitro preset မှာ aws-lambda လို့ ပြနေပါတယ်။

nuxt build လိုက်တဲ့ output တွေက default အတိုင်းဆို .output dir ထဲမှာ သိမ်းပါတယ်။ .output ထဲမှာ ကြည့်လိုက်ရင် folder နှစ်ခု တွေ့ရမှာပါမယ်။ public folder က public asset တွေသိမ်းတာ ဖြစ်ပြီး server folder ကတော့ lambda ပေါ်တင် run ရမဲ့ build ပြီးသား script ဖိုင်တွေဖြစ်ပါတယ်။ nuxt.config.ts ဖိုင် ထဲမှာ ပြင်ပြီး public နဲ့ server ကို folder တစ်ခုတည်း ပေါင်းထုတ်ခိုင်းလို့လည်းရပါတယ်။ အဲ့တာဆိုရင်တော့ cloudfront ကနေ အသေးစိတ် handle လုပ်စရာမလိုပဲနဲ့ အကုန်လုံးကို lambda ပေါ်ကို ပေါင်းပြီးတင်လိုက်လို့ရတဲ့အတွက် ပိုရှင်းလင်းပြီး လွယ်ကူပါတယ်။ ဒါပေမဲ့ ဒီနည်းလမ်းက production မှာ မသုံးသင့်ပါဘူး။ static file တွေကို request လုပ်တိုင်း lambda function ထ run ရမယ်ဆိုရင် လုံးဝ မတန်ပါဘူး။ ဒါ့ကြောင့် public assess တွေကို s3 ပေါ်တင်သုံးတာက ပို သင့်တော်ပါတယ်။

Creating Basic SAM Template

ကျွန်တော်တို့ SAM template file စရေးကြည့်ပါမယ်။ အရင်ဆုံး ထုံစံအတိုင်း env variables တွေ နဲ့ parameters တွေကို ရေးလိုက်ပါမယ်။

#template.yaml
---
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM template for nuxt-lambda

Globals:
  Function:
    Runtime: nodejs18.x
    Timeout: 15
    Environment:
      Variables:
        NODE_ENV: !Ref NodeEnv
        NUXT_PUBLIC_ROOT_API: !Ref NuxtPublicRootApi
        NUXT_APP_URL: !Ref NuxtAppUrl


Parameters:
  Stage:
    Type: String
    Default: dev
  S3Bucket:
    Type: String
  NodeEnv:
    Type: String
    Default: production
  NuxtPublicRootApi:
    Type: String
  NuxtAppUrl:
    Type: String

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: "Application Configuration"
        Parameters:
          - Stage
          - NodeEnv
          - NuxtPublicRootApi
          - NuxtAppUrl
    ParameterLabels:
      Stage:
        default: "Deployment Stage"
      NodeEnv:
        default: "Node Environment"
      NuxtPublicRootApi:
        default: "Nuxt Public Root API"
      NuxtAppUrl:
        default: "Nuxt App URL"

Resources:

Outputs:

Resources

နောက်တဆင့်မှာ Resource တွေထည့်ပါမယ်။ ကျွန်တော်တို့မှာ cloudFront distribution, S3 Bucket, API Gateway, နဲ့ Lambda Function ဆိုပြီး basic resource လေးခုထည့်ဖို့လိုပါမယ်။ နောက်ထပ် additional resources တွေအနေနဲ့ cloudfront ကနေ s3 ကို access ရဖို့အတွက် s3 bucket policy နဲ့ CloudFront OAC တို့ကို ထည့်ပေးဖို့လိုပါမယ်။ နောက်တဆင့်ကြရင်တော့ resource တစ်ခုခြင်းဆီအတွက် detail လေးတွေ ဆက်ရေးကြပါမယ်။

Resources:
  # HttpApi to trigger the Lambda function
  HttpApi:

  # Lambda function to serve the Nuxt.js application
  LambdaFunction:

  # S3 bucket to store the Nuxt.js app's static assets
  StaticBucket:

  # S3 bucket policy to allow CloudFront to access the static assets
  StaticBucketPolicy:

  # CloudFront OAC to restrict access to the S3 bucket
  CloudFrontOriginAccessControl:

  # CloudFront distribution to serve the Nuxt.js app
  CloudFrontDistribution:

HttpApi:

API GateWay အတွက် ကတော့ ဒီလိုလေးပဲ create လုပ်ပေးလိုက်ရင် လုံလောက်ပါတယ်။

  # HttpApi to trigger the Lambda function
  HttpApi:
    Type: AWS::Serverless::HttpApi

LambdaFunction:

Lambda function မှာတော့ လိုအပ်တဲ့ properties တွေနည်းနည်းများပါမယ်။ ဒီအထဲက အချို့ကိုရှင်းပြရရင်တော့

  • BuildProperties ကတော့ lambda ပေါ်ကိုတင်တဲ့အခါမှာ မလိုအပ်တဲ့ ဖိုင်တွေပါမသွားပဲ .output/server/ ထဲက ဖိုင်တွေပဲ ပါသွားဖို့အတွက်ဖြစ်ပါတယ်။

  • Event > ProxyResource ကတော့ အပေါ်မှာ create လုပ်ခဲ့တဲ့ HttpApi နဲ့ integrate လုပ်ဖို့ဖြစ်ပါတယ်။

  # Lambda function to serve the Nuxt.js application
  LambdaFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: nodejs18.x
      BuildProperties:
        Exclude:
          - '**/*'
        Include:
          - '.output/server/**'
    Properties:
      Handler: index.handler
      CodeUri: .output/server
      MemorySize: 256
      Policies: 
        - AWSLambdaBasicExecutionRole
      Events:
        ProxyResource:
          Type: HttpApi
          Properties:
            ApiId: !Ref HttpApi
            Path: $default
            Method: ANY

StaticBucket:

static file တွေ သိမ်းဖို့အတွက် s3 bucket လေး create လုပ်ပါမယ်။

  # S3 bucket to store the Nuxt.js app's static assets
  StaticBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Ref S3Bucket
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerPreferred

StaticBucketPolicy:

CloudFront နေဝင်လာတဲ့ request တွေကို access လုပ်ဖို့အတွက် s3 bucket မှာ policy လေးထည့်ပေးဖို့လိုပါတယ်။

  # S3 bucket policy to allow CloudFront to access the static assets
  StaticBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref StaticBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: "cloudfront.amazonaws.com"
            Action: "s3:GetObject"
            Resource: !Sub "${StaticBucket.Arn}/*"
            Condition:
              StringEquals:
                "AWS:SourceArn": !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}"

CloudFrontOriginAccessControl:

CloudFront ကနေ s3 ကို OAC နဲ့ လှမ်းခေါ်လို့ရဖို့ OAC တစ်ခု ကြိုပြီး create လုပ်ပါမယ်။

  # CloudFront OAC to restrict access to the S3 bucket
  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: !Sub "${AWS::StackName}-OAC"
        SigningProtocol: sigv4
        SigningBehavior: always
        OriginAccessControlOriginType: s3

CloudFrontDistribution:

နောက်ဆုံးအနေနဲ့ CloudFront distribution create လုပ်ပါမယ်။ ကျွန်တော်တို့ရဲ့ distribution ထဲမှာ origin နှစ်ခုလိုပါမယ်။ တစ်ခုက S3 ဖြစ်ပြီး နောက်တစ်ခုကတော့ HttpApi ပါ။ Default Cache Behavior ထဲမှာတော့ Target Origin အဖြစ်နဲ့ HttpApi ကိုသုံးပါမယ်။ static file တွေကို request လုပ်တဲ့အခါမှာ S3 ကို forward လုပ်ပေးဖို့အတွက် Cache Behaviors တွေ ထပ်ထည့်ရပါမယ်။ public folder ထဲမှာ _nuxt , images folder နှစ်ခု နဲ့ favicon.ico ဖိုင် တခုရှိတဲ့အတွက် Cache behaviors စုစုပေါင်း သုံးခုလိုအပ်ပါတယ်။ ဒါကတော့ ကိုယ့် app ရဲ့ public folder ထဲမှာ ဘယ်နှစ်ခုရှိိလဲဆိုတာပေါ် မူတည်ပြီး ရေးဖို့လိုပါတယ်။

Aliases (Alternate domain name) နဲ့ ViewerCertificate certificate ကိုတော့ domain setup မလုပ်ချင်တော့လို့ comment ပိတ်ခဲ့ပါမယ်။ သုံးချင်ရင် comment ဖွင့်ပြီး နည်းနည်းပြင်သုံးလို့ရပါတယ်။

  # CloudFront distribution to serve the Nuxt.js app
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        # Aliases:
        #   - !Ref CloudFrontDomain
        # ViewerCertificate:
        #   AcmCertificateArn: !Ref CloudFrontCertificate
        #   SslSupportMethod: sni-only
        Comment: !Sub "${AWS::StackName} ${Stage} Nuxt App"
        CustomErrorResponses:
          - ErrorCachingMinTTL: 0
            ErrorCode: 500
          - ErrorCachingMinTTL: 0
            ErrorCode: 504
        Origins:
          - Id: SsrOrigin
            # DomainName: !Select [2, !Split ["/", !Ref LambdaFunctionUrl]]
            DomainName: !Sub "${HttpApi}.execute-api.${AWS::Region}.amazonaws.com"
            OriginPath: ""
            CustomOriginConfig:
              HTTPSPort: 443
              OriginProtocolPolicy: https-only
              OriginSSLProtocols: 
                - TLSv1.2
          - Id: S3Origin
            # DomainName: !GetAtt StaticBucket.DomainName
            DomainName: !Sub "${StaticBucket}.s3.${AWS::Region}.amazonaws.com"
            OriginAccessControlId: !Ref CloudFrontOriginAccessControl
            S3OriginConfig: {}
        DefaultCacheBehavior:
          TargetOriginId: SsrOrigin
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
            - OPTIONS
            - PUT
            - PATCH
            - POST
            - DELETE
          CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
          OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader
          Compress: true
        CacheBehaviors:
          - PathPattern: "/_nuxt/*"
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods:
              - GET
              - HEAD
              - OPTIONS
            Compress: true
            CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
            OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf # CORS-S3Origin
          - PathPattern: "/images/*"
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods:
              - GET
              - HEAD
              - OPTIONS
            Compress: true
            CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
            OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf # CORS-S3Origin
          - PathPattern: "/favicon.ico"
            TargetOriginId: S3Origin
            ViewerProtocolPolicy: redirect-to-https
            AllowedMethods:
              - GET
              - HEAD
              - OPTIONS
            Compress: true
            CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
            OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf # CORS-S3Origin

Outputs:

Resource တွေအကုန်ထည့်ပြီးတဲ့အခါမှာတော့ Output ထုတ်ပါမယ်။ ကျန်တာကိုတော့ အထွေအထူး မထုတ်တော့ပဲ cloudfront distribution domain တစ်ခုပဲ ထုတ်ကြည့်လိုက်ပါမယ်။

Outputs:
  CloudFrontDistributionDomainName:
    Value: !GetAtt CloudFrontDistribution.DomainName
    Description: Domain name of the CloudFront distribution

Deploy commands

ကျွန်တော်တို့ template file ရေးပြီးပြီဆိုရင်တော့ အရင်ဆုံး validate လုပ်ကြည့်ပါမယ်

sam validate --lint

validate လုပ်ကြည့်လို့ အတင်ပြေတယ်ဆိုရင်တော့ deploy လို့ရပါပြီ။ deploy မလုပ်ခင် build ထားပြီးသားဖြစ်ဖို့တော့ လိုပါမယ်နော်။ deploy တဲ့အခါမှာ guided mode နဲ့ deploy ဖို့အတွက် sam deploy -g command ကို သုံးပါမယ်။

NUXT_NITRO_PRESET=aws-lambda npm run build

 sam deploy -g

တစ်ခုသတိထားရမှာက sam deploy မလုပ်ခင် aws configure လုပ်ပြီး credentials တွေ ထည့်ထားဖို့ လိုပါလိမ့်မယ်။

prompt တွေအကုန်လုံးကို ဒီလို ရွေးပြီးသွားတဲ့အခါ်မှာတော့ aws ပေါ်မှာ deploy လုပ်ဖို့ ပြင်ဆင်ပြီးတဲ့အခါမှာ အောက်ကလိုမျိုး ပေါ်လာတဲ့အခါမှာ Y ကို ရွေးပြီး ဆက်သွားလိုက်ပါ။ resource တွေကို AWS ပေါ်မှာ deploy လုပ်ပေးပါလိမ့်မယ်။

Deployment ပြီးတဲ့အချိန်မှာတော့ output ကို ဒီလိုမြင်ရပါမယ်

SAM deployment ပြီးရင် ကျွန်တော်တို့ တစ်ခုတော့ လုပ်ဖို့ကျန်ပါသေးတယ်။ .output/public ထဲက ဖိုင်တွေကို S3 ပေါ်တင်ပေးရပါမယ် aws s3 sync command ကို သုံးပြီးတင်ပေးလိုက်ပါ။

ဒါတွေပြီးရင်တော့ ကျွန်တော်တို့ ရဲ့ deployment က အားလုံးပြီးသွားပါပြီ။ Result ကို sam deploy ပြီးတဲ့အခါမှာထွက်လာတဲ့ output ထဲက cloudfront domain ကို ဖွင့်ကြည့်ခြင်းအားဖြင့် မြင်တွေ့နိုင်ပါပြီ။

ဒီ deployment ကို github action သုံးပြီး ဘယ်လို deploy လုပ်မလဲဆိုတာကို နောက် article (https://kalaung.org/aws-sam-deployment-with-github-action) တခုအနေနဲ့ရေးပေးထားပါတယ်။ စိတ်ပါရင် ဆက်ပြီး အားပေးသွားပါအုံး။