更新日:2025年6月12日

9分で読めます

子パイプラインを使用して5つの環境に継続的にデプロイする

使用するGitLabワークフローを最小限に抑えつつ、複数の環境(事前の準備なしに一時的に利用できるsandboxなど)への継続的デプロイを管理する方法を解説します。

DevSecOpsチームでは、複数の環境にまたがる継続的デプロイを管理する機能が必要となることがあります。その場合、ワークフローを変更せずに、デプロイを行えるようにする必要があります。GitLab DevSecOpsプラットフォームなら、事前の準備なしに一時的に利用できるsandboxを使用して工数を最小限に抑えるアプローチなどを通じて、こうしたニーズに対応できます。この記事では、Terraformを使って複数の環境上でインフラの継続的デプロイを行う方法についてご紹介します。

この手法は、PulumiAnsibleのような別の技術を使用したInfrastructure as Code(IaC)でも、どのような言語で書かれたソースコードでも、または多様な言語が混在するモノレポであっても、あらゆるプロジェクトに簡単に適用できます。

このチュートリアルの終了時には、以下のような環境をデプロイするパイプラインが完成します。

  • 各フィーチャーブランチの一時的な**レビュー(review)**環境。
  • 簡単に消去可能で、mainブランチからデプロイされる**統合(integration)**環境。
  • **品質管理(qa)**環境。同様にmainブランチからデプロイされ、品質管理プロセスを実行します。
  • タグ付けされるたびにデプロイされる**ステージング(staging)**環境。これは本番環境前の最後のステージです。
  • ステージング環境の直後の**本番(production)**環境。今回はデモ用に手動でトリガーしますが、継続的にデプロイされるようにすることも可能です。

この記事で使用されるフローチャートの説明は以下のとおりです。

  • 角が丸いボックスはGitLabブランチです。
  • 四角のボックスは環境です。
  • 矢印上のテキストは、あるボックスから次のボックスへのアクションを指します。
  • ひし形のボックスは決定ステップです。
flowchart LR
    A(main) -->|新機能| B(feature_X)

    B -->|自動デプロイ| C[review/feature_X]
    B -->|マージ| D(main)
    C -->|破棄| D

    D -->|自動デプロイ| E[integration]
    E -->|手動| F[qa]

    D -->|タグ付け| G(X.Y.Z)
    F -->|検証| G

    G -->|自動デプロイ| H[staging]
    H -->|手動| I{plan}
    I -->|手動| J[production]

ステップごとに、理由行うことを説明した上で、方法をご紹介します。これにより、このチュートリアルを完全に理解し、正確に実行しやすくなります。

理由

  • 継続的インテグレーションはほぼ事実上の業界標準と言えます。ほとんどの企業は、CIパイプラインを実装済みであるか、その実践の標準化を検討しています。

  • また、CIパイプラインの最後にリポジトリまたはレジストリにアーティファクトをプッシュする継続的なデリバリーも一般的です。

  • 継続的デプロイメントはさらに進んで、これらのアーティファクトを自動的にデプロイしますが、その普及はまだ限定的です。導入されている場合、主にアプリケーション分野で見られます。インフラの継続的デプロイメントに関しては、状況がやや不明瞭で、複数の環境の管理に重きが置かれる傾向があります。一方で、インフラのコードをテストし、セキュリティを確保し、検証することはより難しいとされています。この分野は、DevOpsがまだ成熟に至っていない分野のひとつです。ほかの分野としては、セキュリティのシフトレフトが挙げられます。具体的には、セキュリティチームの介入、さらに重要なことに、セキュリティ上のリスクへの対応をデリバリーライフサイクルの早期に組み込み、DevOpsからDevSecOpsへと発展させる取り組みのことです。

このような概況を踏まえ、本チュートリアルでは、インフラにDevSecOpsをシンプルかつ効果的に導入するシナリオに取り組みます。5つの環境にリソースをデプロイする例を交えながら、開発から本番環境へと段階的に進めていきます。

:個人的にはFinOpsアプローチを採用し、環境の数を減らすことを推奨していますが、開発環境、ステージング環境、本番環境以外の環境を保持すべき場合もあります。そのため、これからご紹介する例をご自身のニーズに合わせて調整してください。

行うこと

クラウド技術の台頭により、IaCの利用が促進されています。この分野を最初に開拓したのは、AnsibleとTerraformでした。OpenTofu、Pulumi、AWS CDK、Google Deploy Managerを始めとする多くの会社がその後に続きました。

IaCを定義することは、インフラストラクチャを安全にデプロイするのに最適なソリューションです。目標を達成できるまで必要なだけ、テスト、デプロイ、再実行を繰り返し行えます。

残念なことに、ターゲット環境ごとに複数のブランチやリポジトリを保持している企業がよくあります。これが原因で問題が生じます。こういった企業では、プロセスの実施が徹底されていません。本番環境のコードベースへの変更が、その前の環境で正しくテストされているかどうかも確認できません。その結果、ある環境から別の環境へ流れるだけになります。

このチュートリアルが必要だと気づいたのは、あるカンファレンスに参加した際に、本番環境へのデプロイ前にインフラストラクチャを十分にテストするワークフローがないと参加者全員から聞いたときです。みなが、本番環境で直接コードにパッチを適用することもあると言っていました。確かにこの方法は手っ取り早いですが、果たして安全でしょうか?前の環境にフィードバックをどう戻すのでしょうか?また副次効果が生じないようにするにはどうすればよいのでしょうか?新たな脆弱性が本番環境にあまりにも早くプッシュされることで会社がリスクにさらされないようにするには、どのように管理すべきでしょうか?

ここで重要なのは、DevOpsチームが本番環境に直接デプロイするのはなぜかということです。パイプラインの効率性や速度を向上できる可能性があるためでしょうか?自動化できないのでしょうか?それどころか、本番環境以外で正確にテストする方法がないからなのでしょうか?

次のセクションでは、インフラストラクチャを自動化し、ほかの人に影響を及ぼす環境にコードがプッシュされる前に、DevOpsチームが効果的かつ確実にテストを実施するための方法をご説明します。また、コードがどのように保護され、エンドツーエンドでデプロイが管理されているかも確認していきます。

方法

前述のとおり、現在では多くのIaC言語が存在しているため、この記事だけで客観的にすべてを取り上げることはできません。そのため、この記事ではバージョン1.4で実行される基本的なTerraformコードを使用します。IaC言語そのものではなく、貴社のエコシステムに適用できるプロセスに注目してください。

Terraformコード

まずは、基本的なTerraformコードから始めましょう。

仮想ネットワークであるAWSの仮想プライベートクラウド(VPC)にデプロイしたいと思います。VPCには、パブリックサブネットとプライベートサブネットをデプロイします。名前からわかるように、これらはメインVPCのサブネットです。最後に、パブリックサブネットにAmazon Elastic Cloud Compute(EC2)インスタンス(仮想マシン)を追加します。

これは、比較的簡単な方法で4つのリソースをデプロイする方法を示しています。コードではなく、パイプラインに焦点を当てることがここでの目的です。

ここで目指すリポジトリの完成形は、以下のとおりです。

リポジトリの完成図

ステップごとに行っていきましょう。

まずは、terraform/main.tfファイルでリソースをすべて宣言します。

provider "aws" {
  region = var.aws_default_region
}

resource "aws_vpc" "main" {
  cidr_block = var.aws_vpc_cidr

  tags = {
    Name     = var.aws_resources_name
  }
}

resource "aws_subnet" "public_subnet" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.aws_public_subnet_cidr

  tags = {
    Name = "Public Subnet"
  }
}
resource "aws_subnet" "private_subnet" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.aws_private_subnet_cidr

  tags = {
    Name = "Private Subnet"
  }
}

resource "aws_instance" "sandbox" {
  ami           = var.aws_ami_id
  instance_type = var.aws_instance_type

  subnet_id = aws_subnet.public_subnet.id

  tags = {
    Name     = var.aws_resources_name
  }
}

ご覧のとおり、このコードではいくつかの変数が必要となるため、terraform/variables.tfファイルでそれらを宣言します。

variable "aws_ami_id" {
  description = "The AMI ID of the image being deployed."
  type        = string
}

variable "aws_instance_type" {
  description = "The instance type of the VM being deployed."
  type        = string
  default     = "t2.micro"
}

variable "aws_vpc_cidr" {
  description = "The CIDR of the VPC."
  type        = string
  default     = "10.0.0.0/16"
}

variable "aws_public_subnet_cidr" {
  description = "The CIDR of the public subnet."
  type        = string
  default     = "10.0.1.0/24"
}

variable "aws_private_subnet_cidr" {
  description = "The CIDR of the private subnet."
  type        = string
  default     = "10.0.2.0/24"
}

variable "aws_default_region" {
  description = "Default region where resources are deployed."
  type        = string
  default     = "eu-west-3"
}

variable "aws_resources_name" {
  description = "Default name for the resources."
  type        = string
  default     = "demo"
}

すでにIaC側に関しては、これでほぼ準備が整いました。しかしながら、これではTerraformの状態を共有できません。ご存知ない方のために大まかに説明すると、Terraformは以下を行うことで動作します。

  • planにより、インフラストラクチャの現在の状態とコートで定義されている内容の差分を確認してから、差分を出力します。
  • applyにより、planの差分を適用して、状態を更新します。

最初のラウンドでは状態は空で、その後、Terraformによって適用されたリソースの詳細(IDなど)が挿入されます。

問題は、その状態がどこに保存されるかということです。また、複数のデベロッパーがコード上で共同作業を行えるようにするにはどうすればよいのでしょうか?

解決策はとても簡単で、GitLabを利用して、Terraform HTTPバックエンドを介して状態を保存して共有するだけです。

このバックエンドを使用するには、まずはもっともシンプルなterraform/backend.tfファイルを作成します。次のステップは、パイプラインで処理します。

terraform {
  backend "http" {
  }
}

これで、4つのリソースをデプロイするための最低限のTerraformコードができあがりました。変数の値は実行する際に指定するので、後でご説明します。

ワークフロー

これから以下のワークフローを実装します。

flowchart LR
    A(main) -->|新機能| B(feature_X)

    B -->|自動デプロイ| C[review/feature_X]
    B -->|マージ| D(main)
    C -->|破棄| D

    D -->|自動デプロイ| E[integration]
    E -->|手動| F[qa]

    D -->|タグ付け| G(X.Y.Z)
    F -->|検証| G

    G -->|自動デプロイ| H[staging]
    H -->|手動| I{plan}
    I -->|手動| J[production]
  1. フィーチャーブランチを作成します。これにより、コードに対して継続的にすべてのスキャナーが実行され、コンプライアンスとセキュリティを確保できます。このコードは、現在のブランチの名前が付けられた一時的な環境review/feature_branchに継続的にデプロイされます。これは、デベロッパーと運用チームが誰にも影響を与えることなくコードをテストできる安全な環境です。また、ここでコードレビューやスキャナーの実行などのプロセスを実施し、コードの品質とセキュリティが許容範囲内であることを確認し、資産が危険にさらされることのないようにします。このブランチでデプロイされたインフラストラクチャは、ブランチが閉じられると自動的に破棄されます。これにより予算範囲内に収めやすくなります。
flowchart LR
    A(main) -->|新機能| B(feature_X)

    B -->|自動デプロイ| C[review/feature_X]
    B -->|マージ| D(main)
    C -->|破棄| D
  1. 承認されると、フィーチャーブランチはmainブランチにマージされます。これは保護ブランチであり、誰もプッシュできません。本番環境への変更リクエストをすべて十分にテストするために必要です。このブランチも継続的にデプロイされます。ここでのターゲットはintegration環境です。この環境をもう少し安定させるために、削除は自動化されておらず、手動でトリガーできるようになっています。
flowchart LR
    D(main) -->|自動デプロイ| E[integration]
  1. ここから次のデプロイをトリガーするには、手動による承認が必要となります。これにより、mainブランチがqa環境にデプロイされます。ここでパイプラインからの削除を防ぐルールを設定します。何しろすでに3つ目のこの環境はかなり安定しているはずなので、このルールは誤って削除されるのを防ぐことを目的とします。貴社のプロセスに合わせて、お好きなようなルールを調整してください。
flowchart LR
    D(main)-->|自動デプロイ| E[integration]
    E -->|手動| F[qa]
  1. 次に進むには、コードにタグ付けする必要があります。保護タグを使用して、特定のユーザーのみが最後の2つの環境にデプロイできるようにします。これにより、staging環境へのデプロイが即座にトリガーされます。
flowchart LR
    D(main) -->|タグ付け| G(X.Y.Z)
    F[qa] -->|検証| G

    G -->|自動デプロイ| H[staging]
  1. ついにproductionに到達しました。インフラストラクチャに関して言うと、(10%や25%など)段階的にデプロイするのは難しい場合が多いため、インフラストラクチャ全体をデプロイします。ただし、この最後のステップで行う手動トリガーで、このデプロイを制御します。そして、この極めて重要な環境を最大限に制御できるようにするために、保護環境として管理します。
flowchart LR
    H[staging] -->|手動| I{plan}
    I -->|手動| J[production]

パイプライン

上記のワークフローを実装するために、2つのダウンストリームパイプラインとともにパイプラインを構築します。

メインパイプライン

まずは、メインパイプラインから始めましょう。メインパイプラインは、フィーチャーブランチへのプッシュデフォルトブランチへのマージ、またはタグ付けが発生すると、必ず自動的にトリガーされます。このパイプラインによって、devintegrationstaging環境に対する真の継続的デプロイを実現できます。プロジェクトのルートにある.gitlab-ci.ymlファイルで宣言します。

リポジトリのターゲット

Stages:
  - test
  - environments

.environment:
  stage: environments
  variables:
    TF_ROOT: terraform
    TF_CLI_ARGS_plan: "-var-file=../vars/$variables_file.tfvars"
  trigger:
    include: .gitlab-ci/.first-layer.gitlab-ci.yml
    strategy: depend            # Wait for the triggered pipeline to successfully complete
    forward:
      yaml_variables: true      # Forward variables defined in the trigger job
      pipeline_variables: true  # Forward manual pipeline variables and scheduled pipeline variables

review:
  extends: .environment
  variables:
    environment: review/$CI_COMMIT_REF_SLUG
    TF_STATE_NAME: $CI_COMMIT_REF_SLUG
    variables_file: review
    TF_VAR_aws_resources_name: $CI_COMMIT_REF_SLUG  # Used in the tag Name of the resources deployed, to easily differenciate them
  rules:
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH

integration:
  extends: .environment
  variables:
    environment: integration
    TF_STATE_NAME: $environment
    variables_file: $environment
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

staging:
  extends: .environment
  variables:
    environment: staging
    TF_STATE_NAME: $environment
    variables_file: $environment
  rules:
    - if: $CI_COMMIT_TAG

#### TWEAK
# This tweak is needed to display vulnerability results in the merge widgets.
# As soon as this issue https://gitlab.com/gitlab-org/gitlab/-/issues/439700 is resolved, the `include` instruction below can be removed.
# Until then, the SAST IaC scanners will run in the downstream pipelines, but their results will not be available directly in the merge request widget, making it harder to track them.
# Note: This workaround is perfectly safe and will not slow down your pipeline.
include:
  - template: Security/SAST-IaC.gitlab-ci.yml
#### END TWEAK

このパイプラインは、testenvironmentsの2つのステージのみを実行します。前者は、*TWEAK(微調整)*により、スキャナーを実行するために必要です。後者では、上記で定義したケース(ブランチへのプッシュ、デフォルトブランチへのマージ、タグ付け)ごとに異なる変数セットを持つ子パイプラインがトリガーされます。

ここで子パイプラインにstrategy:dependキーワードで依存を追加します。これにより、デプロイの完了後にGitLabのパイプラインビューが更新されます。

ご覧のとおりベースとなるジョブが無効になるように定義し、特定の変数とルールで拡張して、ターゲット環境ごとに単一のデプロイメントだけがトリガーされるようにしています。

定義済み変数に加え、定義する必要がある新たな2つのエントリを使用します。

  1. 各環境固有の変数../vars/$variables_file.tfvars
  2. 子パイプライン.gitlab-ci/.first-layer.gitlab-ci.ymlで定義

まずは、簡単な方、つまり変数の定義から始めましょう。

変数の定義

ここでは、2つのソリューションを組み合わせてTerraformに変数を提供します。

  • 1つ目は、.tfvarsファイルを使用して、機密性の低い入力をすべて行う方法です。これはGitLab内に保存する必要があります。

Terraformに変数を提供するための1つ目のソリューション

  • 2つ目は、プレフィックスにTF_VARを付けた環境変数を使用する方法です。変数を挿入するこの2つ目の方法は、変数をマスクし、保護し、さらにスコープの環境を設定するGitLabの機能とも関係する、機密情報の漏えいを防ぐ強力なソリューションです(本番環境のプライベートClassless Inter-Domain Routing(CIDR)で非常に機密性が高いデータをやり取りすると考えられる場合は、この方法で保護すれば、本番環境、および保護ブランチやタグに対して実行されるパイプラインでのみ利用できるようにし、ジョブのログでその値がマスクされるようにすることができます)。

Terraformに変数を提供するための2つ目のソリューション

また、各変数ファイルを変更できるユーザーを設定するために、CODEOWNERSファイルで各変数ファイルを管理する必要があります。

[Production owners] 
vars/production.tfvars @operations-group

[Staging owners]
vars/staging.tfvars @odupre @operations-group

[CodeOwners owners]
CODEOWNERS @odupre

この記事は、Terraformのトレーニング用ではないため、詳しく説明せず、ここではvars/review.tfvarsファイルを紹介するだけに留めます。当然ながら、これに続く環境ファイルもほぼ同じです。ここでは機密性の低い変数とその値を設定するだけです。

aws_vpc_cidr = "10.1.0.0/16"
aws_public_subnet_cidr = "10.1.1.0/24"
aws_private_subnet_cidr = "10.1.2.0/24"

子パイプライン

実際の作業はこのパイプライン内で行われます。そのため、最初のパイプラインよりも少し複雑です。しかしながら、力を合わせれば何でも乗り越えられます!

メインパイプラインの定義で説明したように、ダウンストリームパイプラインは.gitlab-ci/.first-layer.gitlab-ci.ymlで宣言されています。

ファイルで宣言されているダウンストリームパイプライン

小さなステップに分けて説明します。最後に全体像が見えるはずです。

Terraformコマンドを実行してコードを保護する

まずは、Terraformのパイプラインを実行したいと思います。GitLabはオープンソースであるため、Terraform用のテンプレートもオープンソースです。そのため、このテンプレートを含めるだけで済みます。以下のスニペットを使用して行えます。

include:
  - template: Terraform.gitlab-ci.yml

このテンプレートは、planとapplyが行われる前に、Terraformによるフォーマットのチェックとコードの検証を実行します。デプロイしたものを破棄することもできます。

さらに、GitLabは統合された単一のDevSecOpsプラットフォームであるため、このテンプレート内に2つのセキュリティスキャナーを自動的に組み込み、コード内の潜在的な脅威を検出し、次の環境にデプロイされる前に警告を発します。

これでコードの確認、保護、ビルド、デプロイが完了したので、いくつかの便利な技をご紹介します。

ジョブ間でキャッシュを共有する

ジョブの結果をキャッシュして、後続のパイプラインジョブで再利用します。これはとても簡単で、以下のコードを追加するだけで行えます。

default:
  cache:  # Use a shared cache or tagged runners to ensure terraform can run on apply and destroy
    - key: cache-$CI_COMMIT_REF_SLUG
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
      paths:
        - .

ここでは、コミットごとに異なるキャッシュを定義し、必要に応じてmainブランチ名にフォールバックするようにします。

使用しているテンプレートをよく見ると、ジョブの実行タイミングを制御するルールがあることがわかります。全ブランチですべての制御(QAとセキュリティの両方)を実行したいと思います。そのため、次にこれらの設定を上書きします。

すべてのブランチで制御を実行する

GitLabテンプレートは強力な機能で、テンプレートの一部のみを上書きできます。品質チェックとセキュリティチェックが必ず実行されるよう、一部のジョブのルールを上書きしたいと思います。これらのジョブ向けに定義するその他すべては、テンプレートで定義された内容のままにします。

fmt:
  rules:
    - when: always

validate:
  rules:
    - when: always

kics-iac-sast:
  rules:
    - when: always

iac-sast:
  rules:
    - when: always

これで品質とセキュリティの制御を実施できたため、ワークフロー内のメインの環境(integrationとstaging)とreview環境の動作に違いを付けたいと思います。まずはメインの環境の振る舞いを定義し、review環境用にこの設定を微調整していきましょう。

integrationとstaging環境への継続的デプロイ

前述のように、この2つの環境にmainブランチとタグをデプロイしたいため、そのように制御するルールをbuilddeployの両方のジョブに追加します。そして、integration環境でのみdestroyを有効にします。staging環境は重要度が高いため、ワンクリックで削除できないようにします。この操作はエラーを引き起こしやすく、避けたいと考えています。

最後に、deployジョブをdestroyジョブにリンクして、GitLab GUIから直接環境をstopできるようにします。

ここで使用するGIT_STRATEGYは、破棄する際にRunner内のソースブランチからコードが取得されることを防ぎます。これは、ブランチが手動で削除された場合は失敗するため、キャッシュを使用して、Terraformの命令を実行するために必要なものすべてを取得します。

build:  # terraform plan
  environment:
    name: $TF_STATE_NAME
    action: prepare
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

deploy: # terraform apply --> automatically deploy on corresponding env (integration or staging) when merging to default branch or tagging. Second layer environments (qa and production) will be controlled manually
  environment: 
    name: $TF_STATE_NAME
    action: start
    on_stop: destroy
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

destroy:
  extends: .terraform:destroy
  variables:
    GIT_STRATEGY: none
  dependencies:
    - build
  environment:
    name: $TF_STATE_NAME
    action: stop
  rules:
    - if: $CI_COMMIT_TAG  # Do not destroy production
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $TF_DESTROY == "true" # Manually destroy integration env.
      when: manual

前述のとおり、これはintegrationstaging環境へのデプロイというニーズに即しています。しかしながら、デベロッパーがほかの人に影響を及ぼさずに、自分のコードに触れて検証できる一時的な環境がまだ不足しています。そのため、次はreview環境へのデプロイを行います。

review環境への継続的デプロイ

review環境へのデプロイは、integrationstaging環境へのデプロイと大差はありません。そこで、ここでもGitLabの機能を活用して、ジョブ定義の一部のみを上書きします。

まずは、これらのジョブがフィーチャーブランチでのみ実行されるようルールを設定します。

次に、deploy_reviewジョブをdestroy_reviewジョブにリンクします。これにより、GitLabユーザーインターフェイスから手動で環境を停止できるようになりますが、さらに重要なのは、フィーチャーブランチの完了時に環境の破棄が自動的にトリガーされるようになります。これは、運用にかかる費用を抑えるのに効果的な、優れたFinOpsプラクティスです。

Terraformでは、インフラストラクチャの構築時と同様に、破棄する際にもplanファイルが必要なため、destroy_reviewからbuild_reviewに依存を追加して、そのアーティファクトを取得します。

最後に、ご覧のとおり、環境の名前を$environmentに設定します。これは、メインパイプラインreview/$CI_COMMIT_REF_SLUGに設定され、trigger:forward:yaml_variables:trueという命令により、その子パイプラインに転送されます。

build_review:
  extends: build
  rules:
    - if: $CI_COMMIT_TAG
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: on_success

deploy_review:
  extends: deploy
  dependencies:
    - build_review
  environment:
    name: $environment
    action: start
    on_stop: destroy_review
    # url: https://$CI_ENVIRONMENT_SLUG.example.com
  rules:
    - if: $CI_COMMIT_TAG
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: on_success

destroy_review:
  extends: destroy
  dependencies:
    - build_review
  environment:
    name: $environment
    action: stop
  rules:
    - if: $CI_COMMIT_TAG  # Do not destroy production
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH   # Do not destroy staging
      when: never
    - when: manual

さて、まとめると、これで次のことを行うパイプラインができました。

  • 一時的なreview環境へのデプロイ。フィーチャーブランチの完了時に、自動的にクリーンアップされます
  • デフォルトブランチからintegrationへの継続的デプロイ
  • タグからstagingへの継続的デプロイ

さらにレイヤを追加し、今回は手動でのトリガーをもとにqaproduction環境にデプロイされるようにしましょう。

qaとproduction環境への継続的デプロイ

誰もが本番環境に継続的デプロイしたいわけではないため、次の2つのデプロイには手動による検証を追加します。単にCDの観点で考えた場合、このトリガーを追加することはありませんが、ほかのトリガーからジョブを実行する方法を学ぶ機会として捉えてください。

これまでデプロイを実行する際は、必ずメインパイプラインから子パイプラインを開始してきました。

デフォルトブランチとタグからさらにデプロイを実行したいため、これらの追加ステップ用に別のレイヤを追加します。新たな手順は必要ありません。メインパイプラインで行ったのとまったく同じプロセスを再度繰り返します。この方法だと、必要な数だけレイヤを操作できます。中には最大で9つの環境がある例も見たことがあります。環境の数を抑えることの利点についてはあらためて説明しませんが、このプロセスを使用することで、初期段階から最終的なデリバリーまで、同じパイプラインを非常に簡単に実装できます。その上、パイプラインの定義をシンプルに保ちつつ、コストをかけずに維持できる小さな塊に分割可能です。

ここでは変数の競合を防ぐために、新しいvar名を使用してTerraformの状態と入力ファイルを識別しています。

.2nd_layer:
  stage: 2nd_layer
  variables:
    TF_ROOT: terraform
  trigger:
    include: .gitlab-ci/.second-layer.gitlab-ci.yml
    # strategy: depend            # Do NOT wait for the downstream pipeline to finish to mark upstream pipeline as successful. Otherwise, all pipelines will fail when reaching the pipeline timeout before deployment to 2nd layer.
    forward:
      yaml_variables: true      # Forward variables defined in the trigger job
      pipeline_variables: true  # Forward manual pipeline variables and scheduled pipeline variables

qa:
  extends: .2nd_layer
  variables:
    TF_STATE_NAME_2: qa
    environment: $TF_STATE_NAME_2
    TF_CLI_ARGS_plan_2: "-var-file=../vars/$TF_STATE_NAME_2.tfvars"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

production:
  extends: .2nd_layer
  variables:
    TF_STATE_NAME_2: production
    environment: $TF_STATE_NAME_2
    TF_CLI_ARGS_plan_2: "-var-file=../vars/$TF_STATE_NAME_2.tfvars"
  rules:
    - if: $CI_COMMIT_TAG

ここで重要なテクニックは、新しいダウンストリームパイプラインに使用するstrategyの設定です。trigger:strategyはデフォルトの値のままにしておきます。そうしなければ、メインパイプラインは、孫パイプラインが完了するまで待機することになります。手動トリガーだと、非常に長い時間かかり、パイプラインダッシュボードが読みづらく、理解しにくくなる可能性があります。

ここでインクルードした.gitlab-ci/.second-layer.gitlab-ci.ymlファイルが何なのか疑問に感じた方もいらっしゃるかもしれません。こちらは次のセクションで説明します。

1つ目のレイヤのパイプラインに関する全定義

1つ目のレイヤの全詳細(.gitlab-ci/.first-layer.gitlab-ci.ymlに保存)を確認したい場合は、以下のセクションを参照してください。

variables:
  TF_VAR_aws_ami_id: $AWS_AMI_ID
  TF_VAR_aws_instance_type: $AWS_INSTANCE_TYPE
  TF_VAR_aws_default_region: $AWS_DEFAULT_REGION

include:
  - template: Terraform.gitlab-ci.yml

default:
  cache:  # Use a shared cache or tagged runners to ensure terraform can run on apply and destroy
    - key: cache-$CI_COMMIT_REF_SLUG
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
      paths:
        - .

stages:
  - validate
  - test
  - build
  - deploy
  - cleanup
  - 2nd_layer       # Use to deploy a 2nd environment on both the main branch and on the tags

fmt:
  rules:
    - when: always

validate:
  rules:
    - when: always

kics-iac-sast:
  rules:
    - if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
      when: never
    - if: $SAST_EXCLUDED_ANALYZERS =~ /kics/
      when: never
    - when: on_success

iac-sast:
  rules:
    - if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
      when: never
    - if: $SAST_EXCLUDED_ANALYZERS =~ /kics/
      when: never
    - when: on_success

###########################################################################################################
## Integration env. and Staging. env
##  * Auto-deploy to Integration on merge to main.
##  * Auto-deploy to Staging on tag.
##  * Integration can be manually destroyed if TF_DESTROY is set to true.
##  * Destroy of next env. is not automated to prevent errors.
###########################################################################################################
build:  # terraform plan
  environment:
    name: $TF_STATE_NAME
    action: prepare
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

deploy: # terraform apply --> automatically deploy on corresponding env (integration or staging) when merging to default branch or tagging. Second layer environments (qa and production) will be controlled manually
  environment: 
    name: $TF_STATE_NAME
    action: start
    on_stop: destroy
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG

destroy:
  extends: .terraform:destroy
  variables:
    GIT_STRATEGY: none
  dependencies:
    - build
  environment:
    name: $TF_STATE_NAME
    action: stop
  rules:
    - if: $CI_COMMIT_TAG  # Do not destroy production
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $TF_DESTROY == "true" # Manually destroy integration env.
      when: manual
###########################################################################################################

###########################################################################################################
## Dev env.
##  * Temporary environment. Lives and dies with the Merge Request.
##  * Auto-deploy on push to feature branch.
##  * Auto-destroy on when Merge Request is closed.
###########################################################################################################
build_review:
  extends: build
  rules:
    - if: $CI_COMMIT_TAG
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: on_success

deploy_review:
  extends: deploy
  dependencies:
    - build_review
  environment:
    name: $environment
    action: start
    on_stop: destroy_review
    # url: https://$CI_ENVIRONMENT_SLUG.example.com
  rules:
    - if: $CI_COMMIT_TAG
      when: never
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: on_success

destroy_review:
  extends: destroy
  dependencies:
    - build_review
  environment:
    name: $environment
    action: stop
  rules:
    - if: $CI_COMMIT_TAG  # Do not destroy production
      when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH   # Do not destroy staging
      when: never
    - when: manual
###########################################################################################################

###########################################################################################################
## Second layer
##  * Deploys from main branch to qa env.
##  * Deploys from tag to production.
###########################################################################################################
.2nd_layer:
  stage: 2nd_layer
  variables:
    TF_ROOT: terraform
  trigger:
    include: .gitlab-ci/.second-layer.gitlab-ci.yml
    # strategy: depend            # Do NOT wait for the downstream pipeline to finish to mark upstream pipeline as successful. Otherwise, all pipelines will fail when reaching the pipeline timeout before deployment to 2nd layer.
    forward:
      yaml_variables: true      # Forward variables defined in the trigger job
      pipeline_variables: true  # Forward manual pipeline variables and scheduled pipeline variables

qa:
  extends: .2nd_layer
  variables:
    TF_STATE_NAME_2: qa
    environment: $TF_STATE_NAME_2
    TF_CLI_ARGS_plan_2: "-var-file=../vars/$TF_STATE_NAME_2.tfvars"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

production:
  extends: .2nd_layer
  variables:
    TF_STATE_NAME_2: production
    environment: $TF_STATE_NAME_2
    TF_CLI_ARGS_plan_2: "-var-file=../vars/$TF_STATE_NAME_2.tfvars"
  rules:
    - if: $CI_COMMIT_TAG
###########################################################################################################

この段階で、すでに3つの環境に問題なくデプロイしています。個人的にはこのアプローチが理想的でおすすめです。ただし、もっと多くの環境が必要であれば、CDパイプラインに追加してください。

trigger:includeというキーワードでダウンストリームパイプラインをインクルードしていることはすでにお気づきだと思います。これにより、.gitlab-ci/.second-layer.gitlab-ci.ymlファイルがインクルードされます。ほぼ同じパイプラインを実行したいため、当然ながら先ほど詳しく説明したものと内容は非常に似ています。ここで孫パイプラインを定義する主な利点は、それ自体が独立しているため、変数やルールを非常に定義しやすくことです。

孫パイプライン

この2つ目のレイヤとなるパイプラインは、まったく新しいパイプラインです。そのため、1つ目のレイヤの定義を模倣しつつ、以下を行う必要があります。

上述のとおり、TF_STATE_NAMETF_CLI_ARGS_planは、メインパイプラインから子パイプラインに渡されています。これらの値を子パイプラインから孫パイプラインに渡すには、別の変数名が必要でした。そのため、子パイプラインでは変数名の末尾に_2を付け足し、before_scriptの実行中に適切な変数に値をコピーしています。

各ステップについては説明済みであるため、ここでは細かいところは省き、直接グローバルな2つ目のレイヤの定義(.gitlab-ci/.second-layer.gitlab-ci.ymlに保存)の全体像をご確認ください。

# Use to deploy a second environment on both the default branch and the tags.

include:
  template: Terraform.gitlab-ci.yml

stages:
  - validate
  - test
  - build
  - deploy

fmt:
  rules:
    - when: never

validate:
  rules:
    - when: never

kics-iac-sast:
  rules:
    - if: $SAST_DISABLED == 'true' || $SAST_DISABLED == '1'
      when: never
    - if: $SAST_EXCLUDED_ANALYZERS =~ /kics/
      when: never
    - when: always

###########################################################################################################
## QA env. and Prod. env
##  * Manually trigger build and auto-deploy in QA
##  * Manually trigger both build and deploy in Production
##  * Destroy of these env. is not automated to prevent errors.
###########################################################################################################
build:  # terraform plan
  cache:  # Use a shared cache or tagged runners to ensure terraform can run on apply and destroy
    - key: $TF_STATE_NAME_2
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
      paths:
        - .
  environment:
    name: $TF_STATE_NAME_2
    action: prepare
  before_script:  # Hack to set new variable values on the second layer, while still using the same variable names. Otherwise, due to variable precedence order, setting new value in the trigger job, does not cascade these new values to the downstream pipeline
    - TF_STATE_NAME=$TF_STATE_NAME_2
    - TF_CLI_ARGS_plan=$TF_CLI_ARGS_plan_2
  rules:
    - when: manual

deploy: # terraform apply
  cache:  # Use a shared cache or tagged runners to ensure terraform can run on apply and destroy
    - key: $TF_STATE_NAME_2
      fallback_keys:
        - cache-$CI_DEFAULT_BRANCH
      paths:
        - .
  environment: 
    name: $TF_STATE_NAME_2
    action: start
  before_script:  # Hack to set new variable values on the second layer, while still using the same variable names. Otherwise, due to variable precedence order, setting new value in the trigger job, does not cascade these new values to the downstream pipeline
    - TF_STATE_NAME=$TF_STATE_NAME_2
    - TF_CLI_ARGS_plan=$TF_CLI_ARGS_plan_2
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG && $TF_AUTO_DEPLOY == "true"
    - if: $CI_COMMIT_TAG
      when: manual
###########################################################################################################

これで準備完了です。 本番環境にデプロイする前に、ジョブの実行を管理する方法は自由に変更できます。たとえば、GitLabの機能を活用して、本番環境へのデプロイ前にジョブを遅延させる設定をすることも可能です。

実際に試す

ついに目標を達成できました。フィーチャーブランチmainブランチタグだけで、5つの異なる環境へのデプロイを管理できるようになりました。

  • パイプラインの効率とセキュリティを確保するために、GitLabのオープンソーステンプレートを集中的に再利用しました。
  • GitLabテンプレートの機能を活用して、個別に制御が必要なブロックだけを上書きしました。
  • パイプラインを小さな塊に分割し、ニーズに完全に合うようにダウンストリームパイプラインを制御しました。

ここからは、自由に進めてください。たとえば、trigger:rules:changesキーワードを使って、ソフトウェアのソースコードのダウンストリームパイプラインをトリガーするように、メインパイプラインを簡単に更新することも可能です。また、発生した変更に応じて、別のテンプレートを使用できます。その方法はまた別の機会に。

ご意見をお寄せください

このブログ記事を楽しんでいただけましたか?ご質問やフィードバックがあればお知らせください。GitLabコミュニティフォーラムで新しいトピックを作成して、ご意見をお聞かせください。

フォーチュン100企業の50%以上がGitLabを信頼

より優れたソフトウェアをより速く提供

インテリジェントなDevSecOpsプラットフォームで

チームの可能性を広げましょう。