FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

Terraform プロジェクトの .gitlab-ci.yml テンプレート

中途3年目の堀越です。

今年は Terraform で Infrastructure as code デビューしました。
コードは GitLab で管理していたのでその際、作った .gitlab-ci.yml についてお話します。

環境毎に terraform plan, terraform apply できれば基本的なCIの動作は満たせるしテンプレ化しそうだなと思ったのが本ブログを書くに至った背景です。

.gitlab-ci.yml is 何???

GitLab Runner がプロジェクトのジョブを管理するために使用するファイルです。
プロジェクトのルートディレクトリに配置することでCIを設定できます。

docs.gitlab.com

プロジェクト構成

vpc を起動してその中に ec2 を開発・本番環境の2台立ち上げるシンプルなプロジェクトです。 vpc/example.tf には VPCenvironment/example.tf には EC2 を管理する Terraform コードを定義します。

プロジェクトのルートディレクトリに .gitlab-ci.yml を配置します。

├── environment
│   ├── example.tf
├── vpc
│     └── example.tf
├──.gitlab-ci.yml

Terraform のコード

vpc/example.tf

vpc 構築の Terraform コードです。

locals {
  name = "tf-for-gitlab"
}

terraform {
  backend "s3" {
    bucket = "t-horikoshi-bucket"
    key    = "example.tfstate"
    region = "ap-northeast-1"
  }
}

resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"
  tags {
    Name = "${local.name}"
  }
}

resource "aws_security_group" "example" {
  name        = "${local.name}"
  vpc_id      = "${aws_vpc.example.id}"
  tags {
    Name = "${local.name}"
  }
}

output "security-group-ids" {
  value = ["${aws_security_group.example.id}"]
}

output "vpc_id" {
  value = "${aws_vpc.example.id}"
}

ec2 作成時に所属する vpc の情報が必要なので、vpc_idsecurity-group-idsoutput し example.tfstate ファイルへ出力します。

tfstate ファイルの保存先である S3 の情報を backend に指定します。*1

environment/example.tf

ec2 インスタンスvpc 内に立ち上げる Terraform のコードです。

locals {
  stage  = "${terraform.workspace == "prod" ? "prod" : "dev"}"
}

data "terraform_remote_state" "example" {
  backend = "s3"
  config {
    bucket = "t-horikoshi-bucket"
    key = "example.tfstate"
    region = "ap-northeast-1"
  }
}

resource "aws_subnet" "example-a" {
  cidr_block = "10.0.1.0/24"
  vpc_id = "${data.terraform_remote_state.example.vpc_id}"
  tags {
    Name = "example-a"
  }
}

resource "aws_instance" "example" {
  ami = "${var.ami}"
  instance_type = "t2.micro"
  subnet_id = "${aws_subnet.example-a.id}"
  vpc_security_group_ids = ["${data.terraform_remote_state.example.security-group-ids}"]
  tags {
    Name = "tf-for-gitlab-${local.stage}"
  }
}

variable "ami" {
  type ="string"
  default = "ami-0a2de1c3b415889d2"
}

今回は、devprod という環境の切り分けを terraform workspace で実現します。
localsstage 変数に選択した workspace を読み込みます。

data に さきほどの vpc 構築時に設定した s3、tfstate ファイルの情報を読み込めるように設定します。

aws_subnetvpc_id を、aws_instancesecurity-group-ids をそれぞれ設定します。

.gitlab-ci.yml

image:
  name: hashicorp/terraform:0.11.11
  entrypoint: [""]

.artifacts: &artifacts
  paths:
    - 'vpc/.terraform'
    - 'environment/.terraform'

stages:
  - fmt
  - init
  - plan
  - apply

fmt:
  stage: fmt
  script:
    - cd vpc && terraform fmt -check=true
    - cd ../environment && terraform fmt -check=true

init:
  stage: init
  script:
    - cd vpc && terraform init
    - cd ../environment && terraform init
  artifacts: *artifacts

plan_vpc:
  stage: plan
  script:
    - cd vpc && terraform plan
  artifacts: *artifacts

.plan_env_job: &plan_env_job
  stage: plan
  script:
    - cd environment
    - terraform workspace new $ENV && terraform workspace select $ENV && terraform plan
  artifacts: *artifacts

plan_env_dev:
   <<: *plan_env_job
   variables:
     ENV: dev

plan_env_prod:
  <<: *plan_env_job
  variables:
    ENV: prod

apply_vpc:
  stage: apply
  script:
    - cd vpc && terraform apply -auto-approve
  when: manual
  artifacts: *artifacts
  only:
    - master

.apply_env_job: &apply_env_job
  stage: apply
  script:
    - cd environment
    - terraform workspace select $ENV && terraform apply -auto-approve
  when: manual
  artifacts: *artifacts

apply_env dev:
  <<: *apply_env_job
  variables:
    ENV: dev

apply_env prod:
  <<: *apply_env_job
  variables:
    ENV: prod
  only:
    - master

下記より、詳細に見ていきます。

image
image:
  name: hashicorp/terraform:0.11.11
  entrypoint: [""]

HashiCorp が出してる docker image を指定します。

デフォルトだと entrypointterraform が指定されており、
コンテナ内で Pipeline の job が動かせないのでオーバーライドしておきます。

Terraform はアップデートが早いので version は指定するようにしてます。

.artifacts
.artifacts: &artifacts
  paths:
    - 'vpc/.terraform'
    - 'environment/.terraform'

vpc, environment の成果物を stage 間で利用できるようにするための設定です。
terraform init, terraform apply 後の成果物が出力される .terraform ディレクトリを指定します。

stages
stages:
  - fmt
  - init
  - plan
  - apply

Pipeline における各 stage を設定します。
Terraform のプロジェクトなので、fmt >init > plan > apply という構成になっています。

fmt
fmt:
  stage: fmt
  script:
    - cd vpc && terraform fmt -check=true
    - cd ../environment && terraform fmt -check=true

Terraform のコードをフォーマットチェックする stage です。
terraform fmt -check=true とすることでフォーマットに不備があった場合は失敗させるようにしてます。

init
init:
  stage: init
  script:
    - cd vpc && terraform init
    - cd ../environment && terraform init
  artifacts: *artifacts

terraform init して vpc, environment をそれぞれイニシャライズする stage です。

plan
plan_vpc:
  stage: plan
  script:
    - cd vpc && terraform plan
  artifacts: *artifacts

.plan_env_job: &plan_env_job
  stage: plan
  script:
    - cd environment
    - terraform workspace new $ENV || terraform workspace select $ENV && terraform plan
  artifacts: *artifacts

plan_env_dev:
   <<: *plan_env_job
   variables:
     ENV: dev

plan_env_prod:
  <<: *plan_env_job
  variables:
    ENV: prod

terraform plan するステージです。

vpc は一つ立ち上げるだけなのでジョブも一つですが、
environment では dev, prod と環境を分けて構築したいので、
plan_env_dev, plan_env_prod とそれぞれジョブを定義しています。

YAML のアンカーという機能を利用して .plan_env_job という共通ジョブを作成しています。
ENV によって workspace 毎にジョブを実行できるようにしました。

apply
apply_vpc:
  stage: apply
  script:
    - cd vpc && terraform apply -auto-approve
  when: manual
  artifacts: *artifacts
  only:
    - master

.apply_env_job: &apply_env_job
  stage: apply
  script:
    - cd environment
    - terraform workspace select $ENV && terraform apply -auto-approve
  when: manual
  artifacts: *artifacts

apply_env dev:
  <<: *apply_env_job
  variables:
    ENV: dev

apply_env prod:
  <<: *apply_env_job
  variables:
    ENV: prod
  only:
    - master

environment を環境毎に分けたり、ジョブの定義をアンカー使って共通化したりなど、
基本的には plan と同じ仕組みになっています。

terraform apply すると実際にプロビジョニングが始まってしまうので、
when: manual を指定して手動実行するようにしてます。

また、only を指定して ジョブを実行できるブランチを master に限定しました。

GitLabに環境変数を設定

aws credential などのセキュアな情報はコードにコミットしたくないので、
GitLab の環境変数に設定しました。

f:id:t_horikoshi:20181215173338p:plain

完成した Pipeline

これまでのコードを git repository に push すると下記のような Pipeline ができあがります。

f:id:t_horikoshi:20181215174532p:plain

まとめ

今回紹介した GitLab の CI/CD 機能はまだまだ触りの部分です。
他にも便利そうな機能があるので使いこなしていきたいですね。

また、手動で作られたインフラストラクチャを管理するつらみは既知の事実ですが、
コード管理されていると再現性も高まるしだいぶいい感じです。

それでは、Enjoy Infrastructure as code!!!

*1:S3バケットは事前に作成されていることを前提としています。