🐳

Terraform, tfsec, tflint, aws-cli-v2, Docker(DooD)が使えるDocker環境を構築する

2022/07/17に公開

はじめに

業務でTerraformを利用してAWSのインフラ構築する際、
バージョン管理はtfenvを使い、その他諸々のアプリケーションの環境はDockerで構築していました
Terraformだけローカルマシン上に環境構築しなければならないのは微妙だし、
新規参画のメンバーにもちょっと優しくないです
なのでDockerさえ入っていればTerraformを利用できるような環境を構築していこうとおもいます

また今回は、
ECRの作成をトリガーにnull_resourceを作成してlocal-execdocker build & aws-cliECRにinitialのimageをpushする。
といったケースにも対応したいので、
Terraformだけでなくaws-cli-v2DooD(Docker outside of Docker)でDockerも利用できるようにします

2022/8/1追記
tfsectflintも実行できるようにしました

利用技術など

・docker, docker-compose
・Terraform(hashicorp/terraform:1.2.5)
aws-cli-v2
tfsec
tflint

※ソースコード全文はこちら
https://github.com/TadayoshiOtsuka/terraform_docker

最終的なディレクトリ構成はこんな感じです
Prod/Stgのような環境ごとにtfstateを分けて、
各リソースはmodules配下に定義し各環境のmain.tfから利用するパターンを想定しています

├── .gitignore
├── Dockerfile
├── Makefile
├── docker-compose.yaml
├── environments
│   ├── prod
│   │   ├── .env
│   │   ├── Makefile
│   │   ├── backend.tf
│   │   ├── logger
│   │   │   └── Dockerfile
│   │   ├── main.tf
│   │   ├── prod.tfvars
│   │   └── variables.tf
│   └── stg
│   │   ├── .env
│       ├── Makefile
│       ├── backend.tf
│       ├── logger
│       │   └── Dockerfile
│       ├── main.tf
│       ├── stg.tfvars
│       ├── terraform.tfstate
│       └── variables.tf
└── modules
    └── ecr
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

さっそく準備する

Dockerfileを準備する

Dockerfile
FROM hashicorp/terraform:1.2.5
ENV GLIBC_VER=2.34-r0

RUN apk update && \
    apk --no-cache add \
    docker-cli \
    binutils \
    make \
    curl && \
    curl -sL https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub -o /etc/apk/keys/sgerrand.rsa.pub && \
    curl -sLO https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VER}/glibc-${GLIBC_VER}.apk && \
    curl -sLO https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VER}/glibc-bin-${GLIBC_VER}.apk && \
    apk add --no-cache glibc-${GLIBC_VER}.apk glibc-bin-${GLIBC_VER}.apk

# tfsec
ENV TFSEC_VER=1.26.3
RUN curl -Lso tfsec https://github.com/tfsec/tfsec/releases/download/v${TFSEC_VER}/tfsec-linux-amd64 && \
    chmod +x tfsec && mv tfsec /usr/local/bin/

# tflint
ENV TFLINT_VER=0.39.1
RUN curl --fail --silent -L -o /tmp/tflint.zip https://github.com/terraform-linters/tflint/releases/download/v${TFLINT_VER}/tflint_linux_amd64.zip && \
    unzip /tmp/tflint.zip -d /tmp/ && \
    install -c -v /tmp/tflint /usr/local/bin/ && \
    rm /tmp/tflint*

# tflint rule
ENV TFLINT_RULE_SET_AWS_VER=0.15.0
RUN mkdir -p  ~/.tflint.d/plugins/github.com/terraform-linters/tflint-ruleset-aws/${TFLINT_RULE_SET_AWS_VER}/ && \
    curl --fail --silent -L -o /tmp/tflint-ruleset-aws.zip https://github.com/terraform-linters/tflint-ruleset-aws/releases/download/v${TFLINT_RULE_SET_AWS_VER}/tflint-ruleset-aws_linux_amd64.zip && \
    unzip /tmp/tflint-ruleset-aws.zip -d ~/.tflint.d/plugins/github.com/terraform-linters/tflint-ruleset-aws/${TFLINT_RULE_SET_AWS_VER}/ && \
    rm /tmp/tflint-ruleset-aws.zip

ENTRYPOINT [ "ash" ]

docker-compose.yamlを準備する

一旦動作確認のため、以下のように定義します
個人的にvolumeはlong syntaxで書くのが読みやすくて好きです

docker-compose.yaml
version: "3.8"

services:
  terraform-prod:
    build:
      context: .
      dockerfile: Dockerfile
    working_dir: /terraform/environments/prod
    tty: true
    env_file:
      - environments/prod/.env
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock

  terraform-stg:
    build:
      context: .
      dockerfile: Dockerfile
    working_dir: /terraform/environments/stg
    tty: true
    env_file:
      - environments/stg/.env
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock

※のちほどProd/Stgごとに必要なディレクトリ/ファイルをマウントして起動できるようにします

  • env_fileについて
    • Prod/StgでAWSアカウントが分かれている場合があると思うので、
      環境ごとの認証情報はここに入れます。
      以下のように定義しておくことで、aws-cli-v2が勝手に読んで利用してくれます
      enviroments/stg/.env
      AWS_DEFAULT_REGION=ap-northeast-1
      AWS_DEFAULT_OUTPUT=json
      AWS_ACCESS_KEY_ID=dummy
      AWS_SECRET_ACCESS_KEY=dummy
      
  • volumeマウントについて
    • DooDのため、ローカルマシンのdocker.sockをコンテナにbindマウントします

実際に使ってみる

Docker環境が構築できたので実際に
・TerraformでECRを作成
aws-for-fluent-bitのimageをbuild
aws-cliでECRにpush
上記を実行してみます


aws-for-fluent-bitのDockerfile作成

といっても今回はサンプルなのでこれだけです
※以下はStgの例です

environments/stg/logger/Dockerfile
FROM public.ecr.aws/aws-observability/aws-for-fluent-bit:stable

ECRのmodule作成

ECRの作成をトリガーにimageをbuildしてpushするmoduleを作成します
サンプルにしては冗長ですみません、適宜単一ファイルで定義してもらって大丈夫です

modules/ecr/variables.tf
variable "name" {
  description = "ECRで作成するリポジトリの名前"
  type        = string
}

variable "region" {
  description = "リージョン"
  type        = string
}

variable "image_tag_mutability" {
  description = "tagのmutabilityを指定する。IMMUTABLEの場合は、tagが常に一意になるよう運用する"
  type        = string
  default     = "IMMUTABLE"

  validation {
    condition     = contains(["IMMUTABLE", "MUTABLE"], var.image_tag_mutability)
    error_message = "Allowed values for \"ecr_image_tag_mutability\" are \"IMMUTABLE\" or \"MUTABLE\"."
  }
}

variable "docker_build_context" {
  description = "初期push時に参照するDockerfileのcontext"
  type        = string
}
modules/ecr/main.tf
# =================
# ECR
# =================
resource "aws_ecr_repository" "this" {
  name                 = var.name
  image_tag_mutability = var.image_tag_mutability

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "null_resource" "initial_image_push" {
  provisioner "local-exec" {
    command = <<-EOF
      docker build ${var.docker_build_context} -t ${aws_ecr_repository.this.repository_url}:initial; \
      aws ecr get-login-password --region ${var.region} | docker login --username AWS --password-stdin ${aws_ecr_repository.this.repository_url}; \
      docker push ${aws_ecr_repository.this.repository_url}:initial;
    EOF

    on_failure = fail
  }

  depends_on = [
    aws_ecr_repository.this
  ]
}
modules/ecr/outputs.tf
output "name" {
  value = aws_ecr_repository.this.name
}

Prod/Stg環境のmain.tfなど作成

今回は簡易化のためにtfstateはローカルで管理します
※以下はStgの例です

environments/stg/stg.tfvars
region       = "ap-northeast-1"
project_name = "project"
environments/stg/variables.tf
variable "region" {
  description = "リージョン"
  type        = string
}

variable "project_name" {
  description = "プロジェクトの名前"
  type        = string
}
enviroments/stg/backend.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.17.0"
    }
  }
}

provider "aws" {
  region = var.region
}
environments/stg/main.tf
module "ecr_repo" {
  source = "../../modules/ecr"

  name                 = var.project_name
  region               = var.region
  image_tag_mutability = "IMMUTABLE"
  # build contextを引数で渡すのはメタくて微妙ですがサンプルということで許してください
  docker_build_context = "./logger"
}
enviroments/stg/Makefile
SHELL=ash

.PHONY: $(shell egrep -oh ^[a-zA-Z0-9][a-zA-Z0-9_-]+: $(MAKEFILE_LIST) | sed 's/://')

# ===================
# static checks
# ===================

sec:
	tfsec

# severityはお好みで
sec-min:
	tfsec --minimum-severity HIGH

lint:
	tflint

fmt:
	terraform fmt -check -recursive

check: fmt lint sec-min

# ===================
# terraform commands
# ===================

init: fmt
	terraform init

plan-stg: check
	terraform plan -var-file stg.tfvars

plan-stg-verbose: check
	TF_LOG=debug terraform plan -var-file stg.tfvars

apply-stg: check
	terraform apply -var-file stg.tfvars
environments/stg/.tflint.hcl
plugin "aws" {
  enabled = true
  version = "0.15.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

config {
  module = true
}

docker-compose.yamlを更新する

docker-compose.yaml
version: "3.8"

services:
  terraform-prod:
    build:
      context: .
      dockerfile: Dockerfile
    working_dir: /terraform/environments/prod
    tty: true
    env_file:
      - environments/prod/.env
    volumes:
+     - type: bind
+       source: "environments/prod"
+       target: "/terraform/environments/prod"
+     - type: bind
+       source: "modules"
+       target: "/terraform/modules"
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock

  terraform-stg:
    build:
      context: .
      dockerfile: Dockerfile
    working_dir: /terraform/environments/stg
    tty: true
    env_file:
      - environments/stg/.env
    volumes:
+     - type: bind
+       source: "environments/stg"
+       target: "/terraform/environments/stg"
+     - type: bind
+       source: "modules"
+       target: "/terraform/modules"
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock

動作確認

自分の場合はprojectのrootにMakefileを置いて利用しています

Makefile
SHELL=/bin/bash

.PHONY: $(shell egrep -oh ^[a-zA-Z0-9][a-zA-Z0-9_-]+: $(MAKEFILE_LIST) | sed 's/://')

set-up:
	docker-compose build

tf-prod:
	docker-compose run --rm terraform-prod

tf-stg:
	docker-compose run --rm terraform-stg
% make set-up
% make tf-stg
/terraform/environments/stg # make init
/terraform/environments/stg # make plan-stg
/terraform/environments/stg # make apply-stg


AWSのコンソールから確認して正常にECRが作成され、imageがpushされていれば成功です🎉

おわりに

これで簡単かつローカルマシンを汚さずにTerraformを利用できます🎉
また、docker-composeでProd/Stgの環境ごとに.envを渡してContainerを動かすようにしているので、各環境のAWS認証情報をいちいちローカルマシンで切り替えてTerraformを実行する必要もなくなりました
これも地味に嬉しいです!
※git-secretsなどで機微情報の誤commit対策は適切にやりましょう

参考にさせていただいた記事

https://qiita.com/Shoma0210/items/7178284e4fdbcd5f9dc2
https://kiririmode.hatenablog.jp/entry/20200725/1595621558
https://github.com/sgerrand/alpine-pkg-glibc/releases
https://blog.taross-f.dev/terraform-docker/
https://qiita.com/yuua0216/items/0f980bdbe953327f30bc

Discussion