この記事は公開されてから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 →</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 →</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 →</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 →</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についての記事が少なく、思っていたより苦戦しましたが、おかげで自走力が高められ、いい勉強になりました。
現状は別ページでのリロード時、マッピングが動作しないなどの問題があるので、次回はその辺りも改善できればと思います。
ではまた。