更新日:2025年6月12日
9分で読めます
使用するGitLabワークフローを最小限に抑えつつ、複数の環境(事前の準備なしに一時的に利用できるsandboxなど)への継続的デプロイを管理する方法を解説します。
DevSecOpsチームでは、複数の環境にまたがる継続的デプロイを管理する機能が必要となることがあります。その場合、ワークフローを変更せずに、デプロイを行えるようにする必要があります。GitLab DevSecOpsプラットフォームなら、事前の準備なしに一時的に利用できるsandboxを使用して工数を最小限に抑えるアプローチなどを通じて、こうしたニーズに対応できます。この記事では、Terraformを使って複数の環境上でインフラの継続的デプロイを行う方法についてご紹介します。
この手法は、PulumiやAnsibleのような別の技術を使用したInfrastructure as Code(IaC)でも、どのような言語で書かれたソースコードでも、または多様な言語が混在するモノレポであっても、あらゆるプロジェクトに簡単に適用できます。
このチュートリアルの終了時には、以下のような環境をデプロイするパイプラインが完成します。
この記事で使用されるフローチャートの説明は以下のとおりです。
- 角が丸いボックスは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コードから始めましょう。
仮想ネットワークである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]
review/feature_branch
に継続的にデプロイされます。これは、デベロッパーと運用チームが誰にも影響を与えることなくコードをテストできる安全な環境です。また、ここでコードレビューやスキャナーの実行などのプロセスを実施し、コードの品質とセキュリティが許容範囲内であることを確認し、資産が危険にさらされることのないようにします。このブランチでデプロイされたインフラストラクチャは、ブランチが閉じられると自動的に破棄されます。これにより予算範囲内に収めやすくなります。flowchart LR A(main) -->|新機能| B(feature_X) B -->|自動デプロイ| C[review/feature_X] B -->|マージ| D(main) C -->|破棄| D
integration
環境です。この環境をもう少し安定させるために、削除は自動化されておらず、手動でトリガーできるようになっています。flowchart LR D(main) -->|自動デプロイ| E[integration]
qa
環境にデプロイされます。ここでパイプラインからの削除を防ぐルールを設定します。何しろすでに3つ目のこの環境はかなり安定しているはずなので、このルールは誤って削除されるのを防ぐことを目的とします。貴社のプロセスに合わせて、お好きなようなルールを調整してください。flowchart LR D(main)-->|自動デプロイ| E[integration] E -->|手動| F[qa]
staging
環境へのデプロイが即座にトリガーされます。flowchart LR D(main) -->|タグ付け| G(X.Y.Z) F[qa] -->|検証| G G -->|自動デプロイ| H[staging]
production
に到達しました。インフラストラクチャに関して言うと、(10%や25%など)段階的にデプロイするのは難しい場合が多いため、インフラストラクチャ全体をデプロイします。ただし、この最後のステップで行う手動トリガーで、このデプロイを制御します。そして、この極めて重要な環境を最大限に制御できるようにするために、保護環境として管理します。flowchart LR H[staging] -->|手動| I{plan} I -->|手動| J[production]
上記のワークフローを実装するために、2つのダウンストリームパイプラインとともにパイプラインを構築します。
まずは、メインパイプラインから始めましょう。メインパイプラインは、フィーチャーブランチへのプッシュ、デフォルトブランチへのマージ、またはタグ付けが発生すると、必ず自動的にトリガーされます。このパイプラインによって、dev
、integration
、staging
環境に対する真の継続的デプロイを実現できます。プロジェクトのルートにある.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
このパイプラインは、test
とenvironments
の2つのステージのみを実行します。前者は、*TWEAK(微調整)*により、スキャナーを実行するために必要です。後者では、上記で定義したケース(ブランチへのプッシュ、デフォルトブランチへのマージ、タグ付け)ごとに異なる変数セットを持つ子パイプラインがトリガーされます。
ここで子パイプラインにstrategy:dependキーワードで依存を追加します。これにより、デプロイの完了後にGitLabのパイプラインビューが更新されます。
ご覧のとおりベースとなるジョブが無効になるように定義し、特定の変数とルールで拡張して、ターゲット環境ごとに単一のデプロイメントだけがトリガーされるようにしています。
定義済み変数に加え、定義する必要がある新たな2つのエントリを使用します。
まずは、簡単な方、つまり変数の定義から始めましょう。
ここでは、2つのソリューションを組み合わせてTerraformに変数を提供します。
TF_VAR
を付けた環境変数を使用する方法です。変数を挿入するこの2つ目の方法は、変数をマスクし、保護し、さらにスコープの環境を設定するGitLabの機能とも関係する、機密情報の漏えいを防ぐ強力なソリューションです(本番環境のプライベートClassless Inter-Domain Routing(CIDR)で非常に機密性が高いデータをやり取りすると考えられる場合は、この方法で保護すれば、本番環境、および保護ブランチやタグに対して実行されるパイプラインでのみ利用できるようにし、ジョブのログでその値がマスクされるようにすることができます)。また、各変数ファイルを変更できるユーザーを設定するために、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のパイプラインを実行したいと思います。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環境用にこの設定を微調整していきましょう。
前述のように、この2つの環境にmainブランチとタグをデプロイしたいため、そのように制御するルールをbuild
とdeploy
の両方のジョブに追加します。そして、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
前述のとおり、これはintegration
とstaging
環境へのデプロイというニーズに即しています。しかしながら、デベロッパーがほかの人に影響を及ぼさずに、自分のコードに触れて検証できる一時的な環境がまだ不足しています。そのため、次はreview
環境へのデプロイを行います。
review環境へのデプロイは、integration
やstaging
環境へのデプロイと大差はありません。そこで、ここでも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
さて、まとめると、これで次のことを行うパイプラインができました。
integration
への継続的デプロイstaging
への継続的デプロイさらにレイヤを追加し、今回は手動でのトリガーをもとに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つ目のレイヤの全詳細(.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つ目のレイヤの定義を模倣しつつ、以下を行う必要があります。
destroy
ステージは自動化されないようになったことにご注意ください。上述のとおり、TF_STATE_NAME
とTF_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つの異なる環境へのデプロイを管理できるようになりました。
ここからは、自由に進めてください。たとえば、trigger:rules:changesキーワードを使って、ソフトウェアのソースコードのダウンストリームパイプラインをトリガーするように、メインパイプラインを簡単に更新することも可能です。また、発生した変更に応じて、別のテンプレートを使用できます。その方法はまた別の機会に。