はじめに
現在の構成では、Next.jsアプリケーションを更新するたびに以下の作業を手動で行う必要があります。
- Dockerイメージのビルド
- ECRへのpush
- ECSへのデプロイ
これらの作業は手間がかかるだけでなく、作業ミスを招く原因にもなります。
そこで本記事では、CI/CDパイプラインを構築し、GitHubへのpushをトリガーとして
ECSへ自動デプロイを行う構成を紹介します。
また、単なるデプロイの自動化にとどまらず、
lintやtestによる最低限の品質チェックを通過した場合のみ
デプロイが実行される仕組みを構築します。
本シリーズの構成
1. VPC編
2. Route53 独自ドメイン編
3. ACM HTTPS化編
4. WAFセキュリティ編
5. ECS Auto Scaling編
6. CI/CD編(本記事)
CI/CD構築後の構成
前提条件
- 本シリーズの前回までの手順を実施していること
- Next.jsアプリプロジェクトにおいて、lintおよびtestによる品質チェックが可能な状態であること
-
npm run lint(静的解析) -
npm test(テストコードの実行)
-
- GitHubにNext.jsアプリのソースコードが配置されていること
CI/CDとは?
CI/CDは、アプリの変更を自動的にビルド・デプロイする仕組みです。
今回の構成では、GitHubへpushすると以下が実行されます。
- CodePipelineが変更を検知し、各処理を順に実行
- CodeBuildでlintおよびtestを実行しコード品質を確認
- CodeBuildでDockerイメージを作成しECRへpush
- CodePipelineがECS Serviceを更新(EcsDeployAction)
6-0. AWSコンソールにてGitHub接続を作成する
CodePipelineからGitHubのソースコードを取得するため、
あらかじめAWSコンソールでCodeStar Connectionsの接続を作成します。
手順
- AWSコンソールから CodePipeline → 接続 を開く
- 「接続を作成」をクリック
- プロバイダーで GitHub を選択
- 接続名を入力(例:
github-connection) - GitHubの認証・承認を行う
- 「GitHubに接続する」をクリックすると、GitHubの認可画面が表示されます
- 「Authorize AWS Connector for GitHub」をクリックして認可を行います
- 認可が完了するとAWSコンソールに戻り、接続ステータスが「利用可能」になることを確認します
- 作成した接続のARNを控えます
(例)
arn:aws:codestar-connections:ap-northeast-1:xxxxxxxxxxxx:connection/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
このARNは、後ほどCDKから利用します。
6-1. buildspec.ymlを作成する
Next.jsアプリのリポジトリ直下にbuildspec.ymlを作成します。
buildspec.ymlは、CodeBuildで実行する処理内容を定義するファイルです。
本構成では、lintおよびtestによる品質チェックを行ったうえで、
Dockerイメージを作成し、ECRへpushします。
version: 0.2
env:
shell: bash
phases:
install:
runtime-versions:
nodejs: 20
commands:
- echo "Installing dependencies..."
- npm ci
pre_build:
commands:
- echo "Running lint..."
- npm run lint
- echo "Running tests..."
- npm test
- echo "Logging in to Amazon ECR..."
- IMAGE_TAG=latest
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $REPOSITORY_URI
build:
commands:
- echo "Building Docker image..."
- docker build -t $REPOSITORY_URI:$IMAGE_TAG .
post_build:
commands:
- echo "Pushing Docker image..."
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo "Creating imagedefinitions.json..."
- |
printf '[{"name":"NextjsContainer","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
IMAGE_TAG=latest
今回はシンプルさを優先してlatestを使用しています。
ただし、latest のみで運用すると、どのバージョンがデプロイされているのか把握しづらくなります。
実運用では、コミットSHAやビルド番号などをタグに含めることで、
デプロイ対象の追跡性を高める構成を推奨します。
テストについて
本記事では、npm testを実行しています。
これにより、
- テスト失敗 → パイプライン停止
- 問題のあるコードはデプロイされない
という動作になります。
6-2. PipelineStackを作成する
CI/CD用のStackとして、GitHub → CodeBuild → ECSをつなぐ
CodePipelineを定義します。
本Stackでは、以下の処理を1つのパイプラインとして構成します。
- GitHubのソースコードを取得
- CodeBuildでlint / test / Dockerビルドを実行
- ECS Serviceを更新してアプリをデプロイ
対象ファイル
lib/pipeline-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipelineActions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecs from 'aws-cdk-lib/aws-ecs';
interface PipelineStackProps extends cdk.StackProps {
repository: ecr.IRepository;
ecsService: ecs.FargateService;
connectionArn: string;
}
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: PipelineStackProps) {
super(scope, id, props);
const sourceOutput = new codepipeline.Artifact();
const buildOutput = new codepipeline.Artifact();
const project = new codebuild.PipelineProject(this, 'NextjsBuildProject', {
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
privileged: true,
environmentVariables: {
REPOSITORY_URI: {
value: props.repository.repositoryUri,
},
AWS_DEFAULT_REGION: {
value: this.region,
},
},
},
});
props.repository.grantPullPush(project);
const pipeline = new codepipeline.Pipeline(this, 'NextjsPipeline', {
pipelineName: 'NextjsPipeline',
});
pipeline.addStage({
stageName: 'Source',
actions: [
new codepipelineActions.CodeStarConnectionsSourceAction({
actionName: 'GitHub_Source',
owner: '<GitHubユーザー名>',
repo: '<リポジトリ名>',
branch: '<ブランチ名>',
output: sourceOutput,
connectionArn: props.connectionArn,
}),
],
});
pipeline.addStage({
stageName: 'Build',
actions: [
new codepipelineActions.CodeBuildAction({
actionName: 'Docker_Build_Push',
project,
input: sourceOutput,
outputs: [buildOutput],
}),
],
});
pipeline.addStage({
stageName: 'Deploy',
actions: [
new codepipelineActions.EcsDeployAction({
actionName: 'ECS_Deploy',
service: props.ecsService,
input: buildOutput,
}),
],
});
}
}
<GitHubユーザー名>、<リポジトリ名>、<ブランチ名> は、
自身のGitHubリポジトリに合わせて置き換えてください。
6-3. ServiceStackでECS Serviceを外部参照できるようにする
PipelineStackでは、既存のECS Serviceを参照する必要があります。
そのため、service-stack.ts で作成したServiceをプロパティとして公開します。
対象ファイル
lib/service-stack.ts
public readonly service: ecs.FargateService;
コンストラクタ内の const service を this.service に変更します。
this.service = new ecs.FargateService(this, 'NextjsService', {
cluster: props.cluster,
taskDefinition: props.taskDefinition,
desiredCount: 1,
assignPublicIp: false,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [serviceSg],
});
this.service.attachToApplicationTargetGroup(props.targetGroup);
const scalableTarget = this.service.autoScaleTaskCount({
minCapacity: 1,
maxCapacity: 3,
});
6-4. PipelineStackを追加する
CDKのエントリーポイントにPipelineStackを追加します。
connectionArnには、6-0 で作成したGitHub接続のARNを指定します。
対象ファイル
bin/cdk-nextjs-infra.ts
※ 本シリーズでは CDKプロジェクト名を cdk-nextjs-infra としています。
import { PipelineStack } from '../lib/pipeline-stack';
//...
new PipelineStack(app, 'NextjsInfraPipelineStack', {
env,
repository: ecrStack.repository,
ecsService: serviceStack.service,
connectionArn: '<CodeStar Connections の ARN>',
});
<CodeStar Connections の ARN> は、実際に作成したGitHub接続のARNに置き換えてください。
6-5. デプロイ
cdk deploy NextjsInfraPipelineStack --profile <プロファイル名>
本記事で作成したスタックに含まれるAWSリソースは、削除するまで料金が発生します。
検証が不要になった場合は、以下のコマンドでスタックを削除してください。
cdk destroy <スタック名> --profile <プロファイル名>
6-6. 確認
① CodePipelineの確認
AWSコンソール → CodePipeline → 対象Pipeline
- 最新の実行ステータスが Succeeded になっている
対象Pipeline を開き、以下を確認します。
- Source / Build / Deploy の3ステージが存在する
- 各ステージが成功している
6-7. GitHub pushで自動デプロイを試す
Next.jsアプリ側で軽微な変更(例:表示文言の修正など)を行い、
その変更を main ブランチへpushします。
git add .
git commit -m "test: ci cd deploy"
git push origin main
push後、自動でCodePipelineが起動し、
Source → Build → Deploy の各ステージが順に実行され、
すべて成功すればデプロイ完了です。
最終コード(今回追加・修正したファイル)
buildspec.yml
コード全体
version: 0.2
env:
shell: bash
phases:
install:
runtime-versions:
nodejs: 20
commands:
- echo "Installing dependencies..."
- npm ci
pre_build:
commands:
- echo "Running lint..."
- npm run lint
- echo "Running tests..."
- npm test
- echo "Logging in to Amazon ECR..."
- IMAGE_TAG=latest
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $REPOSITORY_URI
build:
commands:
- echo "Building Docker image..."
- docker build -t $REPOSITORY_URI:$IMAGE_TAG .
post_build:
commands:
- echo "Pushing Docker image..."
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo "Creating imagedefinitions.json..."
- |
printf '[{"name":"NextjsContainer","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
artifacts:
files:
- imagedefinitions.json
lib/pipeline-stack.ts
コード全体
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipelineActions from 'aws-cdk-lib/aws-codepipeline-actions';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ecs from 'aws-cdk-lib/aws-ecs';
interface PipelineStackProps extends cdk.StackProps {
repository: ecr.IRepository;
ecsService: ecs.FargateService;
connectionArn: string;
}
export class PipelineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: PipelineStackProps) {
super(scope, id, props);
const sourceOutput = new codepipeline.Artifact();
const buildOutput = new codepipeline.Artifact();
const project = new codebuild.PipelineProject(this, 'NextjsBuildProject', {
environment: {
buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
privileged: true,
environmentVariables: {
REPOSITORY_URI: {
value: props.repository.repositoryUri,
},
AWS_DEFAULT_REGION: {
value: this.region,
},
},
},
});
props.repository.grantPullPush(project);
const pipeline = new codepipeline.Pipeline(this, 'NextjsPipeline', {
pipelineName: 'NextjsPipeline',
});
pipeline.addStage({
stageName: 'Source',
actions: [
new codepipelineActions.CodeStarConnectionsSourceAction({
actionName: 'GitHub_Source',
owner: '<GitHubユーザー名>',
repo: '<リポジトリ名>',
branch: '<ブランチ名>',
output: sourceOutput,
connectionArn: props.connectionArn,
}),
],
});
pipeline.addStage({
stageName: 'Build',
actions: [
new codepipelineActions.CodeBuildAction({
actionName: 'Docker_Build_Push',
project,
input: sourceOutput,
outputs: [buildOutput],
}),
],
});
pipeline.addStage({
stageName: 'Deploy',
actions: [
new codepipelineActions.EcsDeployAction({
actionName: 'ECS_Deploy',
service: props.ecsService,
input: buildOutput,
}),
],
});
}
}
<GitHubユーザー名>、<リポジトリ名>、<ブランチ名> は、
自身のGitHubリポジトリに合わせて置き換えてください。
lib/service-stack.ts
コード全体
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
interface ServiceStackProps extends cdk.StackProps {
cluster: ecs.Cluster;
taskDefinition: ecs.FargateTaskDefinition;
targetGroup: elbv2.ApplicationTargetGroup;
vpc: ec2.IVpc;
albSecurityGroup: ec2.SecurityGroup;
}
export class ServiceStack extends cdk.Stack {
public readonly service: ecs.FargateService;
constructor(scope: Construct, id: string, props: ServiceStackProps) {
super(scope, id, props);
const serviceSg = new ec2.SecurityGroup(this, 'ServiceSecurityGroup', {
vpc: props.vpc,
allowAllOutbound: true,
});
serviceSg.addIngressRule(
props.albSecurityGroup,
ec2.Port.tcp(3000),
'Allow traffic from ALB'
);
this.service = new ecs.FargateService(this, 'NextjsService', {
cluster: props.cluster,
taskDefinition: props.taskDefinition,
desiredCount: 1,
assignPublicIp: false,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [serviceSg],
});
this.service.attachToApplicationTargetGroup(props.targetGroup);
const scalableTarget = this.service.autoScaleTaskCount({
minCapacity: 1,
maxCapacity: 3,
});
scalableTarget.scaleOnCpuUtilization('CpuScaling', {
targetUtilizationPercent: 50,
scaleInCooldown: cdk.Duration.seconds(60),
scaleOutCooldown: cdk.Duration.seconds(60),
});
}
}
bin/cdk-nextjs-infra.ts
コード全体
import * as cdk from 'aws-cdk-lib';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { EcrStack } from '../lib/ecr-stack';
import { NetworkStack } from '../lib/network-stack';
import { EcsStack } from '../lib/ecs-stack';
import { CertificateStack } from '../lib/certificate-stack';
import { AlbStack } from '../lib/alb-stack';
import { ServiceStack } from '../lib/service-stack';
import { WafStack } from '../lib/waf-stack';
import { PipelineStack } from '../lib/pipeline-stack';
const app = new cdk.App();
const env = {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
};
const ecrStack = new EcrStack(app, 'NextjsInfraEcrStack', {
env,
});
const networkStack = new NetworkStack(app, 'NextjsInfraNetworkStack', {
env,
});
const ecsStack = new EcsStack(app, 'NextjsInfraEcsStack', {
env,
vpc: networkStack.vpc,
});
const hostedZone = route53.HostedZone.fromLookup(networkStack, 'HostedZone', {
domainName: 'example.com',
});
const certificateStack = new CertificateStack(app, 'NextjsInfraCertificateStack', {
env,
hostedZone,
});
const albStack = new AlbStack(app, 'NextjsInfraAlbStack', {
env,
vpc: networkStack.vpc,
hostedZone,
certificate: certificateStack.certificate,
});
const serviceStack = new ServiceStack(app, 'NextjsInfraServiceStack', {
env,
cluster: ecsStack.cluster,
taskDefinition: ecsStack.taskDefinition,
targetGroup: albStack.targetGroup,
vpc: networkStack.vpc,
albSecurityGroup: albStack.albSecurityGroup,
});
new WafStack(app, 'NextjsInfraWafStack', {
env,
alb: albStack.alb,
});
new PipelineStack(app, 'NextjsInfraPipelineStack', {
env,
repository: ecrStack.repository,
ecsService: serviceStack.service,
connectionArn: '<CodeStar Connections の ARN>',
});
ここまでの成果
GitHubへのpushをきっかけに、以下の一連の処理を自動化できました。
- Dockerイメージのビルド
- ECRへのpush
- ECSへのデプロイ
これにより、手動作業に依存しないCI/CD基盤が整いました。
まとめ
本シリーズでは、AWS CDKを使ってNext.jsアプリを段階的に本番構成へ改善してきました。
単にアプリを公開するだけでなく、ネットワーク分離、HTTPS化、WAFによる防御、
Auto Scalingによる可用性向上、そしてCI/CDによる自動化までを順に整備しています。
その結果、セキュリティ・可用性・運用効率を意識した基盤を一通り構築することができました。