TerraformのStateやModuleに関する設計

概要

TerraformのStateやModuleに関する設計に関する考慮点をざっくり書く

名前付け

  • ユビキタス言語とIaCのディレクトリ名、変数名を合わせて認知不可を下げる
  • リソース名でなく役割・機能ベースのディレクトリ名にして認知不可を下げる
    プロダクトのコードネームはユビキタス言語なのでディレクトリ名にしても問題ない
  • ModuleやStateのREADME.mdを書く
    • https://github.com/terraform-docs/terraform-docs を使う
    • うまく説明が書けない場合は設計に問題がある可能性がある
    • チームメンバーが利用できるように考慮する
      いつどのようなときに使えばいいかわからないModuleは誰からも使われない

抽象度

  • 抽象度が低いModuleを作らない
    例:modules/s3など

参考: https://developer.hashicorp.com/terraform/language/modules/develop#when-to-write-a-module

  • 大きく作ってから小さくしていく
    小さいものをまとめるほうがコストが大きいため
    stateやmoduleが小さいときは上記のように抽象化を間違えて不要に小さいパターンが多いため

インフラアーキテクチャ

アーキテクチャ図を円で囲ってstate、moduleの単位を作る

チーム

自分が把握していない変更が出るとapplyしていいかわからないのでコミュニケーションが発生するのでチーム単位で分ける

認証情報

CI実行時などを考慮して認証情報の粒度でディレクトリを分ける

変更(デプロイ)のライフサイクル

DRYの誤用

不要な複雑さにつながるので偶発的凝集をModuleにしない

アプリケーションレイヤーとの違い

アプリケーションレイヤーと異なりライフサイクルが長く作り直しの難易度が高く、作り直しに時間がかかることを考慮する

まとめ

  • 適切に名前付けできないのは設計に問題があるサイン
  • なぜその抽象度で設計するか説明できるようになる
  • 公式ドキュメントやAWSGCP公式のModuleを読む

プレビュー環境に必要な要件を整理する

概要

プレビュー環境、マルチステージング環境などと呼ばれる環境について要件を整理する。

要件

  • プルリクエスト(任意のブランチ)のコードをデプロイできる
    • 他の開発者と分離された環境をデプロイできる
  • プルリクエストにラベルを貼るとインフラが作成される
    • プレビュー環境が必要ないプルリクエストを考慮する(Renovateなど)
  • 短い時間で起動できる
    • 3分以上待つのは厳しい
    • 起動するリソースの種類を考慮する
    • ロードバランサーやストレージは起動に時間がかかるので起動しなくて済む設計にする
  • 環境変数を変更できる
  • プルリクエストごとにURLが発行される
    • リクエストはパスベースでルーティングする
  • デプロイされたら通知が来る
    • リポジトリ
    • アプリケーション名
    • コミットハッシュ
    • URL
  • プルリクエストがクローズされたらインフラリソースが削除される

まとめ

プレビュー環境に必要な要件を整理した。
他にも検証用に開発メンバー個人専用の環境を用意する方法もある。

最近の仕事まとめ2023冬

概要

今年もお疲れ様でした。

新規プロジェクトインフラ基盤構築・レビュー

ecspresso利用

既存プロダクトではTerraformでECSをデプロイしていたが、新規プロダクトではecspressoを導入するように推進した。
https://github.com/kayac/ecspresso

Atlantis導入

AWS、Azureのマルチクラウドインフラ基盤をAtlantisで構築した。
https://www.runatlantis.io/

AWSアカウント移行

AWSのアカウント移行作業を進めた。

ECS->EKS移行

ECSからEKS移行を進めた。
https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Generators-Pull-Request/
dev00~10のような運用をしており、動作確認をしたいときに使えないという課題を解決するため、ArgoCDのPull request generatorを使ってPRを作成したらコンテナが立ち上がるようにした。
プロジェクトは諸事情で一時中断させた。  

勉強会

新メンバーに対して、Terraform勉強会とDevOps、CI/CD勉強会を開催した。
分野を増やしつつ毎年継続的にやっていきたい。
講師も交代できるレベルにしていきたい。

登壇系

SRE勉強会のLTと社内カンファレンスのLT枠で登壇した。
プロジェクトの失敗要因やSREの実践について話した。

プロジェクト管理

ロールは決まってないが無いが適切なタイミングでプロジェクト管理について、提案をしている。
チケットのテンプレートを作成したり、KPTしたり、タスクの優先順位を変更したり、DesignDoc書いたりなど。

その他

  • チームや人事の課題に対して適切にエスカレーションし、他部署のメンバーを巻き込んで仕事を進めた。
  • 静的解析の追加
  • Renovateの更新とバージョンアップ作業
  • self hosted runnerの追加
  • チームメンバーの育休に備えてドキュメントの整備、育休中の対応

などコツコツやっている

まとめ

戦略面のカバーをできたのが大きかった。
来年も引き続き組織の再現性、スケーラビリティを意識して仕事していきたい。

Amazon ECRでイメージを保護する

概要

Amazon ECRでイメージを保護したいが保護のアクションがないので対応策

結論

保護したいイメージの削除期間やイメージの個数を大きな数にする

下のようなJSONにすると

  • vから始まるタグ(v1.1.0など)は1年間削除されない
  • sha-から始めるタグは1日で削除される
  • v1.1.0とsha-xxxxxxxがついていた場合は優先順で365日削除されない

という挙動になる

{
  "rules": [
    {
      "action": {
        "type": "expire"
      },
      "selection": {
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 365,
        "tagStatus": "tagged",
        "tagPrefixList": [
          "v"
        ]
      },
      "description": "keep released images",
      "rulePriority": 1
    },
    {
      "action": {
        "type": "expire"
      },
      "selection": {
        "countType": "sinceImagePushed",
        "countUnit": "days",
        "countNumber": 1,
        "tagStatus": "tagged",
        "tagPrefixList": [
          "sha-"
        ]
      },
      "description": "remove development images",
      "rulePriority": 2
    }
  ]
}

参考

GCPとAzureにはあった。

https://cloud.google.com/artifact-registry/docs/repositories/cleanup-policy#create

https://learn.microsoft.com/en-ca/azure/container-registry/container-registry-image-lock#protect-an-image-from-deletion

Terraformの改善活動まとめ

概要

最近のやっていたTerraformリポジトリの改善活動をまとめる

課題点

リポジトリの分割粒度

prd,stgなどの実行環境ごとにリポジトリが別れており、AWSアカウントとリポジトリが1対1でない。また、明確なルールがない環境のため

  • どのリポジトリにコードを書けばいいかわからない
  • どのmoduleを使えばいいかわからない(terraform-moduleリポジトリがあるのにdev-terraformリポジトリにもモジュールがある)
  • リソースを書く場合リポジトリを間違える、間違いを指摘する手間が増える
  • カスタムスクリプトやドキュメントが重複しており管理コストが高い

stateの粒度

1terraform resourceにつき1stateになっていたため

  • apply作業を何度も実行する必要がある
  • 定期的にapplyされないのでコードとリソースの差分が発生する
  • インフラアーキテクチャの全体像が分かりづらい

カスタムスクリプトによる開発体験の低下

  • メンテナンスできるメンバーが少ない
  • 拡張性の低下
    linterなどの周辺ツールを導入するためにカスタムスクリプトに大幅な変更が必要
    CI変更時も同様
  • terraform applyする対象をファイルで管理するためPR作成時にコンフリクトする

Terraform, AWS providerのバージョンが古い

リソースが細かいことにも相まってバージョンアップ作業が大変

やったこと

リポジトリの統合

リポジトリをmonorepo構成に変更

カスタムスクリプトの脱却

  • カスタムスクリプトを削除しterraformコマンドを実行するように変更
  • apply対象をファイル管理していたものを、変更差分をCIで検出するように変更
    カスタムスクリプトをなくしたことで、

CIの拡張

CodeBuildからGitHub Actionsに変更

  • Plan結果をPull Requestのコメントに表示するように変更
  • apply結果をSlackに通知
  • DynamoDBによるstateのlockの追加

静的解析の追加

  • tfsec、tflint、conftestなどの静的解析を導入

Terraform, AWS providerのバージョンアップ

  • スクリプトを書いて最新版に更新
    Terraformバージョン...0.13,0.14系から1.3系に
    AWSプロバイダー...3系から4系に
  • Renovateでバージョンアップに追従できるように

やれなかったこと

  • stateの統合
  • drift detection

学んだこと

やったことは基本的なことなので、そこから抽象化した学びをメモする  

  • 守れないルールは必要ないので、ルールと仕組み化はセットにする
  • チームとして負債をハンドリングできないと返却に大幅な時間がかかる
  • 負債とは正面から向き合う必要があるか考える
  • 開発組織の人数規模が増えてくると基礎的なフォローアップや、同じことを繰り返し言う必要がある

ベースブランチにマージされていないGitHub Actionを動作検証する

環境

gh --version    
gh version 2.25.1 (2023-03-21)
https://github.com/cli/cli/releases/tag/v2.25.1

手順

1.action作成 .github/workflows/foo.yaml

name: Foo

on:
  pull_request:
# pull_requestにすることでworkflow listに追加される

jobs:
  foo:
    runs-on: ubuntu-latest
    steps:
      - name: foo
        run: |
          echo "foo"

2.リモートブランチにプッシュ

git push origin foo

3.gh workflow listで確認

gh workflow list
...
Foo  active  xxx

4.gh workflow runで実行

gh workflow run foo.yaml --ref foo

参考

https://cli.github.com/manual/gh_workflow_run

zxでTerraformのファイルにlifecycle prevent_destroyを一括で付与する

概要

Terraformの任意のリソースにprevent_destory = trueを付与したい。
zxでスクリプトを書いたので記録。

  • 挙動
    lifecycleブロックがなければ追記してprevent_destory = trueをセット。
    lifecycleブロックがある場合は、falseのものもtrueにする。

環境

zx --version                                             
7.1.1

スクリプト

#!/usr/bin/env zx
// eg. zx scripts/addPreventDestroy.mjs --dir path/to/resource --resource aws_s3_bucket
const { resource, dir } = argv;
$.verbose = true

async function hasLifeCycle(file, block) {
  const res = await $`cat ${file} | hcledit block get ${block}.lifecycle`
  if (res.stdout === "") return false
  return true
}

async function hasPreventDestroy(file, block) {
  const res = await $`cat ${file} | hcledit attribute get ${block}.lifecycle.prevent_destroy`
  if (res.stdout === "") return false
  return true
}

async function appendLifeCycleWithPreventDestory(file, block) {
  await $`cat ${file} | hcledit block -f ${file} -u append ${block} lifecycle --newline`
  await $`cat ${file} | hcledit attribute -f ${file} -u append ${block}.lifecycle.prevent_destroy true`
}

async function setPreventDestroy(file, block) {
  await $`cat ${file} | hcledit attribute -f ${file} -u set ${block}.lifecycle.prevent_destroy 'true'`
}

async function appendPreventDestroy(file, block) {
  await $`cat ${file} | hcledit attribute -f ${file} -u append ${block}.lifecycle.prevent_destroy 'true'`
}

function isCommentOut(blocks) {
  return (blocks.length === 1 && blocks[0] === "")
}

async function listFliles() {
  const files = (await $`grep -l -r --exclude-dir .terraform "resource \\"${resource}\\"" ${dir}  || true `).stdout.trim().split('\n')
  return files.filter((v) => v)
}

async function addPreventDestroy(resource) {
  const files = listFliles()
  for await (const file of files) {
    const res = (await $`cat ${file} | hcledit block list | grep "resource.${resource}\\." || true `)
    const blocks = res.stdout.trim().split('\n')
    if (isCommentOut(blocks)) continue
    for await (const block of blocks) {
      const ifHasLifleCycle = await hasLifeCycle(file, block)
      if (ifHasLifleCycle) {
        const ifHasPreventDestroy = await hasPreventDestroy(file, block)
        if (ifHasPreventDestroy) {
          await setPreventDestroy(file, block)
        } else {
          await appendPreventDestroy(file, block)
        }
      } else {
        await appendLifeCycleWithPreventDestory(file, block)
      }
    }
  }
}

await addPreventDestroy(resource)

実行方法

zx scripts/addPreventDestroy.mjs --dir path/to/resource --resource aws_s3_bucket

これでpath/to/resource配下のすべてのaws_s3_bucketprevent_destory=trueを付与する。

参考

https://github.com/google/zx

https://developer.hashicorp.com/terraform/tutorials/state/resource-lifecycle#prevent-resource-deletion

https://github.com/minamijoyo/hcledit