App RunnerにExpressアプリをデプロイする

ECR+App RunnerをCDKデプロイする - dyoshikawa’s blog

をさらに実戦に近づけるために、今度はNode.js Expressアプリをデプロイしてみる。

環境

  • M1 Mac Big Sur
  • node 16.15.0
  • typescript 5.0.2
  • aws-cdk-lib 2.69.0
  • constructs 10.1.281
  • @aws-cdk/aws-apprunner-alpha 2.69.0-alpha.0
  • cdk-docker-image-deployment 0.0.195
  • esbuild 0.17.12

コード

  1. esbuildでバンドルJSファイル生成
  2. 1のバンドルファイルを入れたDockerイメージをビルドする
  3. 2のDockerイメージをECRリポジトリに上げる
  4. 3のECRリポジトリを参照するApp Runnerサービスを起動

という方針。

CDKコード:

// lib/app-runner-stack-express.ts
import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as apprunner from "@aws-cdk/aws-apprunner-alpha";
import * as imagedeploy from "cdk-docker-image-deployment";

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

    const repository = new cdk.aws_ecr.Repository(this, "expressRepository", {
      repositoryName: "express-repository2",
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    new imagedeploy.DockerImageDeployment(this, "imageDeploy", {
      source: imagedeploy.Source.directory("./src/app-runner-express"),
      destination: imagedeploy.Destination.ecr(repository, {
        tag: "latest",
      }),
    });

    const service = new apprunner.Service(this, "apprunnerService", {
      source: apprunner.Source.fromEcr({
        imageConfiguration: { port: 3000 },
        repository,
        tagOrDigest: "latest",
      }),
    });

    new cdk.CfnOutput(this, "serviceUrl", {
      exportName: "serviceUrl",
      value: service.serviceUrl,
    });
  }
}

Dockerfile:

# src/app-runner-express/Dockerfile
FROM --platform=linux/amd64 node:18-slim

WORKDIR /app

COPY main.js main.js

CMD [ "node", "main.js" ]

Expressアプリケーションコード:

import express, { Request, Response } from "express";

const app = express();
const port = 3000;

app.get("/", (req: Request, res: Response) => {
  res.send("Hello, World!");
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

package.json:

  // package.json
  "scripts": {
    "build:app-runner": "esbuild src/app-runner-express/main.ts --platform=node --target=node18 --bundle --minify --outfile=./src/app-runner-express/main.js",
  },

esbuildでアプリケーションTSコードをJSファイルにバンドルするnpm scriptsを登録しておく。

あとはこんな感じでビルドとデプロイできる:

npm run build:app-runner && npx cdk deploy

ハマった点

最初、バンドルファイルを {PROJECT_ROOT}/dist/app-runner-express/main.js に吐くようにしていた:

  // package.json
  "scripts": {
    "build:app-runner": "esbuild src/app-runner-express/main.ts --platform=node --target=node18 --bundle --minify --outfile=./dist/app-runner-express/main.js",
  },

そしてDockerfileのCOPYは次のようにしていた:

# src/app-runner-express/Dockerfile
COPY ../../dist/app-runner-express/main.js main.js

これで docker build src/app-runner-express すると

 => ERROR [3/3] COPY ../../dist/app-runner-express/main.js main.js                                                                         0.0s
------
 > [3/3] COPY ../../dist/app-runner-express/main.js main.js:
------
failed to compute cache key: failed to walk /var/lib/docker/tmp/buildkit-mountxxxxxxx/dist/app-runner-express: lstat /var/lib/docker/tmp/buildkit-mountxxxxxxxx/dist/app-runner-express: no such file or directory

とエラー。

Dockerビルド時のContext設定を合わせることで解決できるようだったが、今回はシンプルにDockerfileの隣にバンドルファイルを吐くようにすることで対応した。

  // package.json
  "scripts": {
-   "build:app-runner": "esbuild src/app-runner-express/main.ts --platform=node --target=node18 --bundle --minify --outfile=./dist/app-runner-express/main.js",
+   "build:app-runner": "esbuild src/app-runner-express/main.ts --platform=node --target=node18 --bundle --minify --outfile=./src/app-runner-express/main.js",
  },
# src/app-runner-express/Dockerfile
- COPY ../../dist/app-runner-express/main.js main.js
+ COPY main.js main.js

参考