0
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?

Terraform : CodePipelineを構築する際に押さえておきたいポイント(2) — CodeBuild、タグトリガー設計、パイプラインの分離

0
Posted at

前編では、アーティファクト保存用のS3バケットとIAMロール設計、GitHub連携の準備を扱いました。後編では、実際にDockerイメージをビルドするCodeBuildプロジェクト、パイプラインの起動条件、そしてAPI用パイプラインとバッチ用パイプラインを分離する設計判断について解説します。

CodeBuildプロジェクト:Docker-in-Dockerとログ設定

privileged_modeが必要な理由

CodeBuildの中でDockerイメージをビルドしてECRへプッシュする場合、CodeBuildの実行環境自体がコンテナであるため、「コンテナの中でさらにDockerを動かす」Docker-in-Docker構成になります。これには privileged_mode = true の指定が必須です。

resource "aws_cloudwatch_log_group" "codebuild" {
  name              = "/codebuild/${var.project}-${var.env}"
  retention_in_days = 14
}

resource "aws_codebuild_project" "build" {
  name          = "${var.project}-${var.env}-build"
  description   = "Build Docker image and push to ECR"
  build_timeout = 20
  service_role  = aws_iam_role.codebuild.arn

  artifacts {
    type = "CODEPIPELINE"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                       = "aws/codebuild/standard:7.0"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = true # Docker-in-Docker に必要

    environment_variable {
      name  = "AWS_DEFAULT_REGION"
      value = var.aws_region
    }
    environment_variable {
      name  = "ECR_REPO_URI"
      value = var.app_repository_url
    }
    environment_variable {
      name  = "FLYWAY_ECR_REPO_URI"
      value = var.flyway_repository_url
    }
    environment_variable {
      name  = "ECS_CLUSTER"
      value = var.ecs_cluster_name
    }
    environment_variable {
      name  = "FLYWAY_TASK_DEF_FAMILY"
      value = var.flyway_task_definition_family
    }
    environment_variable {
      name  = "FLYWAY_SUBNET"
      value = var.flyway_subnet_id
    }
    environment_variable {
      name  = "FLYWAY_SG"
      value = var.flyway_sg_id
    }
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = "backend/buildspec.yml"
  }

  logs_config {
    cloudwatch_logs {
      group_name = aws_cloudwatch_log_group.codebuild.name
    }
  }

  tags = {
    Project = var.project
    Env     = var.env
  }
}

privileged_mode = true はセキュリティ上の緩和設定であるため、必要なプロジェクトにだけ限定して付与するのが望ましい考え方です。今回のようにDockerビルドを行うプロジェクトでは必須ですが、Dockerを使わないビルドタスクがあれば、そちらには付与しないという判断も必要になります。

環境変数でビルドの挙動を外部化する

environment_variable ブロックを見ると、ECRリポジトリURIやECSクラスタ名、Flywayマイグレーション用のサブネット・セキュリティグループIDなど、多くの値が環境変数として渡されています。これらの値をbuildspec内にハードコードせず環境変数経由で渡すことで、buildspecファイル自体はどの環境(dev/staging/prod)でも共通のまま使い回せます。環境固有の値はTerraform側の変数として注入し、アプリケーション側のビルド手順(buildspec)は環境非依存に保つ、という責務分離です。

ログの保持期間

CodeBuildの実行ログはCloudWatch Logsに出力し、保持期間は14日としています。ビルドログはデバッグ用途が主で、ALB/WAFのアクセスログのような長期監査要件は薄いため、比較的短い保持期間にとどめています。ロググループを明示的に作成せずCodeBuildに任せると無期限保持になってしまうため、コスト管理の観点で aws_cloudwatch_log_group を先に作り retention_in_days を明示しておくのが定石です。

パイプラインのトリガー設計:ブランチpushではなくタグpush

CodePipeline本体を組み立てる前に、最も重要な設計判断を確認しておきます。それは「何をきっかけにパイプラインを起動するか」です。

多くのチュートリアルでは「mainブランチへのpushで自動デプロイ」という構成を紹介しますが、今回の構成では api-${env}-vX.Y.Z という形式のタグがpushされたときにのみパイプラインが起動するようにしています。

locals {
  api_tag_pattern   = "api-${var.env}-v*"
  batch_tag_pattern = "batch-${var.env}-v*"
}
resource "aws_codepipeline" "this" {
  name          = "${var.project}-${var.env}-pipeline"
  role_arn      = aws_iam_role.pipeline.arn
  pipeline_type = "V2"

  artifact_store {
    location = aws_s3_bucket.artifacts.bucket
    type     = "S3"
  }

  # タグ "api-${env}-vX.Y.Z" のpushでのみ起動する(ブランチへの通常pushでは起動しない)
  trigger {
    provider_type = "CodeStarSourceConnection"

    git_configuration {
      source_action_name = "GitHub_Source"

      push {
        tags {
          includes = [local.api_tag_pattern]
        }
      }
    }
  }

  # (ステージ定義は後述)
}

ブランチへの通常pushではなく、意図的にタグ付けされたコミットのみをデプロイ対象にすることで、次のような利点があります。

  • 「いつ・何を」デプロイしたかがGitのタグ履歴として明示的に残る:ブランチ運用だけでは「mainの最新が今どのバージョンなのか」を追いにくいが、タグならバージョン番号がそのままデプロイ単位になる
  • デプロイのタイミングを人間が制御できる:mainへのマージ = 即デプロイではなく、マージ後に任意のタイミングでタグを打つことでデプロイを実行できる
  • pipeline_type = "V2" のタグフィルタ機能により、Terraformコード側で起動条件を宣言的に管理できる(V1ではCloudWatch Events側で別途トリガーを組む必要があった)

git_configuration の push.tags.includes にワイルドカードパターンを指定するのがポイントで、api-${env}-v* という命名規則を守る限り、バージョン番号が変わるたびに新しいタグをpushするだけでデプロイが起動します。

ソースステージでDetectChanges = falseにする理由

Sourceステージの configuration には DetectChanges = "false" が設定されています。

stage {
  name = "Source"

  action {
    name             = "GitHub_Source"
    category         = "Source"
    owner            = "AWS"
    provider         = "CodeStarSourceConnection"
    version          = "1"
    output_artifacts = ["source_output"]

    configuration = {
      ConnectionArn        = aws_codestarconnections_connection.github.arn
      FullRepositoryId     = var.github_repository   # "owner/repo"
      BranchName           = var.github_branch
      OutputArtifactFormat = "CODE_ZIP"
      DetectChanges        = "false"
    }
  }
}

DetectChanges はソースアクション自体が変更検知(Webhookのポーリングトリガー)を持つかどうかの設定です。今回は前述の trigger ブロック(タグpushのフィルタ)側でパイプラインの起動条件を制御しているため、ソースアクション側の変更検知は false にして無効化し、トリガーの管理場所を一箇所に統一しています。両方を有効にしてしまうと、「タグ以外のpushでも起動してしまう」という意図しない挙動につながるため、どちらか一方に制御を寄せるのが安全です。

Buildステージ・Deployステージ

Buildステージは前述のCodeBuildプロジェクトを呼び出すだけのシンプルな構成です。

stage {
  name = "Build"

  action {
    name             = "CodeBuild"
    category         = "Build"
    owner            = "AWS"
    provider         = "CodeBuild"
    version          = "1"
    input_artifacts  = ["source_output"]
    output_artifacts = ["build_output"]

    configuration = {
      ProjectName = aws_codebuild_project.build.name
    }
  }
}

Deployステージは provider = "ECS" を指定し、CodeBuildが生成した imagedefinitions.json を使ってECSサービスをローリングアップデートします。

stage {
  name = "Deploy"

  action {
    name            = "ECS_Deploy"
    category        = "Deploy"
    owner           = "AWS"
    provider        = "ECS"
    version         = "1"
    input_artifacts = ["build_output"]

    configuration = {
      ClusterName = var.ecs_cluster_name
      ServiceName = var.ecs_service_name
      FileName    = "imagedefinitions.json"
    }
  }
}

imagedefinitions.json は、どのコンテナ定義(コンテナ名)に対してどのイメージ(ECR上のタグ付きURI)を使うかを記述したJSONファイルで、CodeBuild側のbuildspec内で生成する想定のファイルです。このファイル名は固定の規約であり、ECSデプロイアクションはこのファイル名を前提に動作します。

なぜバッチ用パイプラインを分離するのか

ここまでのAPI用パイプライン(Source→Build→Deployの3ステージ)に加え、バッチ処理用に別のパイプラインを用意しています。

resource "aws_codebuild_project" "batch_build" {
  name          = "${var.project}-${var.env}-batch-build"
  description   = "Build batch Docker image, push to ECR and register a new task definition revision"
  build_timeout = 20
  service_role  = aws_iam_role.codebuild.arn

  artifacts {
    type = "CODEPIPELINE"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                       = "aws/codebuild/standard:7.0"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = true # Docker-in-Docker に必要

    environment_variable {
      name  = "AWS_DEFAULT_REGION"
      value = var.aws_region
    }
    environment_variable {
      name  = "ECR_REPO_URI"
      value = var.batch_repository_url
    }
    environment_variable {
      name  = "BATCH_TASK_DEF_FAMILY"
      value = var.batch_task_definition_family
    }
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = "batch/buildspec.yml"
  }

  logs_config {
    cloudwatch_logs {
      group_name = aws_cloudwatch_log_group.codebuild.name
    }
  }

  tags = {
    Project = var.project
    Env     = var.env
  }
}
resource "aws_codepipeline" "batch" {
  name          = "${var.project}-${var.env}-batch-pipeline"
  role_arn      = aws_iam_role.pipeline.arn
  pipeline_type = "V2"

  artifact_store {
    location = aws_s3_bucket.artifacts.bucket
    type     = "S3"
  }

  # タグ "batch-${env}-vX.Y.Z" のpushでのみ起動する(ブランチへの通常pushでは起動しない)
  trigger {
    provider_type = "CodeStarSourceConnection"

    git_configuration {
      source_action_name = "GitHub_Source"

      push {
        tags {
          includes = [local.batch_tag_pattern]
        }
      }
    }
  }

  stage {
    name = "Source"

    action {
      name             = "GitHub_Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["source_output"]

      configuration = {
        ConnectionArn        = aws_codestarconnections_connection.github.arn
        FullRepositoryId     = var.github_repository
        BranchName           = var.github_branch
        OutputArtifactFormat = "CODE_ZIP"
        DetectChanges        = "false"
      }
    }
  }

  stage {
    name = "Build"

    action {
      name             = "CodeBuild"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      input_artifacts  = ["source_output"]
      output_artifacts = ["build_output"]

      configuration = {
        ProjectName = aws_codebuild_project.batch_build.name
      }
    }
  }

  tags = {
    Project = var.project
    Env     = var.env
  }
}

分離の理由は大きく2つあります。

1. デプロイ・設定・ソースをAPIと混在させないため

APIとバッチは別々のタイミング・別々の関心事でリリースされることが多く、同じパイプラインに混在させると「バッチの修正のためだけにAPI全体のパイプラインが動く/待たされる」といった余計な結合が生まれます。タグの命名規則(api- / batch- のプレフィックス)を分けることで、どちらのパイプラインを起動するかが明確に切り分けられます。

2. バッチにはECS Serviceが存在せず、Deployステージという概念がそもそも成立しない

APIはECS Service上で常時稼働するコンテナですが、バッチはEventBridge Schedulerなどから都度 RunTask される使い捨てのタスクで、常駐するECS Serviceを持ちません。ECSデプロイアクション(provider = "ECS")は「サービスのローリングアップデート」を行う仕組みであるため、Serviceが存在しないバッチにはそもそも適用できません。

そのためバッチ用パイプラインは、CodeBuild内で新しいタスク定義リビジョンを登録する(RegisterTaskDefinition)ところまでを担い、Source→Buildの2ステージで完結する構成になっています。次回の RunTask 実行時には、EventBridge Scheduler側の設定次第で最新のタスク定義リビジョンが使われる、という前提です。

この判断は、「同じCodePipelineというサービスを使っているからといって、全てのワークロードを同じパイプライン構造に当てはめる必要はない」という実践的な教訓でもあります。ワークロードの実行モデル(常駐サービス vs 使い捨てタスク)が異なれば、パイプラインの構造自体も変えるべきです。

運用:失敗検知と本番デプロイの承認フロー

ここまでの構成は「タグをpushすればビルドからデプロイまで自動で進む」ことを重視した設計でした。しかし運用を見据えると、自動化された分だけ「失敗に気づけない」「意図せず本番まで進んでしまう」というリスクも生まれます。

パイプライン失敗の通知

現状、パイプラインが失敗してもコンソールを見に行かない限り気づけません。EventBridgeでCodePipelineの実行状態変化を拾い、SNS経由で通知する仕組みを加えます。

resource "aws_cloudwatch_event_rule" "pipeline_failed" {
  name = "${var.project}-${var.env}-pipeline-failed"

  event_pattern = jsonencode({
    source      = ["aws.codepipeline"]
    detail-type = ["CodePipeline Pipeline Execution State Change"]
    detail = {
      state    = ["FAILED"]
      pipeline = [aws_codepipeline.this.name, aws_codepipeline.batch.name]
    }
  })
}

resource "aws_cloudwatch_event_target" "pipeline_failed_sns" {
  rule      = aws_cloudwatch_event_rule.pipeline_failed.name
  target_id = "sns"
  arn       = var.alarm_sns_topic_arn
}

タグpushでのみ起動する構成のため、失敗に気づかず放置すると「タグを打ったのにデプロイされていない」状態が長期間続くリスクがあります。ECS側でタスク/デプロイの状態変化をEventBridgeで監視するのと同じ考え方を、パイプライン自体にも適用します。

本番デプロイ前の承認ステージ

現状、タグをpushすれば即座にDeployステージまで自動で進みます。dev環境ではこれで問題ありませんが、本番環境ではBuild完了後・Deploy実行前に人間の承認を挟む Approval ステージを検討すべきです。

stage {
  name = "Approval"

  action {
    name     = "ManualApproval"
    category = "Approval"
    owner    = "AWS"
    provider = "Manual"
    version  = "1"

    configuration = {
      NotificationArn = var.alarm_sns_topic_arn
      CustomData      = "本番環境へのデプロイ承認"
    }
  }
}

タグpush方式は「デプロイのタイミングを人間が制御できる」という設計思想でしたが、タグを打った瞬間に自動でDeployまで進む点は変わりません。本番ではこのギャップを承認ステージで埋める必要があります。dev/staging/prodで環境ごとにステージ構成を変える(count や dynamic で本番のみステージを追加する)といった実装が考えられます。

パイプラインが「動いていない」ことの検知

タグpush運用ならではの落とし穴として、「そもそもタグを打ち忘れている」「CI側の何らかの理由でトリガー自体が発火しない」といったケースがあります。これは失敗通知では拾えません。最終成功実行からの経過時間を監視する(定期的にLambdaで実行履歴を確認しCloudWatchカスタムメトリクスへ出す、など)までは要件次第ですが、少なくとも「最後にいつデプロイが成功したか」を追える状態にしておくのは有用です。

運用:コストとパフォーマンスの余地

CodeBuildのキャッシュ設定

現状の aws_codebuild_project には cache ブロックが無いため、毎回フルビルドになります。Dockerレイヤーキャッシュを有効化すると、ビルド時間とCodeBuildの実行コストを削減できます。

resource "aws_codebuild_project" "build" {
  # ...(既存の設定)

  cache {
    type  = "LOCAL"
    modes = ["LOCAL_DOCKER_LAYER_CACHE"]
  }
}

ビルド頻度が低い間は優先度は低いですが、リリースサイクルが早まってきたタイミングで効果が出てきます。

並行実行の挙動確認

pipeline_type = "V2" は実行のキューイングをサポートしますが、execution_mode などの明示的な設定はこの構成には含まれていません。短時間に複数タグを連続pushした場合に「順次実行されるのか」「後発が先発を待つのか」は、一度実際に確認しておくと安心です。

出力値(outputs)

他のモジュール(EventBridgeによるパイプライン状態の監視や、CloudWatchダッシュボードなど)から参照できるよう、パイプライン名とアーティファクトバケット名を出力しておきます。

output "pipeline_name" {
  value = aws_codepipeline.this.name
}

output "artifacts_bucket" {
  value = aws_s3_bucket.artifacts.bucket
}

output "batch_pipeline_name" {
  value = aws_codepipeline.batch.name
}

まとめ

  1. パイプライン本体より先に、アーティファクトの受け皿となるS3バケットを用意する
  2. CodePipeline用・CodeBuild用のIAMロールはサービスごとに分離し、iam:PassRole はリソースを明示的に列挙して権限昇格を防ぐ
  3. CodeStar Connectionsは作成後にコンソール側での手動認可が必要であることを忘れない
  4. Docker-in-Docker構成には privileged_mode = true が必須で、環境固有の値はbuildspecにハードコードせず環境変数で渡す
  5. パイプラインの起動条件はブランチpushではなくタグpushにすることで、デプロイのタイミングを人間が制御できるようにする
  6. ソースアクションの変更検知(DetectChanges)とパイプラインのトリガー設定を両方有効にせず、制御場所を一箇所に統一する
  7. ワークロードの実行モデル(常駐サービス vs 使い捨てタスク)が異なる場合、無理に1つのパイプライン構造にまとめず、別パイプラインとして分離する
  8. パイプライン失敗はEventBridge+SNSで通知し、コンソールを見に行かないと気づけない状態を避ける
  9. 本番環境ではBuildとDeployの間に承認ステージを挟み、タグpushによる自動デプロイの範囲を制御する
  10. アーティファクトバケットには非TLSアクセス拒否ポリシーを加え、IAMポリシーは動的に変化しないリソースであればResource = "*"を避けて絞り込む

CodePipeline自体は数個のリソースで組めますが、「何をトリガーに、どの粒度でパイプラインを分けるか」という設計判断が、運用のしやすさを大きく左右するという点が、このモジュール全体を通しての要点です。

0
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
0
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?