2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AWS CDKでNext.jsを本番構成にする【第6回】〜CI/CDで自動デプロイする〜

2
Last updated at Posted at 2026-04-02

はじめに

現在の構成では、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の接続を作成します。

手順

  1. AWSコンソールから CodePipeline → 接続 を開く
  2. 「接続を作成」をクリック
  3. プロバイダーで GitHub を選択
  4. 接続名を入力(例:github-connection
  5. GitHubの認証・承認を行う
    • 「GitHubに接続する」をクリックすると、GitHubの認可画面が表示されます
    • 「Authorize AWS Connector for GitHub」をクリックして認可を行います
  6. 認可が完了するとAWSコンソールに戻り、接続ステータスが「利用可能」になることを確認します
  7. 作成した接続の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による自動化までを順に整備しています。

その結果、セキュリティ・可用性・運用効率を意識した基盤を一通り構築することができました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?