Pulumiを使って、Next.jsページをホスティングしてみた

2022.04.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。データアナリティクス事業本部 サービスソリューション部の北川です。

業務内でpulumiを使用することになったので、今回はNext.jsで作成したページのホスティングを実行してみました。

pulumiとは

Pulumiはインフラ構築・運用をコード化するツール、Infrastructure as Codeの一つです。IaCでは、CloudFormationや、Terraformが有名ですよね。 言語は、TypeScript、JavaScript、Python、Go、C#をサポートしています。

今回、AWS Profileとpulumiの紐付けに関しては、記述しません。 こちらに関して、チームメンバーがエントリしている以下の記事が参考になると思います。

PulumiのGet Started with Google Cloudを試してみた

プロジェクト作成

早速、プロジェクトを作成していきたいと思います

mkdir pulumi-nextjs-project && cd pulumi-nextjs-project

Next.js用のフォルダ作成

初めに、Next.js用のフォルダを作成していきます。

mkdir next && cd next

Next.jsのセットアップ。

$ npx create-next-app --ts .

ローカルで起動

$ yarn dev

Next.jsを使用したことがある方なら、お馴染みの画面が表示されます。

package.jsonを変更

  "scripts": {
    "dev": "next dev",
    "build": "next build && next export",
    "start": "next start",
    "lint": "next lint"
  },

next exportを使用すると、outディレクトリが生成され、静的ホスティングサービスで使用することができます。

しかし、next/imageはnext exportではサポートされていないらしく、このままyarn buildをするとエラーが出ます。趣旨とずれてしまうので、今回はindex.tsx内のImageタグは削除します。

/next/pages/index.tsx

import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          Welcome to <a href="https://nextjs.org">Next.js!</a>
        </h1>

        <p className={styles.description}>
          Get started by editing{' '}
          <code className={styles.code}>pages/index.tsx</code>
        </p>

        <div className={styles.grid}>
          <a href="https://nextjs.org/docs" className={styles.card}>
            <h2>Documentation &rarr;</h2>
            <p>Find in-depth information about Next.js features and API.</p>
          </a>

          <a href="https://nextjs.org/learn" className={styles.card}>
            <h2>Learn &rarr;</h2>
            <p>Learn about Next.js in an interactive course with quizzes!</p>
          </a>

          <a
            href="https://github.com/vercel/next.js/tree/canary/examples"
            className={styles.card}
          >
            <h2>Examples &rarr;</h2>
            <p>Discover and deploy boilerplate example Next.js projects.</p>
          </a>

          <a
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
            className={styles.card}
          >
            <h2>Deploy &rarr;</h2>
            <p>
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </a>
        </div>
      </main>

      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
        </a>
      </footer>
    </div>
  )
}

export default Home

ビルドを実行します。

$ yarn build

成功すると、nextフォルダの配下にoutディレクトリが作成されます。

Next.js側の設定は以上です。

pulumi-next-projectに戻ります。

$ cd ..

Pulumiの設定

Pulumi側のフォルダを作成

$ mkdir pulumi && cd pulumi

pulumiのセットアップ。今回はtypeScriptで書いていきます。ディレクトリの中が空でないとエラーになります。

$ pulumi new aws-typescript —name pulumi-nextjs-project

プロジェクト名、説明、スタック名、リージョンの記述を求められます。今回、リージョンはap-northeast-1に変更し、他はデフォルトにしました。

作成されたindex.tsを変更し、S3バケットを作成します。

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
import * as mime from "mime";
import * as fs from "fs";

let siteDir = "./frontend/out";

let siteBucket = new aws.s3.Bucket("s3-website-bucket", {
  tags: {
    owner: "cm-kitagawa.keita",
  },
});

const crawlDirectory = (dir: string, f: (_: string) => void) => {
  const files = fs.readdirSync(dir);
  for (const file of files) {
    const filePath = `${dir}/${file}`;
    const stat = fs.statSync(filePath);
    if (stat.isDirectory()) {
      crawlDirectory(filePath, f);
    }
    if (stat.isFile()) {
      f(filePath);
    }
  }
};

// 指定のpathの中身をS3に同期させます。
crawlDirectory(siteDir, (filePath: string) => {
  const relativeFilePath = filePath.replace(siteDir + "/", "");
  const contentFile = new aws.s3.BucketObject(
    relativeFilePath,
    {
      key: relativeFilePath,
      acl: "public-read",
      bucket: siteBucket,
      contentType: mime.getType(filePath) || undefined,
      source: new pulumi.asset.FileAsset(filePath),
    },
    {
      parent: siteBucket,
    }
  );
});

export const bucketName = siteBucket.bucket;

スタックをデプロイします。

$ pulumi up

実行した際に作成される、スタックの中身が表示されます。 スタックの更新を実行するか聞かれますので、問題がなければ「yes」を選択します。

Do you want to perform this update?  [Use arrows to move, enter to select, type to filter]
> yes
  no
  details

$ pulumi stack outputで作成されたスタックを出力します。

$ pulumi stack output
Current stack outputs (1):
    OUTPUT         VALUE
    bucketName  s3-website-bucket-bf4d365

AWSのコンソール画面でも、S3が作成されていることを確認します。

nextの方で作成したoutフォルダの中身も、アップロードされています。

静的ウェブサイトホスティングを有効化

次にS3のホスティング機能を有効化します。また、オブジェクトを公開して読み取り可能にするため、 バケットポリシーをを作成し、適用させます。 index.tsを変更します。

pulumi/index.ts

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";
import * as mime from "mime";
import * as fs from "fs";

let siteDir = "../next/out";

let siteBucket = new aws.s3.Bucket("s3-website-bucket", {
  // ホスティングを有効化
  website: {
    indexDocument: "index.html",
  },
  tags: {
    owner: "cm-kitagawa.keita",
  },
});

const crawlDirectory = (dir: string, f: (_: string) => void) => {
  const files = fs.readdirSync(dir);
  for (const file of files) {
    const filePath = `${dir}/${file}`;
    const stat = fs.statSync(filePath);
    if (stat.isDirectory()) {
      crawlDirectory(filePath, f);
    }
    if (stat.isFile()) {
      f(filePath);
    }
  }
};

crawlDirectory(siteDir, (filePath: string) => {
  const relativeFilePath = filePath.replace(siteDir + "/", "");
  const contentFile = new aws.s3.BucketObject(
    relativeFilePath,
    {
      key: relativeFilePath,
      acl: "public-read",
      bucket: siteBucket,
      contentType: mime.getType(filePath) || undefined,
      source: new pulumi.asset.FileAsset(filePath),
    },
    {
      parent: siteBucket,
    }
  );
});

const publicReadPolicyForBucket = (bucketName: string) => {
  return JSON.stringify({
    Version: "2012-10-17",
    Statement: [
      {
        Effect: "Allow",
        Principal: "*",
        Action: ["s3:GetObject"],
        Resource: [
          `arn:aws:s3:::${bucketName}/*`,
        ],
      },
    ],
  });
}

// バケット内のオブジェクトのパブリックアクセスを許可する
let bucketPolicy = new aws.s3.BucketPolicy("bucketPolicy", {
  bucket: siteBucket.bucket,
  policy: siteBucket.bucket.apply(publicReadPolicyForBucket),
});

export const bucketName = siteBucket.bucket;
export const websiteUrl = siteBucket.websiteEndpoint;

再度スタックをデプロイします。

$ pulumi up

スタックを出力します。

$ pulumi stack output
Current stack outputs (2):
    OUTPUT      VALUE
    bucketName  s3-website-bucket-bf4d365
    websiteUrl  s3-website-bucket-bf4d365.s3-website-ap-northeast-1.amazonaws.com

websiteUrlの値をコピーし、アクセスします。

Next.jsのWelcomeページが表示されました。

まとめ

今回は、Next.jsで作成したプロジェクトをpulumiを使ってホスティングしてみました。 Pulumiについての記事が少なく、思っていたより苦戦しましたが、おかげで自走力が高められ、いい勉強になりました。

現状は別ページでのリロード時、マッピングが動作しないなどの問題があるので、次回はその辺りも改善できればと思います。

ではまた。