はじめに
以前 ヘッドレス CMS(MicroCMS)を利用し、CloudFront + S3 へデプロイしました。
しかし、Next.js(SSG) は静的ファイル(HTML)を生成しなければならないため、MicroCMS で更新を行うたびに、「ビルド」→「デプロイ」が必要です。
そこで、AWS CodePipeline を利用して、15分おきなど定時に「ビルド」→「デプロイ」を行う方法を調べました。
対象読者
- CI/CD を試してみたい方
- AWS CodePipeline を始めてみたい方
- 構成の各サービス紹介や役割を知りたい方
構成図
下記 1 から 3 を AWS CodePipeline に登録します。
- 開発者が AWS CodeCommit にプッシュ処理(Git push)を行います。
- プッシュをきっかけに AWS CodeBuild が MicroCMS からデータを取得し、静的ファイル(HTML)を生成します。
- S3DeployAction 経由で静的ファイルを S3 にデプロイします。
- Git push があれば即座にビルド → デプロイを実行します。
- その他に 15 分ごとのスケジュール実行でもビルド → デプロイを実行します。
CI/CD で利用するサービス紹介
| サービス名 | 用途 | 月額料金 (USD) |
料金単位 |
|---|---|---|---|
| AWS CodeCommit | ソースコードの管理 | 1.00 | アクティブユーザー数 |
| AWS CodeBuild | ユニットテストやビルドの実行 | 0.005 | 1 分あたりの料金 |
| AWS CodePipeline | CI/CD パイプラインの構築 | 0.002 | 1 分あたりの実行時間 |
※月額料金は 2024 年 3 月 6 日時点で試算しています。
各サービスには無料枠があります。
試してみました
事前準備
実行環境は前回のとおりです。
ステップ 5 の「CDK環境の初期化(ブートストラップ)」まで終わっていることを想定しています。
ステップ1:CloudFront + S3 の作成
前回のステップ6からデプロイまでを行ってください。
バケット名などは適宜設定してください。
既に用意している方は次のステップへ進んでください。
ステップ2:CDK ファイル作成とデプロイ
2-1. 作業ディレクトリの作成
cdk-codepipeline-s3 というディレクトリを作成しています。
お好きな名前に変更してください。
> mkdir cdk-codepipeline-s3 && cd cdk-codepipeline-s3
cdk-codepipeline-s3>
2-2. CDK プロジェクトの作成
cdk-codepipeline-s3> cdk init app --language=typescript
cdk-codepipeline-s3> npm install dotenv --save
※成功すると cdk-codepipeline-s3 内にいろいろファイルが作られます。
2-3. スタック(Stack)の修正
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
export interface PipelineStackProps extends cdk.StackProps {
/**
* 既存のディストリビューション ID
*/
existingDistributionId: string;
/**
* 既存の S3 バケット名
*/
existingBucketName: string;
/**
* AWS CodeCommit リポジトリ名
* @default 'my-web-app-repo'
*/
repositoryName?: string;
/**
* ブランチ名
* @default 'main'
*/
branchName?: string;
/**
* AWS CodePipeline 名
* @default 'WebAppPipeline'
*/
pipelineName?: string;
/**
* 既存の AWS CodeCommit リポジトリを使用するかどうか
* @default false (新規作成)
*/
useExistingRepository?: boolean;
}
export class PipelineStack extends cdk.Stack {
public readonly repository: codecommit.IRepository;
public readonly bucket: s3.IBucket;
public readonly pipeline: codepipeline.Pipeline;
constructor(scope: Construct, id: string, props: PipelineStackProps) {
super(scope, id, props);
// existingDistributionId が存在することを確認
if (!props.existingDistributionId) {
throw new Error('existingDistributionId is required');
}
// existingBucketName が存在することを確認
if (!props.existingBucketName) {
throw new Error('existingBucketName is required');
}
// デフォルト値の設定
const repositoryName = props?.repositoryName || 'my-web-app-repo';
const branchName = props?.branchName || 'main';
const pipelineName = props?.pipelineName || 'WebAppPipeline';
const useExistingRepository = props?.useExistingRepository ?? false;
// ========================================
// 既存の S3 バケットを参照
// ========================================
this.bucket = s3.Bucket.fromBucketName(this, 'ExistingBucket', props.existingBucketName);
// ========================================
// AWS CodeCommit リポジトリ
// ========================================
if (useExistingRepository) {
// 既存のリポジトリを参照
this.repository = codecommit.Repository.fromRepositoryName(
this,
'Repository',
repositoryName
);
} else {
// 新規リポジトリを作成
this.repository = new codecommit.Repository(this, 'Repository', {
repositoryName: repositoryName,
description: `Repository for ${pipelineName}`,
});
}
// ========================================
// AWS CodeBuild プロジェクト
// ========================================
const buildProject = new codebuild.PipelineProject(this, 'BuildProject', {
projectName: `${pipelineName}-Build`,
description: 'Build project for static website',
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
computeType: codebuild.ComputeType.SMALL,
environmentVariables: {
DISTRIBUTION_ID: {
value: props.existingDistributionId || '',
},
},
},
buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec.yml'),
cache: codebuild.Cache.local(codebuild.LocalCacheMode.SOURCE),
});
// AWS CodeBuild に S3 への書き込み権限を付与
this.bucket.grantReadWrite(buildProject);
// AWS CodeBuild に CloudFront キャッシュクリア権限を付与
if (buildProject.role) {
buildProject.role.addToPrincipalPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['cloudfront:CreateInvalidation'],
resources: ['*'],
})
);
}
// ========================================
// AWS CodePipeline
// ========================================
this.pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
pipelineName: pipelineName,
pipelineType: codepipeline.PipelineType.V2,
crossAccountKeys: false, // 単一アカウント内での使用の場合
});
// Source Stage
const sourceOutput = new codepipeline.Artifact('SourceOutput');
this.pipeline.addStage({
stageName: 'Source',
actions: [
new codepipeline_actions.CodeCommitSourceAction({
actionName: 'CodeCommit_Source',
repository: this.repository,
branch: branchName,
output: sourceOutput,
trigger: codepipeline_actions.CodeCommitTrigger.EVENTS, // プッシュ時に自動実行
}),
],
});
// Build Stage
const buildOutput = new codepipeline.Artifact('BuildOutput');
this.pipeline.addStage({
stageName: 'Build',
actions: [
new codepipeline_actions.CodeBuildAction({
actionName: 'CodeBuild',
project: buildProject,
input: sourceOutput,
outputs: [buildOutput],
}),
],
});
// Deploy Stage
this.pipeline.addStage({
stageName: 'Deploy',
actions: [
new codepipeline_actions.S3DeployAction({
actionName: 'S3_Deploy',
input: buildOutput,
bucket: this.bucket,
extract: true, // ZIP ファイルを展開してデプロイ
}),
],
});
// ========================================
// Schedule Trigger (15 minutes)
// ========================================
const pipelineTarget = new targets.CodePipeline(this.pipeline);
new events.Rule(this, 'ScheduleRule', {
schedule: events.Schedule.rate(cdk.Duration.minutes(15)),
targets: [pipelineTarget],
description: 'Trigger the pipeline every 15 minutes',
});
// ========================================
// Outputs
// ========================================
new cdk.CfnOutput(this, 'RepositoryCloneUrlHttp', {
description: 'AWS CodeCommit repository clone URL (HTTP)',
value: this.repository.repositoryCloneUrlHttp,
});
new cdk.CfnOutput(this, 'RepositoryCloneUrlSsh', {
description: 'AWS CodeCommit repository clone URL (SSH)',
value: this.repository.repositoryCloneUrlSsh,
});
new cdk.CfnOutput(this, 'WebsiteURL', {
description: 'S3 website URL',
value: this.bucket.bucketWebsiteUrl,
});
new cdk.CfnOutput(this, 'BucketName', {
description: 'S3 bucket name',
value: this.bucket.bucketName,
});
new cdk.CfnOutput(this, 'PipelineName', {
description: 'AWS CodePipeline name',
value: this.pipeline.pipelineName,
});
}
}
2-4. アプリ(App)
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { PipelineStack } from '../lib/cdk-codepipeline-s3-stack';
import * as dotenv from 'dotenv';
// .env ファイルを読み込む
dotenv.config();
const app = new cdk.App();
// 環境変数から値を取得
const existingDistributionId = process.env.EXISTING_DISTRIBUTION_ID;
const existingBucketName = process.env.EXISTING_BUCKET_NAME;
const repositoryName = process.env.REPOSITORY_NAME;
const branchName = process.env.BRANCH_NAME || 'main';
const pipelineName = process.env.PIPELINE_NAME || 'ScheduledS3DeployPipeline';
const useExistingRepository = process.env.USE_EXISTING_REPOSITORY?.toLowerCase() === 'true' || false;
const ProjectName = process.env.TAGS_PROJECT || 'WebApp';
const EnvironmentName = process.env.TAGS_ENVIRONMENT || 'Development';
// 必須項目のチェック
if (!existingDistributionId || !existingBucketName || !repositoryName) {
throw new Error('EXISTING_DISTRIBUTION_ID and EXISTING_BUCKET_NAME and REPOSITORY_NAME must be defined in .env file');
}
// スタックの作成
new PipelineStack(app, 'CodePipelineStack', {
// スタックの設定
existingDistributionId: existingDistributionId,
existingBucketName: existingBucketName,
repositoryName: repositoryName,
branchName: branchName,
pipelineName: pipelineName,
useExistingRepository: useExistingRepository,
// スタックの説明
description: 'AWS CodePipeline stack with AWS CodeCommit, AWS CodeBuild, and S3 deployment',
// タグの設定(オプション)
tags: {
Project: ProjectName,
Environment: EnvironmentName,
},
});
app.synth();
2-5. 環境変数
# 既存のディストリビューション ID
EXISTING_DISTRIBUTION_ID=your-distribution-id
# 既存の S3 バケット名
EXISTING_BUCKET_NAME=your-existing-bucket-name
# 既存リポジトリを使用有無 (デフォルト: false)
# 既存リポジトリを使用する場合は true に設定
USE_EXISTING_REPOSITORY=false
# AWS CodeCommit リポジトリ名
REPOSITORY_NAME=your-repository-name
# ブランチ名 (デフォルト: main)
BRANCH_NAME=main
# パイプライン名 (デフォルト: ScheduledS3DeployPipeline)
PIPELINE_NAME=ScheduledS3DeployPipeline
# タグ
# プロジェクトタグ名
TAGS_PROJECT=WebApp
# 環境タグ名
TAGS_ENVIRONMENT=Development
環境に合わせて変更してください。
2-6. デプロイ
cdk-codepipeline-s3> cdk deploy
時間がかかりますので気長に待ちましょう!
ステップ3:ローカルでリポジトリをクローンして初期コミットを作成
3-1. 事前情報
AWS CodeCommit リポジトリの URL と AWS CodeCommit リポジトリへのログイン情報を確認します。
AWS CodeCommit リポジトリの URL
-
AWSマネジメントコンソールにログイン
AWS CodeCommit リポジトリへのログイン情報
-
AWSマネジメントコンソールにログイン
-
「ユーザー」から該当ユーザーをクリック
「ユーザー名」と「パスワード」は適切に管理してください。
3-2. 初期コミット
下記コマンドを実行し、初回コミットを行ってください。
お好きなディレクトリで作業してください。
以下例ではデスクトップで作業している想定です。
> cd Desktop
> git clone [[AWS CodeCommit リポジトリの URL]]
# ここで「ユーザー名」と「パスワード」の入力が求められます。
# 1. リポジトリディレクトリに移動
> cd my-cdk-app
# 2. ファイルを追加
my-cdk-app> echo "# My Web App" > README.md
# 3. Git に追加
my-cdk-app> git add .
# 4. コミット
my-cdk-app> git commit -m "Initial commit"
# 5. ブランチを main に変更(オプション:既に main の場合は不要)
my-cdk-app> git branch -M main
# 6. リモートにプッシュ
my-cdk-app> git push -u origin main
過去に同じ名前のリポジトリで作業していた場合、git clone でパスワードが聞かれず失敗する場合があります。
おそらく Windows11 で、パスワード保存されている可能性がありますので、参考記事の手順どおり、AWS CodeCommit リポジトリ URL の情報を削除して試してみてください。
ステップ4:コミットファイルの作成
4-1. Next.js
上記 3-2 で作成されたリポジトリ内に前回記事を参考に、MicroCMS + Next.js(SSG) を作成してください。
npm run build まで行いました。
4-2. buildspec.yml
新規で下記ファイルを作成します。
version: 0.2
# AWS CodeBuild のビルド仕様ファイル
# このファイルは AWS CodeCommit リポジトリのルートに配置してください
phases:
install:
# ランタイムのバージョンを指定
runtime-versions:
nodejs: 20
commands:
- echo "Installing dependencies..."
- npm --version
- node --version
pre_build:
commands:
- echo "Running pre-build commands..."
- npm ci # package-lock.json を使用してクリーンインストール
build:
commands:
- echo "Building the application..."
- npm run build
- echo "Build completed on $(date)"
post_build:
commands:
- echo "Running post-build commands..."
- echo "Listing build output..."
- |
if [ ! -z "$DISTRIBUTION_ID" ]; then
aws cloudfront create-invalidation \
--distribution-id $DISTRIBUTION_ID \
--paths "/*"
echo "CloudFront cache invalidation initiated"
else
echo "DISTRIBUTION_ID not set, skipping cache invalidation"
fi
# ビルド成果物の設定
artifacts:
files:
- '**/*'
base-directory: out # ビルド出力ディレクトリ(プロジェクトに応じて変更してください)
discard-paths: no
# キャッシュ設定(ビルド時間を短縮)
cache:
paths:
- 'node_modules/**/*'
next.config.js に以下の設定を追加してください。
export default {
output: 'export', // out/ ディレクトリに出力
}
ステップ5:CI/CD の動作確認
5-1. 環境(.env)ファイルのコミット
今回は AWS CodeCommit に直接作成していますが、.env ファイルに記載している MicroCMS の API キーのような秘匿情報は、AWS Systems Manager Parameter Store や AWS Secrets Manager に保存し、CodeBuild の環境変数として注入する方法をお勧めします。
- AWSマネジメントコンソールにログイン
- AWS CodeCommit サービスにアクセス
- 「リポジトリ」から作成したリポジトリ(例では「my-cdk-app」)をクリック
- 右上の「ファイルの追加」を展開し、「ファイルの作成」をクリック
- 「環境変数ファイルの作成」を参考に .env ファイルを作成
NEXT_PUBLIC_MICROCMS_SERVICE_ID=your_service_id
NEXT_PUBLIC_MICROCMS_API_KEY=your_api_key
5-2. Next.js ファイルのコミット
下記コマンドを実行し、初回コミットを行ってください。
# 1. Git に追加
my-cdk-app> git add .
# 2. コミット(コメントはお好きに変えてください。)
my-cdk-app> git commit -m "Add Next.js project files"
# 3. リモートにプッシュ
my-cdk-app> git push -u origin main
5-3. AWS CodePipeline の動作確認
AWS CodePipeline のパイプラインから、AWS CodeCommit → AWS CodeBuild → S3DeployAction が成功しているか確認

5-4. デプロイ結果その1
ディストリビューションドメイン名をブラウザに入力すると、MicroCMS のデータが表示されます。

5-5. デプロイ結果その2
MicroCMS のデータを更新します。15 分後、再デプロイが実行され、更新したデータが表示されます。
※テストでは「〇〇」を追加しました。

まとめ
今回は CDK で CI/CD を実行してみました。
CDK では AWS CodeCommit リポジトリ作成時に default ブランチが自動的に生成されるため、buildspec.yml の設定や環境変数の指定に注意が必要でした・・・
なお記事では 15 分おきにデプロイと CloudFront のキャッシュクリアを行っています。
パイプランの実行時間やキャッシュクリアの必要性は要件によってお試しください。
この記事が、誰かのお役に立てば幸いです。





