Technote

by sizuhiko

Next.js で作ったアプリケーションを AppRunner にデプロイする

こちらの記事は AppRunner へのデプロイは cdk でサクッとできるのか? の続編となります。 前編を読まないとわからない内容ではないですが、もし良ければ事前に確認してください。

まず前編でも触れてますが、アプリケーションのリポジトリ構成は以下のようになっています。

  • Next.js のアプリケーションリポジトリ
  • AWSのリソースを管理するインフラリポジトリ

アプリのリポジトリでは、ECR へのデプロイまでやってます。 インフラのリポジトリで、AppRunner など AWS リソースを CDK で構築しています。

AppRunner で Next.js アプリケーションを動かすには

Next.js を standalone モードでビルドして、Docker コンテナで起動する。これだけで ok です。

ECR に Next.js の standalone モードビルドしたコンテナイメージを push する

まずは Next.js のアプケーションリポジトリの CI/CD で ECR に Docker イメージを push します。 アプリケーションリポジトリ側にも CDK を入れてあるので、以下のようなコードで ECR にデプロイしています。 タグは package.jsonversion から入手します。

import * as imagedeploy from 'cdk-docker-image-deployment'
import * as ecr from 'aws-cdk-lib/aws-ecr'

export class DeployStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const projectRoot = path.join(__dirname, '../..')
    const { version } = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json')).toString())
    const repository = ecr.Repository.fromRepositoryName(this, 'webapp-ect', 'webapp')
    new imagedeploy.DockerImageDeployment(this, 'DockerImageDeployment', {
      source: imagedeploy.Source.directory(projectRoot),
      destination: imagedeploy.Destination.ecr(repository, {
        tag: version,
      }),
    })

Dockerfile は Next.js の公式サンプルWith Docker を参考に(というかほぼそのまま流用)すれば大丈夫です。 ベースイメージの Node.js バージョンが古かったりするので、そこは自分たちが使うバージョンに変更しておきましょう。

AppRunner で ECR からデプロイする

こちらは前編でも触れた @aws-cdk/aws-apprunner-alpha が使えるので簡単にデプロイできます。 以下のような感じで書けば良いでしょう。ヘルスチェックを何でやるかは、いろいろだと思いますが、ここではいったん favicon にしています。 CDK で作成した ECR のリポジトリと、デプロイ対象のアプリケーションバージョンはコンストラクタの引数で渡せるようにしています。

import { Service, Source } from '@aws-cdk/aws-apprunner-alpha'

export class WebAppConstruct extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: WebAppConstructProps) {
    super(scope, id, props)

    const service = new Service(this, 'WebAppService', {
      source: Source.fromEcr({
        imageConfiguration: { port: 3000 },
        repository: props.ecrRepository,
        tagOrDigest: props.webAppVersion,
      }),
      serviceName: 'webapp-service',
      autoDeploymentsEnabled: true,
      healthCheck: HealthCheck.http({
        healthyThreshold: 2,
        interval: Duration.seconds(10),
        path: '/favicon.ico',
        timeout: Duration.seconds(10),
        unhealthyThreshold: 10,
      }),
    })

コラム: ヘルスチェックについて、いろいろと参考になった記事

実はヘルスチェックはちょっといろいろあって実際も favicon にしたんですけど、そのときに参考になった記事があるので、載せておきます

デプロイできたので動かしてみるが動かない

Error: listen EADDRNOTAVAIL: address not available 10.0.1.2:3000

何か動きません。

あれ、Dockerfile に HOSTNAME=0.0.0.0 入ってるのに何でだろう? ローカルで build したイメージ動かしたときは大丈夫だったんだけど?と思ったら、1つ罠があります。

こちらの記事がとても参考になりました。 AWS App RunnerでNext.jsのstandaloneモードを動かす時のTips(ないしは失敗談)

App Runnerの環境では環境変数HOSTNAMEに対して、暗黙的にアタッチされているENIのprivate DNSが指定されるようです。

なんですって? AppRunner が環境変数にセットしてくるの??

だから 10.0.1.2:3000 みたいな ENI の private アドレスになるんですね。Dockerfile では

ENV HOSTNAME="0.0.0.0"
CMD ["node",  "server.js"]

のように指定されていても、コンテナ起動時に HOSTNAME を指定されるので環境変数が上書きされてセットされます。

CDK で HOSTNAME を設定する

ということで、AppRunner のサービスを作るときに環境変数 HOSTNAME を指定してあげます。

    const service = new Service(this, 'WebAppService', {
      source: Source.fromEcr({
        imageConfiguration: {
            port: 3000,
            environmentVariables: {
                HOSTNAME: '0.0.0.0',
            }
        },

これでデプロイしたら無事起動しました。👏👏

少しでも AppRunner で Next.js アプリを動かす人の役にたてばと思います。