ECSとCodePipelineのローリングアップデート構成をCDKで実装してみた

2023.09.27

こんにちは、つくぼし(tsukuboshi0755)です!

以前以下のブログで、CDK(TypeScript)でECSとECRのコンテナ構成を実装しました。

今回はこの構成に対して、CodeCommit及びCodeBuildを含むCodePipelineを追加し、ECSのローリングアップデート構成を実現するCDKコードを作ってみたいと思います!

前提条件

今回は以下の通り、CDKv2を使ってコードを書いていきます。

$ cdk version
2.83.1 (build 006b542)

またDockerクライアントとしては、Rancher Desktopを使用します。

$ rdctl version
rdctl client version: 1.1.0, targeting server version: v1

全体構成

今回はVPC+ALB+ECS(Fargate)+ECRに加えて、CodeCommit+CodeBuild+CodePipelineのCI/CD構成を追加で作成し、nginxコンテナをパブリック公開します。

リポジトリ

コード全体については、以下のリポジトリに格納していますのでご参照ください。

tsukuboshi/cdk-microservices-rollingupdate-template

コード解説

CDKコードの中核となるlib/cdk-microservices-rollingupdate-template-stack.ts について説明します。

なお以前のECSとECRのコンテナ構成をCDKで実装してみた | DevelopersIOで説明した箇所は省略します。

また今回ローリングアップデートで必要なビルド仕様ファイル及びイメージ定義ファイルについて触れますが、該当ファイルの詳細は以下のブログをご参照ください。

ECSのデプロイ設定

    // Create ALB and ECS Fargate Service
    const service = new ecs_patterns.ApplicationLoadBalancedFargateService(
      this,
      "FargateService",
      {
        loadBalancerName: `${resourceName}-lb`,
        publicLoadBalancer: true,
        cluster: cluster,
        serviceName: `${resourceName}-service`,
        cpu: 256,
        desiredCount: 2,
        memoryLimitMiB: 512,
        assignPublicIp: true,
        taskSubnets: { subnetType: ec2.SubnetType.PUBLIC },
        taskImageOptions: {
          family: `${resourceName}-taskdef`,
          containerName: `${resourceName}-container`,
          image: ecs.ContainerImage.fromEcrRepository(ecrRepository, "latest"),
          logDriver: new ecs.AwsLogDriver({
            streamPrefix: `container`,
            logGroup: logGroup,
          }),
        },
        deploymentController: {
          type: ecs.DeploymentControllerType.ECS,
        },
      }
    );

deployControllerのtypeにDeploymentControllerType.ECSを設定し、ECS自身がデプロイを制御するように設定します。

この設定により、ECSタスクがローリングアップデートでデプロイされるようになります。

CodeCommitリポジトリの作成

    // Create CodeCommit Repository
    const codeCommitRepository = new codecommit.Repository(
      this,
      "CodeCommitRepo",
      {
        repositoryName: `${resourceName}-codecommit-repo`,
      }
    );

CodePipelineのソースステージに指定するCodeCommitリポジトリを作成します。

なお今回は記述していませんが、以下のようにcodeにディレクトリパスを指定する事で、CodeCommitリポジトリが作成された後に、特定のディレクトリに含まれる初期コードをすぐにプッシュできます。

      {
        repositoryName: `${resourceName}-codecommit-repo`,
        code: codecommit.Code.fromDirectory(
          path.join(__dirname, "..", "app"),
          "main"
        ),
    }

CodeBuildプロジェクトの作成

    // Create CloudWatch Log Group
    const buildLogGroup = new logs.LogGroup(this, "BuildLogGroup", {
      logGroupName: `/aws/codebuild/${resourceName}`,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // Create CodeBuild Project
    const codeBuildProject = new codebuild.Project(this, "CodeBuildProject", {
      projectName: `${resourceName}-codebuild-project`,
      source: codebuild.Source.codeCommit({
        repository: codeCommitRepository,
      }),
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_5_0,
        computeType: codebuild.ComputeType.SMALL,
        privileged: true,
        environmentVariables: {
          AWS_ACCOUNT_ID: {
            value: accountId,
          },
          REPOSITORY_URI: {
            value: ecrRepository.repositoryUri,
          },
          CONTAINER_BUILD_PATH: {
            value: ".",
          },
          CONTAINER_NAME: {
            value: service.taskDefinition.defaultContainer?.containerName,
          },
        },
      },
      logging: {
        cloudWatch: {
          logGroup: buildLogGroup,
        },
      },
      buildSpec: codebuild.BuildSpec.fromObject({
        version: "0.2",
        phases: {
          pre_build: {
            commands: [
              "echo Logging in to Amazon ECR...",
              "aws --version",
              `aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com`,
              "COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)",
              "IMAGE_TAG=${COMMIT_HASH:=latest}",
            ],
          },
          build: {
            commands: [
              "echo Build started on `date`",
              "echo Building the Docker image...",
              "docker build -t $REPOSITORY_URI:latest $CONTAINER_BUILD_PATH",
              "docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG",
            ],
          },
          post_build: {
            commands: [
              "echo Build completed on `date`",
              "echo Pushing the Docker images...",
              "docker push $REPOSITORY_URI:$IMAGE_TAG",
              "docker push $REPOSITORY_URI:latest",
              "echo Writing image definitions file...",
              'printf \'[{"name":"%s","imageUri":"%s"}]\' $CONTAINER_NAME $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json',
            ],
          },
        },
        artifacts: {
          files: ["imagedefinitions.json"],
        },
      }),
    });

CodePipelineのビルドステージに指定するCodeBuildプロジェクトを作成します。

今回はbuildSpecBuildSpec.fromObjectを設定し、ビルド仕様をコマンド形式で書く形にしています。

この形にする事で、ビルド仕様をCDKコード内に記述でき、CDKの機能を利用しやすくなります。

参考:CodeBuildのbuildspecはCDKのBuildSpec.fromObjectで作成するようにした - mazyu36の日記

なおコマンド形式ではなく、ソースに存在するビルド仕様ファイル(buildspec.yml)を読み込む形にしたい場合は、fromSourceFilenameを用いる事で実現できます。

その場合はBuildSpecの箇所を以下の内容に書き換えた上で、CodeCommitリポジトリにビルド仕様ファイルをプッシュしてください。

        buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec.yml')

CodeBuildサービスロールへのECRアクセス権付与

    // Create ECR Access Policy
    const ecrAccessPolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: [
        "ecr:BatchCheckLayerAvailability",
        "ecr:CompleteLayerUpload",
        "ecr:GetAuthorizationToken",
        "ecr:InitiateLayerUpload",
        "ecr:PutImage",
        "ecr:UploadLayerPart",
      ],
      resources: ["*"],
    });

    // Add ECR Access Policy to CodeBuild Project
    codeBuildProject.addToRolePolicy(ecrAccessPolicy);

先程のProjectではCodeBuildサービスロールも自動的に作成されますが、デフォルトではECRへのPush権限がついていません。

そのため必要なECR権限を、CodeBuildサービスロールに付与します。

参考:CodeBuild のDocker サンプル - AWS CodeBuild

S3アーティファクトバケットの作成

    // Create Artifact Bucket for CodePipeline
    const artifactBucket = new s3.Bucket(this, "ArtifactBucket", {
      bucketName: `${resourceName}-artifact-bucket-${accountId}`,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      encryption: s3.BucketEncryption.S3_MANAGED,
      removalPolicy: RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

CodePipelineで使用するS3アーティファクトバケットを作成します。

今回のS3バケットは検証用のため、autoDeleteObjectsをtrueで設定し、バケットにオブジェクトが存在する場合でも削除できるようにしています。

CodePipelineの作成

    // Create CodePipeline
    const pipeline = new codepipeline.Pipeline(this, "CodePipeline", {
      artifactBucket: artifactBucket,
      pipelineName: `${resourceName}-pipeline`,
    });

    // Add Source Stage to Pipeline
    const sourceOutput = new codepipeline.Artifact(`${resourceName}-source`);
    const sourceAction = new codepipeline_actions.CodeCommitSourceAction({
      actionName: "Source",
      repository: codeCommitRepository,
      output: sourceOutput,
      branch: "main",
      trigger: codepipeline_actions.CodeCommitTrigger.EVENTS,
    });
    pipeline.addStage({
      stageName: "Source",
      actions: [sourceAction],
    });

    // Add Build Stage to Pipeline
    const buildOutput = new codepipeline.Artifact(`${resourceName}-build`);
    const buildAction = new codepipeline_actions.CodeBuildAction({
      actionName: "Build",
      project: codeBuildProject,
      input: sourceOutput,
      outputs: [buildOutput],
    });
    pipeline.addStage({
      stageName: "Build",
      actions: [buildAction],
    });

    // Add Deploy Stage to Pipeline
    const deployAction = new codepipeline_actions.EcsDeployAction({
      actionName: "Deploy",
      service: service.service,
      imageFile: buildOutput.atPath("imagedefinitions.json"),
    });
    pipeline.addStage({
      stageName: "Deploy",
      actions: [deployAction],
    });

CodePipelineを作成し、ソースステージ/ビルドステージ/デプロイステージを各々設定します。

CodeCommitSourceActiontriggerCodeCommitTrigger.EVENTSを設定する事で、CodePipeline用のEventBridgeルールが合わせて作成され、CodeCommitリポジトリにプッシュされると自動的にパイプラインが起動するようになります。

またEcsDeployActionimageFileには、ビルドステージで作成したイメージ定義ファイル(imagedefinitions.json)を指定します。

動作確認

CDKコードのデプロイ、及びECSタスクの起動確認については、ECSとECRのコンテナ構成をCDKで実装してみた | DevelopersIOをご参照ください。

ここではCodePipelineによるローリングアップデートの動作確認を実施します。

CDKコードをデプロイした後、パイプラインが以下の通り作成されている事を確認します。

続いてパイプラインを稼働させるため、コンソール上で作成されたCodeCommitリポジトリをクリックし、"ファイルのアップロード"をクリックします。

"Choose File"をクリックし、例としてGitHubリポジトリのapp/Dockerfileを選択してください。

また今回は検証のため、"作成者名"及び"Eメールアドレス"は何でも構いません。

項目を入力し終えたら、"変更のコミット"をクリックします。

コミット完了後、正常に設定されていればパイプラインが稼働します。

数分後、Deployステージまで成功していればローリングアップデートの動作確認は完了です!

最後に

今回はCodeCommit及びCodeBuildを含むCodePipelineを追加し、ECSのローリングアップデート構成を実現するCDKコードを作ってみました。

ぜひCDKでECSとCodePipelineのローリングアップデート構成を実装する際に、参考にしてみてください。

なおローリングアップデート構成と対をなす、ブルー/グリーンデプロイ構成については、以下のブログで解説していますので、必要に応じてご参照ください。

以上、つくぼし(tsukuboshi0755)でした!