見出し画像

CDK を使って CodeBuild を使った CI を設定する

Solution Architect の t_maru です。

前回までは CloudFormation を使って CI/CD の Pipeline を構築していましたが、これを CDK (Cloud Development Kit) を使って構築するとどのようになるのか、またどういったメリットがあるのかを説明します。

※ 本記事でお伝えする内容は CDK の version 2 を対象とした内容となっておりますのでご注意ください。

前回までの内容を確認したい方は以下のリンクから過去の記事に飛べます。

CDK とは?

AWS Cloud Development Kit (以降、CDK) とは、これまで CloudFormation のテンプレートに JSON もしくは YAML で書いていたリソース定義を TypeScript, JavaScript, Python, Java, C#, Go などの言語を使って行えるようにするツールです。

PC に npm 経由でインストールできる CLI が提供されており、テンプレートの初期構築、デプロイなどの作業を CLI で操作できます。実際に AWS に環境をデプロイするときも `cdk deploy ・・・` というコマンドを使用することで AWS 環境上に必要なリソースが作成されます。コマンドの裏側では CloudFormation の template が生成され、それに従って CloudFormation で環境構築という処理が自動的に行われるため、開発者は CloudFormation を意識することなくリソース定義が可能になります。

CDK を使用する具体的なメリットとしては以下のようなものがあります。

  1. 最低限のパラメータ設定で各種リソースを定義できる

  2. ループなどの処理を行うことが容易になる

  3. 言語、エディタによってはコード補完が使えるようになる

上記の 1 については賛否が分かれる部分かとは思いますが、CDK でデフォルト設定がされているパラメータ (正確には Constructs でデフォルトのパラメータが存在しており、リソース定義時に optional のパラメータとなっているもの) については、変更不要の場合はリソース定義時に値の指定が不要となるためリソース定義が非常にシンプルになります。

例として VPC を作るリソース定義を CDK (TypeScript) を使った場合と CloudFormation を使った場合でどのように違うのかを考えてみます。

CDK の場合は以下のようにリソース定義すると、VCP, Subnet, Internet Gateway, Route Table, Nat Gateway などが一通り作成され、Public subnet, Private subnet の 2 層構成になった VPC ネットワークをすぐに使い始めることができます。

import * as cdk from 'aws-cdk-lib';
import { Construct  } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';

export class CdkVpcStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const vpc = new ec2.Vpc(this, 'my-vpc');
  }
}

これに対して CloudFormation で設定しようとする場合、VPC と Subnet, Internet Gateway を作るだけで以下のような量の template を定義する必要があるため、単純な作業量を比較すると明確に差があることがわかります。

AWSTemplateFormatVersion: "2010-09-09"
Description: Network Resources

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "172.31.0.0/16"
      EnableDnsHostnames: true
      EnableDnsSupport: true

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: "172.31.0.0/20"
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 0
        - Fn::GetAZs: !Ref "AWS::Region"

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: "172.31.16.0/20"
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 1
        - Fn::GetAZs: !Ref "AWS::Region"

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: "172.31.32.0/20"
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 2
        - Fn::GetAZs: !Ref "AWS::Region"

  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: "172.31.48.0/20"
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 3
        - Fn::GetAZs: !Ref "AWS::Region"

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref InternetGateway

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc

  AttachPublicRouteTable1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1

  AttachPublicRouteTable2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

  AttachPrivateRouteTable1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet1

  AttachPrivateRouteTable2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet2

  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref InternetGateway

CDK がすべての場合においてメリットしかないか?と言われると No であると思います。例えば、上記の VPC を CDK を使って定義する例ですと、設定項目が不要で簡潔な定義になる反面、どのような定義がされているかブラックボックス化し、本来は必要なかったサービスやパラメータが設定されてしまうという危険性もあると思います。このため、`賛否が分かれる` と本項の冒頭で書かせていただきました。

とはいえ、詳細に制御したい場合は必要なパラメータをこれまでの CloudFormation のように定義すればよいだけで、これまでと同等の設定は CDK でも定義できるかと思いますので、この点のみで CDK の導入を見送ってしまうのは少しもったいないと思います。現状比較してどの程度メリット/デメリットがあるのかを評価して皆様にあった選択をして頂ければと思います。

CDK で CodeBuild を使った CI を定義する

本題です。比較対象があったほうが良いと思いますので、`CodeBuild で Pull Request や不特定のブランチへの Push をトリガーとして CI する方法` のサンプルで構築した環境を CDK を使って定義してみようと思います。

CLI を使うと CDK のプロジェクトの init が簡単にできますので、公式のドキュメントを参照してください。

まず、今回作成する必要があるリソースを以前 CloudFormation の template で定義したものを参考にリストアップしてみます。

参考にする CloudFormation template はこちらです。
https://github.com/t-maru078/code-build-github-trigger/blob/main/ci/ci-resources.yml

上記の Template から定義が必要なものの概要を抜き出してみると下記のようなものがあります。

Parameters:
  SourceCodeRepositoryURL:
    Type: "String"

Resources:
  UnitTestProject:
    Type: AWS::CodeBuild::Project
    ...

  UnitTestProjectPolicy:
    Type: AWS::IAM::ManagedPolicy
    ...

  UnitTestProjectRole:
    Type: AWS::IAM::Role
    ...

  UnitTestProjectLogsGroup:
    Type: AWS::Logs::LogGroup
    ...

これらをどのように CDK で定義していくかを順番に見てみようと思います。
CDK の API Reference は下記の公式ドキュメントを参照してください。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html

まず Parameters についてですが、冒頭でも説明したように CDK はデプロイ時に CloudFormation の template を自動生成してデプロイを実施しますので、CDK でも CloudFormation の Parameter を使うことができます。

今回はパラメータとして、CodeBuild と接続される GitHub の情報が必要となりますので以下のようにパラメータとして、`githubOwnerName` と `gitHubRepoName` を定義します。今回の例では TypeScript を使用しており、コードも一部を抜粋していますのでデプロイできる完全な定義は記事の最後に掲載している GitHub のリポジトリを確認してください。

import * as cdk from 'aws-cdk-lib';

// ...

const githubOwnerName = new cdk.CfnParameter(this, 'githubOwnerName', {
  type: 'String',
});
const githubRepoName = new cdk.CfnParameter(this, 'githubRepoName', {
  type: 'String',
});

これらのパラメータは CLI を使ってデプロイする際にオプションとして指定します。指定しない場合はデプロイする前にエラーとなりますのでご注意ください。今回の例の場合、デプロイ時に以下のように指定します。

cdk deploy --parameters githubOwnerName=<GitHub の Owner or Organization 名> --parameters githubRepoName=<GitHub Repository の名前>

詳細については公式のドキュメントを参照してください。
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/parameters.html

次に大きなものとしては CodeBuild の Project があり、CloudFormation と同様、一度にパラメータを設定することもできるのですが、設定の一部を変数として切り出して定義することもできますので、今回は GitHub への接続部分の設定を CodeBuild の Project 設定と切り離して定義してみます。

import * as codebuild from 'aws-cdk-lib/aws-codebuild';

// ...

const githubSource = codebuild.Source.gitHub({
  owner: githubOwnerName.valueAsString,
  repo: githubRepoName.valueAsString,
  webhookFilters: [
    codebuild.FilterGroup.inEventOf(codebuild.EventAction.PUSH)
      .andBranchIsNot('main')
      .andBranchIsNot('develop')
      .andTagIsNot('.*'),
  ],
});

ここでは先程定義した Parameters を `owner`, `repo` の設定の際に使用しています。
その他今回の設定は `feature ブランチに対する Push をトリガーに CodeBuild を起動する` ために上記のように WebhookFilters を設定しました。

CDK の CodeBuild の API Reference のページに、CodeCommit, S3, Bitbucket など各種ソースの設定サンプルが載っているので必要に応じて参考にしてください。
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_codebuild-readme.html

ここで CloudFormation と違うポイントとしては、webhook filter で指定できるイベントが予め定義されている点です。CloudFormation を使っている場合は下記のように文字列で指定する必要があり、記載ミスが発生する可能性がありましたが、CDK と各言語 (今回は TypeScript) で使えるコード補完機能を使うと事前にライブラリで定義されている値を参照することができるため、記載ミスによるテンプレートのエラーを軽減することができます。

Triggers:
  # main, develop ブランチ以外への PUSH イベントトリガー
  FilterGroups:
    - - Type: EVENT
        Pattern: "PUSH"
        ExcludeMatchedPattern: false
      - Type: HEAD_REF
        Pattern: "^refs/heads/main$"
        ExcludeMatchedPattern: true
      - Type: HEAD_REF
        Pattern: "^refs/heads/develop$"
        ExcludeMatchedPattern: true
      - Type: HEAD_REF
        Pattern: "^refs/tags/.*"
        ExcludeMatchedPattern: true

次に、CodeBuild の Project で処理を実行した結果を保存する CloudWatch Logs のグループを定義します。

import * as logs from 'aws-cdk-lib/aws-logs';

// ...

const logGroup = new logs.LogGroup(this, 'UnitTestProjectLogsGroup', {
  retention: logs.RetentionDays.ONE_MONTH,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
});

Logs Group の設定に関しても、直接数値を入れるのではなく `logs.RetentionDays.ONE_MONTH` のように、事前定義されている値を使用することもできますので、ログ保持期間の設定ミスも軽減できそうです。

ここで CloudFormation のテンプレートの抜粋をもう一度確認します。

Parameters:
  SourceCodeRepositoryURL:
    Type: "String"

Resources:
  UnitTestProject:
    Type: AWS::CodeBuild::Project
    ...

  UnitTestProjectPolicy:
    Type: AWS::IAM::ManagedPolicy
    ...

  UnitTestProjectRole:
    Type: AWS::IAM::Role
    ...

  UnitTestProjectLogsGroup:
    Type: AWS::Logs::LogGroup
    ...

残っているのは CodeBuild の Project と、CodeBuild の Project が実行される際に使用される権限 (IAM Role とそれにアタッチされる Policy) です。
冒頭の CDK の紹介でも少し触れたように、CDK では予めデフォルトの設定値が定義されているものが存在しており、今回のケースだと IAM Role と Policy がデフォルトで設定されていますので、特殊な権限が不要の場合はあえて IAM Role と Policy を定義する必要がありません。

ということで、これまで設定した githubSource と logGroup の設定を使用して CodeBuild の Project は以下のように定義することができます。

import * as cdk from 'aws-cdk-lib';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';

// ...

const buildProject = new codebuild.Project(this, 'UnitTestProject', {
  source: githubSource,
  buildSpec: codebuild.BuildSpec.fromSourceFilename('ci/code-build/buildspec.yml'),
  environment: {
    buildImage: codebuild.LinuxBuildImage.STANDARD_6_0,
    computeType: codebuild.ComputeType.SMALL,
    privileged: true,
  },
  cache: codebuild.Cache.local(codebuild.LocalCacheMode.DOCKER_LAYER, codebuild.LocalCacheMode.SOURCE),
  logging: {
    cloudWatch: { enabled: true, logGroup },
  },
  timeout: cdk.Duration.minutes(5),
});

デプロイできる完全なテンプレートは下記を参照してください。
https://github.com/t-maru078/code-build-github-trigger-cdk

ここで、改めてリソース定義がどの程度簡略されたのかをソースコードの行数で確認してみましょう。

  • CloudFormation の場合: 99 行

  • CDK を使った場合: 48 行

作るリソースが多くない例でしたが、今回は約 50% の行を削減することができました。

まとめ

前回までは CloudFormation をそのまま利用してリソース構築を行っていましたが、今回はそれを CDK で定義するとどの様になるのか具体的な例を使って説明しました。

これまで説明してきたように CDK を使うことの主なメリットは以下のようなものがあります。

  • 最低限のパラメータ設定で各種リソースを定義できる

  • 言語、エディタによってはコード補完が使えるようになる

1 点目に関しては、明示的に指定がない場合は CDK 側がデフォルトで持っている値が使われるパラメータが存在することによるメリットですが、予期せぬ設定でリソースが作成されてしまうという危険性もありますので、利用する際には注意して使って頂ければと思います。

また、2 点目で挙げたコード補完機能については非常に便利ですので CDK を使って開発をする際には積極的に使っていただきたい機能です。

最後に、再掲になりますが今回の記事で使用している完全なソースコードは下記を参照してください。

みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!