TerraformとGitLabによるコードとしてのインフラストラクチャ

モチベーション

GitLab内のTerraformインテグレーション機能は、GitOps / Infrastructure-as-Code (IaC)ワークフローをGitLabの認証と作成者に連携させることができます。 これらの機能は、チームがTerraformを導入するための参入障壁を下げ、GitLab内で効果的にコラボレーションし、Terraformのベストプラクティスをサポートすることに重点を置いています。

GitLabが管理するTerraformの状態

GitLab 13.0から導入されました

GitLabはTerraform HTTPバックエンドを使用して、ステートファイルをローカルストレージ(デフォルト)または選択したリモートストアに安全に保存します。

GitLabが管理するTerraformのステートバックエンドは、Terraformのステートを簡単かつセキュアに保存することができ、Amazon S3やGoogle Cloud Storageのような追加のリモートリソースを設定する必要がありません。 その機能は以下の通りです:

  • 転送中および停止中のステートファイルの暗号化をサポートします。
  • ロックとアンロックの状態。
  • リモートTerraformプランと適用実行。

GitLabが管理するTerraform Stateを始めるには、2つの異なるオプションがあります:

現地開発者の利用開始

terraform planterraform apply のコマンドだけをローカルマシンから実行するつもりであれば、これは簡単な方法です:

  1. GitLab インスタンスにプロジェクトを作成します。
  2. settings} 設定>一般に移動し、プロジェクト名とプロジェクトIDをメモしてください。
  3. TerraformプロジェクトのTerraformバックエンドを次のように定義します:

    terraform {
      backend "http" {
      }
    }
    
  4. api スコープでパーソナルアクセストークンを作成します。 Terraform バックエンドはリポジトリへのメンテナーのアクセス権を持つユーザーに制限されます。

  5. ローカルマシンで、terraform initを実行します。<YOUR-PROJECT-NAME><YOUR-PROJECT-ID><YOUR-USERNAME><YOUR-ACCESS-TOKEN> を該当する値に置き換えて、以下のオプションを渡します。 このコマンドはTerraformの状態を初期化し、その状態をGitLabプロジェクト内に保存します。この例では、gitlab.comを使っています:

    terraform init \
        -backend-config="address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>" \
        -backend-config="lock_address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>/lock" \
        -backend-config="unlock_address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>/lock" \
        -backend-config="username=<YOUR-USERNAME>" \
        -backend-config="password=<YOUR-ACCESS-TOKEN>" \
        -backend-config="lock_method=POST" \
        -backend-config="unlock_method=DELETE" \
        -backend-config="retry_wait_min=5"
    

次に、バックエンドを設定します。

GitLab CIを使い始めましょう。

ローカル開発から始めたくない場合は、GitLab CIを使ってterraform planterraform apply コマンドを実行することもできます。

次に、バックエンドを設定します。

バックエンドの設定

terraform init コマンドを実行した後、Terraform バックエンドと CI YAML ファイルを設定する必要があります:

重要:Terraformバックエンドはリポジトリへのメンテナーのアクセス権を持つユーザーに制限されています。
  1. Terraformプロジェクトで、.tf ファイル (backend.tfなど) に以下のコードブロックを追加してHTTPバックエンドを定義し、リモートバックエンドを定義します:

    terraform {
      backend "http" {
      }
    }
    
  2. プロジェクトリポジトリのルートディレクトリで、.gitlab-ci.yaml ファイルを設定します。この例では、gitlab-terraform ヘルパーを含むビルド済みイメージを使用します。サポートされている Terraform のバージョンについては、GitLab Terraform Images プロジェクトを参照してください。

    image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest
    
  3. .gitlab-ci.yaml ファイルでは、開発を容易にするためにいくつかの環境変数を定義します。 このインスタンスでは、TF_STATE は Terraform のステートの名前(プロジェクトは複数のステートを持つことができます)、TF_ADDRESS はこのパイプラインを実行する GitLab インスタンス上のステートの URL、TF_ROOT は Terraform コマンドを実行するディレクトリです:

    variables:
      TF_STATE: ${CI_PROJECT_NAME}
      TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE}
      TF_ROOT: ${CI_PROJECT_DIR}/environments/cloudflare/production
    
    cache:
      key: ${TF_STATE}
      paths:
        - ${TF_ROOT}/.terraform
    
  4. before_scriptで、TF_ROOT

    before_script:
      - cd ${TF_ROOT}
    
    stages:
      - prepare
      - validate
      - build
      - deploy
    
    init:
      stage: prepare
      script:
        - gitlab-terraform init
    
    validate:
      stage: validate
      script:
        - gitlab-terraform validate
    
    plan:
      stage: build
      script:
        - gitlab-terraform plan
        - gitlab-terraform plan-json
      artifacts:
        name: plan
        paths:
          - ${TF_ROOT}/plan.cache
        reports:
          terraform: ${TF_ROOT}/plan.json
    
    apply:
      stage: deploy
      environment:
        name: production
      script:
        - gitlab-terraform apply
      dependencies:
        - plan
      when: manual
      only:
        - master
    
  5. プロジェクトを GitLab にプッシュすると、CI ジョブパイプラインが起動します。 このパイプラインはgitlab-terraform init,gitlab-terraform validate,gitlab-terraform plan コマンドを実行します。

上記のterraform コマンドの出力は、ジョブ・ログで見ることができるはずです。

プロジェクト例

GitLabとTerraformを使ってカスタムVPC内に基本的なAWS EC2をデプロイするリファレンスプロジェクトをご覧ください。

Terraform Planの情報をマージリクエストに出力します。

GitLab Terraform Reportアーティファクトを使うことで、terraform plan の実行の詳細をマージリクエストウィジェットに直接公開することができ、Terraformが作成、変更、破棄するリソースに関する統計を見ることができます。

GitLab Terraform Report のアーティファクトを設定する方法を探ってみましょう。gitlab-terraform plan-json が必要なアーティファクトを出力する、上記のようなgitlab-terraform ヘルパーを含むビルド済みイメージを使うか、以下のように手動で設定することができます:

  1. 簡単にするために、これらのファイルを複数回参照できるように、再利用可能な変数をいくつか定義しましょう:

    variables:
      PLAN: plan.cache
      PLAN_JSON: plan.json
    
  2. 軽量で柔軟なコマンドライン JSON プロセッサjqをインストールします。
  3. terraform plan 出力から抽出したい情報を解析する、特定のjq コマンドのエイリアスを作成します:

    before_script:
      - apk --no-cache add jq
      - alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
    
    注意: Bash を使用するディストリビューション (Ubuntu など) では、alias ステートメントは非インタラクティブモードでは展開されません。convert_report: command not foundというエラーでパイプラインが失敗する場合は、shopt コマンドをスクリプトに追加することで、エイリアスの展開を明示的に有効にできます:
    before_script:
      - shopt -s expand_aliases
      - alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
    
  4. terraform planterraform showを実行するscript を定義します。これらのコマンドは出力をパイプし、関連するビットをストア変数PLAN_JSONに変換します。この JSON はGitLab Terraform Report アーティファクトを作成するために使われます。 Terraform レポートは Terraformtfplan.json ファイルを取得します。収集された Terraform プランレポートはアーティファクトとして GitLab にアップロードされ、マージリクエストに表示されます。

    plan:
      stage: build
      script:
        - terraform plan -out=$PLAN
        - terraform show --json $PLAN | convert_report > $PLAN_JSON
      artifacts:
        reports:
          terraform: $PLAN_JSON
    

    ビルド済みイメージを使った完全な例については、.gitlab-ci.yamlファイルをご覧ください。

    複数のレポーターを表示する例については、.gitlab-ci.yaml 複数レポートファイルをご覧ください。

  5. パイプラインを実行すると、マージリクエストのウィジェットがこのように表示されます:

    Merge Request Terraform widget

  6. ウィジェットの[フルログを表示]ボタンをクリックすると、パイプラインログにあるプラン出力に直接アクセスできます:

    Terraform plan logs

.gitlab-ci.yaml ファイル

image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest

variables:
  TF_STATE: ${CI_PROJECT_NAME}
  TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE}
  TF_ROOT: ${CI_PROJECT_DIR}/environments/cloudflare/production

cache:
  key: ${TF_STATE}
  paths:
    - ${TF_ROOT}/.terraform

before_script:
  - cd ${TF_ROOT}

stages:
  - prepare
  - validate
  - build
  - deploy

init:
  stage: prepare
  script:
    - gitlab-terraform init

validate:
  stage: validate
  script:
    - gitlab-terraform validate

plan:
  stage: build
  script:
    - gitlab-terraform plan
    - gitlab-terraform plan-json
  artifacts:
    name: plan
    paths:
      - ${TF_ROOT}/plan.cache
    reports:
      terraform: ${TF_ROOT}/plan.json

apply:
  stage: deploy
  environment:
    name: production
  script:
    - gitlab-terraform apply
  dependencies:
    - plan
  when: manual
  only:
    - master

複数のTerraformプランレポーター

13.2以降、マージリクエストページに複数のレポートを表示することができます。 レポートにはartifact: name:も表示されます。 推奨される設定については、以下の例を参照してください。

image:
  name: registry.gitlab.com/gitlab-org/gitlab-build-images:terraform
  entrypoint:
    - '/usr/bin/env'
    - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'

cache:
  paths:
    - .terraform

stages:
  - build

.terraform-plan-generation:
  stage: build
  variables:
    PLAN: plan.tfplan
    JSON_PLAN_FILE: tfplan.json
  before_script:
    - cd ${TERRAFORM_DIRECTORY}
    - terraform --version
    - terraform init
    - apk --no-cache add jq
  script:
    - terraform validate
    - terraform plan -out=${PLAN}
    - terraform show --json ${PLAN} | jq -r '([.resource_changes[]?.change.actions?]|flatten)|{"create":(map(select(.=="create"))|length),"update":(map(select(.=="update"))|length),"delete":(map(select(.=="delete"))|length)}' > ${JSON_PLAN_FILE}
  artifacts:
    reports:
      terraform: ${TERRAFORM_DIRECTORY}/${JSON_PLAN_FILE}

review_plan:
  extends: .terraform-plan-generation
  variables:
    TERRAFORM_DIRECTORY: "review/"
  # Review will not include an artifact name

staging_plan:
  extends: .terraform-plan-generation
  variables:
    TERRAFORM_DIRECTORY: "staging/"
  artifacts:
    name: Staging

production_plan:
  extends: .terraform-plan-generation
  variables:
    TERRAFORM_DIRECTORY: "production/"
  artifacts:
    name: Production