Sionの技術ブログ

SREとして日々の学習を書いて行きます。twitterは@sion_cojp

ディレクトリ配下にある.tfファイル全てにterraform planをGoで並列実行する

FOLIO特有のロジックが入ってますが、参考になればと思います。

terraformのディレクトリ構成

FOLIOのterraformはこんなディレクトリ構成になってます。(一部抜粋)

── envs
   ├── mobile-prod
       ├── provider.tf       
       ├── ecs
       ├── iam
       ├── route53
       └── vpc
          ├── backend.tf
          ├── provider.tf
          ├── route_table.tf
          ├── variables.tf
          └── vpc.tf
   ├── mobile-stg
   ├── sandbox
   ├── web-prod
   └── web-stg
── global.tfvars
── backend_tf_template

global.tfvarsは全体の変数定義。(CIDRやIPリストなど)

provider.tfは各環境内の設定です。(subnet_idなど)

planの実行

terraformのオペレーションはMakefileで管理されてます。

下記のコマンドで特定のディレクトリに対してplanが実行できます。

$ make plan CONFIG=envs/mobile-prod/vpc
# initializeした後、terraform plan -var-file=global.tfvarsを打ってるだけです

今回作ったもの

  • ディレクトリを再帰的に検索して、xxx.tfがある場所でmake planを並列実行する
  • backend.tfとprovider.tf以外が対象
  • 差分がある or 問題が起こったら出力する
    • 具体的には、 No changes. Infrastructure is up-to-date というワードがなかったら出力
  • -n で並列数を選べる
  • -tディレクトリ指定も出来る
  • 常に差分が発生するものに対しては、ignorePlanで除外出来る
    • (そんなに変更するものじゃないので、ハードコーディング)
$ ./check-all-plan --help
Usage of ./check-all-plan:
  -n int
        並列数 (default 20)
  -t string
        ターゲット。ex: -t mobile-stg

$ ./check-all-plan
check-all-plan: 2018/09/21 13:49:47 [INFO] Check start......
check-all-plan: 2018/09/21 13:52:37 [INFO] Result: you need check below
make plan CONFIG=envs/mobile-stg/kinesis
make plan CONFIG=envs/web-prod/elasticache
make plan CONFIG=envs/web-prod/teleport
make plan CONFIG=envs/web-stg/iam
make plan CONFIG=envs/web-prod/instances
make plan CONFIG=envs/web-stg/network
make plan CONFIG=envs/web-stg/kinesis

ソースコード

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
    "os/exec"
    "path/filepath"
    "strings"
    "sync"
)

const (
    APP = "check-all-plan"
)

var (
    // parallelNum ... 適当に20並列にしてる。
    parallelNum = 20

    // target ... envs/mobile-stgのように、ターゲット単位で指定出来るようにしてる
    target string
)

// init ... set flag
func init() {
    flag.IntVar(&parallelNum, "n", 20, "並列数")
    flag.StringVar(&target, "t", "", "ターゲット。ex: -t mobile-stg")
    flag.Parse()
}

func main() {
    log.SetOutput(os.Stderr)
    log.SetPrefix(APP + ": ")

    os.Exit(Run())
}

// Run ... 実行
func Run() int {
    // 結果を格納するslice
    var results []string

    // skipするディレクトリ。必要だったら修正してね!
    ignorePlan := []string{
        "sandbox",
        "web-prod/apps",
        "web-stg/apps",
    }

    // planを実行するdirの取得
    targetDirs := GetDir("envs", ignorePlan)

    // default: 20並列
    var m sync.Mutex
    wg := &sync.WaitGroup{}
    semaphore := make(chan struct{}, parallelNum)

    log.Println("[INFO] Check start......")

    for _, targetdir := range targetDirs {
        wg.Add(1)
        semaphore <- struct{}{}
        go func(d string, m *sync.Mutex) {
            defer func() {
                <-semaphore
                defer wg.Done()
            }()
            out := RunTerraformPlan(d)
            if !strings.Contains(out, "No changes. Infrastructure is up-to-date") {
                m.Lock()
                defer m.Unlock()
                results = append(results, d)
            }
        }(targetdir, &m)
    }
    wg.Wait()

    log.Println("[INFO] Result: you need check below")
    for _, result := range results {
        fmt.Printf("make plan CONFIG=%s\n", result)
    }

    return 0
}

// RunTerraformPlan ...make plan CONFIG=xxxxを実行する
func RunTerraformPlan(targetDir string) string {
    dir := fmt.Sprintf("CONFIG=%s", targetDir)

    // make plan打って差分 or 問題が起こったかを検知したいので、
    // ここではエラーハンドリングしない
    out, _ := exec.Command("make", "plan", dir).Output()
    return string(out)
}

// GetDir ...envs/配下の実行対象のディレクトリを取得する
func GetDir(parentDir string, ignorePlan []string) []string {
    var dirs []string
    err := filepath.Walk(parentDir, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return err
        }

        // 隠しディレクトリはskip
        if info.IsDir() && strings.HasPrefix(info.Name(), ".") {
            return filepath.SkipDir
        }

        // ignorePlanに書かれてるディレクトリ以下をskip
        for _, v := range ignorePlan {
            if info.IsDir() && path == fmt.Sprintf("envs/%s", v) {
                return filepath.SkipDir
            }
        }

        // backend.tfとprovider.tf以外のtfを持つディレクトリを抽出
        switch info.Name() {
        case "provider.tf":
            break
        case "backend.tf":
            break
        default:
            if strings.HasSuffix(info.Name(), ".tf") {
                dirs = append(dirs, filepath.Dir(path))
            }
        }

        return nil
    })

    if err != nil {
        log.Printf("[WARN] filepath.Walk: %s\n", err)
    }

    return getUniqueDirs(dirs)
}

// getUniqueDirs ...重複したデータを削除。targetが指定されてたら指定されたものだけをチョイス
func getUniqueDirs(src []string) []string {
    dst := make([]string, 0, len(src))
    m := make(map[string]bool)

    for _, s := range src {
        if _, ok := m[s]; !ok {
            m[s] = true
            // targetが指定されたら、指定されたものだけをappendする
            if target != "" {
                if strings.HasPrefix(s, fmt.Sprintf("envs/%s", target)) {
                    dst = append(dst, s)
                }
            } else {
                dst = append(dst, s)
            }
        }
    }
    return dst
}