amazon-web-servicesnext.jsenvironment-variablesamazon-app-runner

AWS AppRunner not using .env var in Build Phase - Next.JS


I am trying to migrate from Elastic Beanstalk to AWS App Runner, but AWS App Runner does not seem to consider the environment variable during it's build phase.

Troubleshooting:

  1. npm run build (next build) - Works successfully locally and references my .env vars successfully.
  2. My existing elastic beanstalk implementation (shared below successfully gets the .env vars from github actions and are included in the build process).
  3. AppRunner the environment variables are currently set as plain-text, but language around around the build and start steps indicate that the variables are only available in the start phase, not the build phase.
  4. This works without issue on vercel.

Would really like to use AWS AppRunner out of the box, part of the reason of migrating to app runner was ease of use and quickly pushing code and having it published.

References:

Elastic Beanstalk:

name: Deploy to Elastic Beanstalk

on:
  push:
    branches:
      - main
      - dev
  workflow_dispatch:

env:
  BUCKET_NAME: marketbase-web
  DEV_APP_NAME: Marketbase-web-dev
  MAIN_APP_NAME: marketbase-web-prod-2
  DEV_ENV_NAME: Marketbase-web-dev-env
  MAIN_ENV_NAME: marketbase-web-prod-2-env

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:

    - name: Checkout code
      uses: actions/checkout@v2

    - name: Generate Last Updated Timestamp
      run: echo "NEXT_PUBLIC_LAST_UPDATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > .env.local
      working-directory: ./

    - name: Log environment variables
      run: |
        echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}"
        echo "NEXT_PUBLIC_STANDARD_PRICE_ID=${{ secrets.NEXT_PUBLIC_STANDARD_PRICE_ID }}"

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '18'

    - name: Install dependencies
      run: npm install

    - name: Build Next.js project
      run: |
        if [ "${{ github.ref }}" == "refs/heads/dev" ]; then
          export NEXT_PUBLIC_API_URL=${{ secrets.DEV_NEXT_PUBLIC_API_URL }}
          export NEXT_PUBLIC_STANDARD_PRICE_ID=${{ secrets.DEV_NEXT_PUBLIC_STANDARD_PRICE_ID }}
          export NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.DEV_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
          export NEXT_PUBLIC_M_API_KEY=${{ secrets.DEV_NEXT_PUBLIC_M_API_KEY }}
          export NEXT_PUBLIC_CLOUDFRONT_URL=${{secrets.DEV_NEXT_PUBLIC_CLOUDFRONT_URL}}
          export NEXT_PUBLIC_ENVIRONMENT=${{secrets.DEV_NEXT_PUBLIC_ENVIRONMENT}}
        else
          export NEXT_PUBLIC_API_URL=${{ secrets.PROD_NEXT_PUBLIC_API_URL }}
          export NEXT_PUBLIC_STANDARD_PRICE_ID=${{ secrets.PROD_NEXT_PUBLIC_STANDARD_PRICE_ID }}
          export NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.PROD_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
          export NEXT_PUBLIC_M_API_KEY=${{ secrets.PROD_NEXT_PUBLIC_M_API_KEY }}
          export NEXT_PUBLIC_CLOUDFRONT_URL=${{secrets.PROD_NEXT_PUBLIC_CLOUDFRONT_URL}}
          export NEXT_PUBLIC_ENVIRONMENT=${{secrets.PROD_NEXT_PUBLIC_ENVIRONMENT}}

        fi
        npm run build
      env:
        NEXT_PUBLIC_API_URL: ${{ env.NEXT_PUBLIC_API_URL }}
        NEXT_PUBLIC_STANDARD_PRICE_ID: ${{ env.NEXT_PUBLIC_STANDARD_PRICE_ID }}
        NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: ${{ env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
        NEXT_PUBLIC_M_API_KEY: ${{ env.NEXT_PUBLIC_M_API_KEY }}
        NEXT_PUBLIC_CLOUDFRONT_URL: ${{ env.NEXT_PUBLIC_CLOUDFRONT_URL }}
        NEXT_PUBLIC_ENVIRONMENT: ${{ env.NEXT_PUBLIC_ENVIRONMENT }}

    - name: Cache
      uses: actions/cache@v3
      with:
        # See here for caching with `yarn` https://github.com/actions/cache/blob/main/examples.md#node---yarn or you can leverage caching with actions/setup-node https://github.com/actions/setup-node
        path: |
          ~/.npm
          ${{ github.workspace }}/.next/cache
        # Generate a new cache whenever packages or source files change.
        key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
        # If source files changed but packages didn't, rebuild from a prior cache.
        restore-keys: |
          ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-        

    - name: Generate version label
      run: echo "VERSION_LABEL=$(git rev-parse --short HEAD)-$(date +%s)" >> $GITHUB_ENV

    - name: Zip the application
      run: zip -r ${VERSION_LABEL}.zip . -x '*.git*' 'node_modules/*'

    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1 # Change to your desired region

    - name: Deploy to Dev Elastic Beanstalk on commit to dev branch
      if: github.ref == 'refs/heads/dev'
      run: |
        aws s3 cp ${VERSION_LABEL}.zip s3://$BUCKET_NAME/dev/${VERSION_LABEL}.zip
        aws elasticbeanstalk create-application-version --application-name $DEV_APP_NAME --version-label ${VERSION_LABEL} --source-bundle S3Bucket="$BUCKET_NAME",S3Key="dev/${VERSION_LABEL}.zip"
        aws elasticbeanstalk update-environment --environment-name $DEV_ENV_NAME --version-label ${VERSION_LABEL}

    - name: Deploy to Main Elastic Beanstalk on commit to main branch
      if: github.ref == 'refs/heads/main'
      run: |
        aws s3 cp ${VERSION_LABEL}.zip s3://$BUCKET_NAME/main/${VERSION_LABEL}.zip
        aws elasticbeanstalk create-application-version --application-name $MAIN_APP_NAME --version-label ${VERSION_LABEL} --source-bundle S3Bucket="$BUCKET_NAME",S3Key="main/${VERSION_LABEL}.zip"
        aws elasticbeanstalk update-environment --environment-name $MAIN_ENV_NAME --version-label ${VERSION_LABEL}

Config in AWS AppRunner:

AWS App Runner Config

next.config.js:

module.exports = {
    env: {
      NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'default',
      NEXT_PUBLIC_CLOUDFRONT_URL: process.env.NEXT_PUBLIC_CLOUDFRONT_URL || 'default',
      NEXT_PUBLIC_ENVIRONMENT: process.env.NEXT_PUBLIC_ENVIRONMENT || 'default',
      NEXT_PUBLIC_LAST_UPDATED: process.env.NEXT_PUBLIC_LAST_UPDATED || 'default',
      NEXT_PUBLIC_M_API_KEY: process.env.NEXT_PUBLIC_M_API_KEY || 'default',
      NEXT_PUBLIC_STANDARD_PRICE_ID: process.env.NEXT_PUBLIC_STANDARD_PRICE_ID || 'default',
      NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || 'default',
    },
  };

I've even tried using a manual apprunner file and it still will not include the environment variables in the build (they actually disappear from the AppRunner UI until I switch back to the configuration as opposed to apprunner.yaml file.


Solution

  • While I have not been able to directly answer the above, my workaround to deploying Next.JS on AppRunner has been to have github actions deploy to ECR and then use the container in AppRunner as opposed to the source reposistory.

    GH Actions and Dockerfile for reference.

    Github actions (root/.github/workflows/deploy.yml)

    name: Build and Push Docker Image to ECR
    
    on:
      push:
        branches:
          - main
          - dev
      workflow_dispatch:
    
    env:
      AWS_REGION: us-east-1  # Change to your AWS region
      ECR_REPOSITORY_NAME_DEV: your-ecr-name-space/your-service-dev  ECR_REPOSITORY_NAME_MAIN: your-ecr-name-space/your-service-main
    
    jobs:
      build-and-push:
        runs-on: ubuntu-latest
    
        steps:
          # 1. Checkout the repository
          - name: Checkout code
            uses: actions/checkout@v2
    
          # 2. Generate Last Updated Timestamp
          - name: Generate Last Updated Timestamp
            run: echo "NEXT_PUBLIC_LAST_UPDATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" > .env.local
    
          # 3. Set environment variables based on branch using repository variables
          - name: Set environment variables based on branch
            run: |
              if [ "${{ github.ref }}" == "refs/heads/dev" ]; then
                echo "NEXT_PUBLIC_VAR=${{ vars.DEV_NEXT_PUBLIC_VAR}}" >> $GITHUB_ENV
              else
                echo "NEXT_PUBLIC_VAR=${{ vars.PROD_NEXT_PUBLIC_VAR }}" >> $GITHUB_ENV
              fi
    
          # 4. Set up Node.js
          - name: Set up Node.js
            uses: actions/setup-node@v2
            with:
              node-version: '18'
    
          # 5. Install dependencies
          - name: Install dependencies
            run: npm install
    
          # 6. Build Next.js project
          - name: Build Next.js project
            run: npm run build
    
          # 7. Configure AWS Credentials
          - name: Configure AWS Credentials
            uses: aws-actions/configure-aws-credentials@v2
            with:
              aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
              aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
              aws-region: ${{ env.AWS_REGION }}
    
          # 8. Get AWS Account ID
          - name: Get AWS Account ID
            id: aws_account
            run: echo "AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)" >> $GITHUB_ENV
    
          # 9. Set ECR repository URI
          - name: Set ECR repository URI
            run: |
              echo "ECR_REPOSITORY_URI=${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY_NAME }}" >> $GITHUB_ENV
    
          # 10. Login to Amazon ECR
          - name: Login to Amazon ECR
            uses: aws-actions/amazon-ecr-login@v1
    
          # 11. Build Docker image
          - name: Build Docker image
            run: |
              docker build -t $ECR_REPOSITORY_URI:${{ github.sha }} \
                .
    
          # 12. Push Docker image to Amazon ECR
          - name: Push Docker image to Amazon ECR
            run: |
              docker push $ECR_REPOSITORY_URI:${{ github.sha }}
    
          # 13. (Optional) Tag the image as 'latest'
          - name: Tag and push latest Docker image
            run: |
              docker tag $ECR_REPOSITORY_URI:${{ github.sha }} $ECR_REPOSITORY_URI:latest
              docker push $ECR_REPOSITORY_URI:latest
    

    Dockerfile (no extension) in root of app

    # Use the official Node.js 18 image as the base
    FROM node:18
    
    # Set the working directory inside the container
    WORKDIR /app
    
    # Accept build arguments for environment-specific configurations
    ARG NEXT_PUBLIC_VAR
    
    # Set environment variables based on the build arguments
    ENV NEXT_PUBLIC_VAR=${NEXT_PUBLIC_VAR}
    
    # Copy package.json and package-lock.json to leverage Docker cache
    COPY package*.json ./
    
    # Install project dependencies
    RUN npm install
    
    # Copy the rest of the application code to the container
    COPY . .
    
    # Build the Next.js application for production
    RUN npm run build
    
    # Expose port 3000 to allow external access
    EXPOSE 3000
    
    # Define the default command to run the application
    CMD ["npm", "start"]