目の前に僕らの道がある

勉強会とか、技術的にはまったことのメモ

Terraformのtfstateについて考える

この記事は3-shake Advent Calendar 2023の23日目の記事となります。

こちらはSRE Tech Talk #6で話した内容に補足したものです。

資料はこちらとなります。

tfstateとは

Terraformが管理しているリソースの状態を表すjson形式のファイルです。

tfstateとterraformファイルと実際のリソースの状態を比較して、terraformコマンドが実行されます。

一般的には直接変更せずterraform stateコマンドを通して変更を行い、一般ユーザがtfstateに触れることはないです。

tfstateの課題

tfstateについて以下の課題があります。それぞれについて見ていきます。

  • tfstateの管理場所
  • tfstateを管理するリソースの管理
  • tfstateの分割

tfstateの管理場所をどうするか問題

主な保存場所候補としては以下のものがあります。

  • local(デフォルト)
  • クラウドのオブジェクトストレージ
  • Gitレポジトリ統合
    • GitLab
  • SaaS利用
    • Terraform Cloud

local

Terraformのデフォルト保存先です。Terraformを実行する同じディレクトリのterraform.tfstateに保存されます。

1人もしくは変更頻度が著しく低い状況など特殊なとき使えるものとなります。git管理して複数人で使うこともできるが、コンフリクトが発生しうるので、チーム開発には向かないです。

基本的には複数人でterraformを使用するときは非推奨です。

S3/Google Cloud Storage

監理するクラウドのオブジェクトストレージに保存する方法です。これが標準的(当社比)なのかなと思っています。

オブジェクトストレージなので、権限があればどこからでもアクセスすることができます。それゆえ、同時にTerraformが実行されるので排他ロックの処理が必要となります。S3バックエンドを使用した場合はDynamoDBを使用してstate lockを実現します。

Google Cloud Storageは単体でstate lockをサポートしています。

tfstateの参照権限をクラウドのIAMで制御する必要があります。

GitLab

GitLabでtfstateを監理することもできます。tfstateを管理するリソースを管理する必要がないことがメリットとなります。(後述します)

開発にGitLabを使っている場合、親和性が高い方法となります。

Terraform Cloud

GitLabと同様tfstateを管理するリソースを管理する必要がないというところにメリットがあります。

月間500 Managed Rsourcesまで無料で使えます。

web上からリソース差分の確認できたり、applyが可能です。SaaSクラウドのリソース情報を預けることに抵抗がない場合は選択肢としては有望です。

なおTerraformのStateのドキュメントではこういう記述があり、Terraform Cloudを推奨しているようです。

This state is stored by default in a local file named "terraform.tfstate", but we recommend storing it in Terraform Cloud to version, encrypt, and securely share it with your team.

昔はAWSと連携するためにIAM Userのアクセスキーを使わないといけなかったが、OIDC認証もできるようになったので、よりやりやすくなったかと思います。

tfstateを管理するリソースをどう管理する問題

GitLabやTerraform Cloudを使う場合には起きない問題となります。 S3のようなクラウドのオブジェクトストレージを使用する場合は、このS3バケットをどう作るかということが問題となります。コマンドで作る場合、コマンドの管理、terraformで作る場合はそのtfstateはどこに保存するか、そういったことに頭を悩ませます。そこについて考えていきます。

以下の方法が考えられます。

  • aws/gcloudコマンド
  • terraform + local state管理
  • CloudFormation

aws/gcloud コマンド

そもそも作成コマンドしか打たないのであれば、スクリプトをレポジトリに含めておけば良いという考え方はあります。 基本的に一度作れば変えることはないので、これで十分という風に割り切ることはできます。

ただし、tfstateのバケットだけでなく、CI/CD用のIAM RoleやOIDC認証リソースなども初期リソースとして含めて管理したいというユースケースだと、スクリプト管理では力不足になりうります。

terraform + local state 管理

オブジェクトストレージをterraformで作る方法です。ただし、tfstateに関してはlocalに保存し、これをgitも管理します。 かたくなにterraformを使いたい人に向けな方法となります。

デメリットとしては、tfstateもgit管理するのでコミット忘れがあります。また、頻度低いですがterraform自体はローカルで実行せざるを得ないので変更衝突が起きうることです。

CloudFormation / Google Deployment Manager

クラウドごとにコードを変えないといけない。IaCツールを2種類使うというそこはかとない気持ち悪さはあるというデメリットはありますが、gitでインフラ状態管理しなくてすむというメリットがあります。気持ち悪さだけを克服できるなら無難な選択肢だとは思います。

tfstateをどう分割するか問題

第一に考えるのが環境の分離。この分離の仕方だけ他とは系統が違うので独立して説明します。

一部差分があるだけで、以下のような形でほぼ同じ構成の環境を作ることはよくあります。

  • 開発環境
  • ステージング環境
  • 本番環境

これらについてどう分割するのかを考えていきます。

環境分離パターン

大きく2つのパターンを利用することが多いです。それぞれ見ていきます。

ディレクトリ分離パターン

これは環境ごとにディレクトリを分割して、環境ディレクトリを実行単位とします。環境の切り替えはディレクトリ移動することで行います。 環境ごとの差分が大きいときに使うことが多いです。デメリットとしては環境ごとにリソース定義をそれぞれ書くので記述量が多くなるというのがあります。そのため、可能な限りモジュール化して、なるべくパラメータだけの差分にするようにします。

ディレクトリ構成例としては以下の通りです。

.
├── envs
│   ├── dev
│   │   ├── locals.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── prd
│   │   ├── locals.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── stg
│       ├── locals.tf
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
└── modules
    ├── vpc
    │   ├── locals.tf
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf
    ├── application
    │   ├── locals.tf
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variables.tf

backend-configパターン

backend-configオプションとvars-fileオプションを組み合わせて、環境を切り替えるパターンです。

${ENVDIR}/terraform.tfvars に環境ごとの差分パラメータを定義して、${ENVDIR}/backend.tfvars に環境ごとのtfstate保存先を定義します。

terraform initbackend.tfvars を切り替えることで環境の切り替えを行います。

環境ごとに差分が少ないときに向いています。差分は terraform.tfvars に記述されているパラメータだけなので、記述量が少なくて済みます。 ただし差分が多くなるとcount, for_eachで分岐やループを作ることになり読みにくくなるというものがあります。

ディレクトリ構成例としては以下のようになります。

.
├── envs
│   ├── dev
│   │   ├── backend.tfvars
│   │   └── terraform.tfvars
│   ├── prd
│   │   ├── backend.tfvars
│   │   └── terraform.tfvars
│   └── stg
│       ├── backend.tfvars
│       └── terraform.tfvars
├── locals.tf
├── main.tf
├── modules
│   └── vpc
│       ├── locals.tf
│       ├── main.tf
│       ├── outputs.tf
│       └── variables.tf
├── outputs.tf
├── provider.tf
└── variables.tf

設定ではbackendをs3と指定しておき中身はオプションで指定するようにします。

terraform {
  backend "s3" {}
}

以下のようにterraform initするたびに適用する環境を切り替えることができる。

terraform init --backend-config=${ENVDIR}/backend.tfvars --reconfigure
terraform apply --var-file=${ENVDIR}/terraform.tfvars

workspace

workspaceは同じような環境を複製するときに使ういます。シングルテナント環境を量産する場合や開発環境を複数作る場合などに使います。環境を切り替える用途には作られてないとドキュメントまでは記載されています。

In particular, organizations commonly want to create a strong separation between multiple deployments of the same infrastructure serving different development stages or different internal teams. In this case, the backend for each deployment often has different credentials and access controls. CLI workspaces within a working directory use the same backend, so they are not a suitable isolation mechanism for this scenario.

自分自身がworkspaceを実運用で使ったことがないので多くは語れないです。別でちゃんと使ってから書きたいと思います。

環境分離以外の分割をどうするか問題

小さいサービスでは環境を分離するだけでだいたいは問題ないことがおおいですが、terraformを運用していると運用面、管理面でいろいろ課題が出てくると思います。

管理するリソースが増えるとplan/applyの時間が増えたり、リソースの見通しが悪くなったりしてきます。

特に実行時間が意外に馬鹿にできなかったりします。下手するとplanに数分かかるようになったりします。

そのため、ある程度大きくなったらtrstateを分割して、リソースの管理範囲を分割する必要が出てきます。

これをどうやって分割するかが自分の中で答えが出ていない出てないし、分脈によって解決策は異なるとは思います。

ここで、解決策を考えるうえで、分割するための観点を見ていきましょう。

分割する観点

分割する観点は以下のようなものがあるかと思います。

  • プロバイダー
  • 管理権限
  • 変更頻度

プロバイダーで分割

プロバイダー単位で分割するパターンです。

例としてはAWSとDatadogのようにプロバイダーで分割します。プロバイダー間で依存がない場合は分けやすいかと思います。

また、プロバイダー間で管理主体が違うことも多いので素直な分け方だとは思います。

しかしながら、アプリケーションリソースとアプリケーションの監視を近いところにおいたほうが見通しがよいのではという観点もあるので運用体制にあわせて考えるとよいでしょう。

管理権限で分割

チームの権限で分割するパターンです。ただし、より堅くするなら、ディレクトリではなくレポジトリ自体も分割して、コードの参照権限も分割する方が望ましい場合もあります。

    • ネットワーク ⇒ インフラチーム
    • アプリケーション ⇒ 開発チーム

変更頻度で分割

変更をあまりしないリソースを変更が頻繁なリソースと一緒のplan/applyするのは無駄なので変更の頻度でtfstateを分割するパターンもあります。

    • 変更が少ない ⇒ DB/ネットワーク
    • 変更が多い ⇒ EC2/ECS

依存の方向性で分割

少し観点を変えてみます。実際に分割をした場合に問題となるのはtfstate間のリソースの依存が課題になります。 tfstate間で相互に依存するようなコードを書くとtarget指定してそれぞれのstateのリソースを作成しなくてはなりません。 こうすると管理が煩雑となってしまうので、原則的に片方向だけの依存になるように分割するようにするのが望ましいです。

tfstate間のリソース参照

terraform_remote_state を使うことで、参照元のTerraformでoutputした内容を別のTerraformで利用することができます。

# 参照元 networkアカウント
output "vpc_id" {
  value = aws_vpc.main.id
}
# 参照先 applicationアカウント
# data.terraform_remote_state.network.vpc_id の形式でVPC IDを参照できる
data "terraform_remote_state" "network" {
  backend = "s3"

  config {
    bucket = "terraform-tfstate-network-xxxxx"
    key    = "tfstate"
    region = "ap-northeast-1"
  }
}

まとめ

正直tfstateをどう扱うかに正解はないです。サービス規模や性質によって選択は変わります。

本当に小さい規模であれば、tfstateを分割せず一つで十分でしょうし、チーム開発せず一人で扱うなら、通常であれば推奨されないtfstateのlocal git管理という手段がふさわしい場合もあります。 また、組織やサービスの成長や時間経過によっても最適な選択は変わると思います。大事なのは選んだ技術要素に関しては選定理由を説明できるようにはしておくということです。 選定理由及び不採用理由を明確にしておくことで、変更時に最適な選択の助けになるでしょう。

Terraform使いがPulumiに入門しました

この記事は3-shake Advent Calendar 2023の16日目の記事です。

qiita.com

この内容はSRETT #8で発表した内容に補足しています。

3-shake.connpass.com

前提

筆者は以下の背景を持っています。

  • 普段はAWSをメインに触っている
  • 普段はTerraformをメインで使ってる
  • Pulumiはプロダクションでは使ったことがない
    • ちゃんとは把握できてない

語らないこと

以下のようなPulumi以外の基本的なことは語りません

  • IaCとは
    • 概要、特徴、メリット・デメリット
  • Terraformとは
    • 概要、特徴、メリット・デメリット、操作方法

モチベーション

なんでPulumiを今回調べようかと思った動機について書こうと思います。

Terraformの記述力に限界を感じていたというところが大きいです。以下の点がつらいかなと思っていたところです。

  • 足りない関数
  • 二重ループのためのModule使用
  • 分岐処理のためのcountと三項演算子

とはいえ、記述力が低いからこそ複雑なことを抑制できて可読性が上がっている面もあると思います。 冗長でも、可読性が高いというのはメリットではあります。

他の選択肢としては以下のものがあるかと思います。

  • CDK
    • AWSに限定される
  • CDKTF(CDK for Terraform)
    • 結局terraformのJSONコードに変換されるので、terraformに依存します
    • それ自体は悪くないが、どうせならTerraformから離れたものを学びたい

そこでなにか良いものがないかと思い当たったところにPulumiがあったので調べてみようとなりました。

Pulumiとは

Pulumiはプログラミング言語でインフラを構築可能なプロビジョニングツールです。

Terraformと同じようにProviderを通して複数のクラウドに対応しています。

TerraformはHCLという宣言的言語を使用するのに対し、Pulumiは汎用的なプログラミング言語を使用してインフラリソースを定義します。

Pulumi - Infrastructure as Code in Any Programming Language

対応言語

参考: Pulumi Languages & SDKs | Pulumi Docs

Pulumiのアーキテクチャ

以下のようの構成になっています。

参考: How Pulumi Works | Pulumi Docs

  • Language host
    • インフラリソースの定義を Program (後述)として好きな言語で定義します。
  • Deployment Engine
    • 希望する状態に変更するための操作セットを実行する役割を果たします。
  • Resource Provider
    • クラウドサービスとの通信を処理して、Programで定義したリソースの変更処理を行います。

上記の例だと、Programにリソースの定義がある場合、Stateと比較して、管理されているリソースであるかを確認します。

存在すれば、プロバイダーを通して実際のクラウドのリソースの状態と比較して差分があれば適用。

存在しない場合、プロバイダーを通してリソースを作成。

Pulumiのコンポーネント

What is Pulumi? | Pulumi Docs

Pulumiのコンポーネントは以下のようになっています。

  • Project
  • Program
    • インフラのあるべき姿を定義したもの
  • Resource
    • インフラを構成するオブジェクト。ResourceのプロバティはOutputとして他のResourceのInputに使用することができます
  • Stack
    • Programを実行すると作成されるインスタンス。同一のProgramから開発、ステージング、本番環境のStackを個別に作成することができます。

Pulumi Cloud

Terraform Cloudのようなものと考えていただいて良いです。 デプロイの状態、履歴やシークレットを管理して、CI/CDやGitHubと連携してデプロイを実行することもできます。 Pulumi CLIはバックエンドを明示的に指定しない限りはでデフォルトでPulumi Cloudを使用します。

Terraformはデフォルトでlocalバックエンドを使用します。

以下はPulumi Cloudの画面です。

Pulumi Cloud 料金

個人で使う限りは無料で使用することができます。

※2023/12/18現在

Pulumi操作方法

ここからPulumiの操作方法を見て行きたいと思います

Pulumiインストール

個人的にはバージョン管理したいのでasdfでインストールします。brewでもインストールできます。

# .tool-versions
pulumi 3.97.0 
asdf install

Pulumi Cloudへログイン

デフォルトではPulumi Cloudへログインします。以下のコマンドを実行するとブラウザが起動するので、ログイン処理をします。

pulumi login

Pulumi Cloudを使わず、ローカルにstateを保存したい場合は以下のとおりです。

pulumi logout
pulumi loign --local

Projectの作成

pulumi new コマンドで新しいProjectを作成できます。同時にStackも作成されます。引数にテンプレートを指定できます。 ウィザード形式で設定をすることができます。

以下の例は awsプロバイダーを使用して、言語はTypeScriptを使用するテンプレートとなります。

ディレクトリ内にはPulumi実行に必要な各種ファイルが生成されます。 ここで見るべきは以下の3ファイルです。

  • Pulumi.yaml
    • プロジェクト設定
  • Pulumi.dev.yaml
    • Stack(dev)設定
  • index.ts
    • リソース定義
# Pulumi.yaml
name: sample
runtime: nodejs
description: A minimal AWS TypeScript Pulumi program
# Pulumi.dev.yaml
config:
aws:region: us-east-1
// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";

// Create an AWS resource (S3 Bucket)
const bucket = new aws.s3.Bucket("my-bucket");

// Export the name of the bucket
export const bucketName = bucket.id;

変更を確認

plumi preview コマンドでStackの変更差分を確認できます。 terraform plan を似ていますが、こちらは差分の詳細は表示されません。

Stackデプロイ

pulumi up コマンドでStackをデプロイできます。 terraform planterraform apply を組み合わせた挙動になります。

実行すると選択肢が出ます。

details を選択すると変更差分の詳細が表示されます。

yesを選択すると、変更が適用されます。

リソース削除

pulumi destroy でStackを削除できます。 pulumi up と同じようにdetailsで詳細表示、 yes で削除実行ができます

state操作

PulumiではStackごとにStateが保存されています。 Stateを操作するコマンドは以下のとおりです。

  • state出力(terraform state pull 相当 )
    • pulumi stack export
  • state インポート(terraform import相当)
    • pululmi import <TYPE> <NAME> <ID>
  • state 削除(terraform state rm 相当)
    • pulumi state delete <URN>

Terraformからの移行

Terraformからの移行オプションは以下の通りとなります。

  1. terraformとPulumiを共存する
    • Pulumiからtfstateを参照する
  2. tfstateからリソースをPulumiへインポートする
  3. TerraformのコードをPulumiのコードに変換する

参考: Adopting Pulumi | Pulumi Docs

参考: Migrating from Terraform | Pulumi Docs

TerraformとPulumiを共存する(tfstateを参照)

networkリソースに関しては既存のterraformを使いつつ、そのoutputをPulumiで使うイメージになります。 以下のようなコードでlocalのtfstateが参照できるので、値を参照して利用することができます。

import * as aws from "@pulumi/aws";
import * as terraform from "@pulumi/terraform";

// Reference the Terraform state file:
const networkState = new terraform.state.RemoteStateReference("network", {
    backendType: "local",
    path: "/path/to/terraform.tfstate",
});

// Read the VPC and subnet IDs into variables:
const vpcId = networkState.getOutput("vpc_id");
const publicSubnetIds = networkState.getOutput("public_subnet_ids");

// Now spin up servers in the first two subnets:
for (let i = 0; i < 2; i++) {
    new aws.ec2.Instance(`instance-${i}`, {
        ami: "ami-7172b611",
        instanceType: "t2.medium",
        subnetId: publicSubnetIds[i],
    });
}

tfstateからインポート

pulumi import --from terraform ./terraform.tfstate のようにすることによってtfstateからリソースをインポートすることができます。

terraformからコード変換

pulumi convert --from terraform コマンドを使用することで、既存のTerraformのコードをPulumiのコードに変換することができます。

ただし、変換できないコードはTODOコメントが付く。90%~95%は変換が対応しているとのこと。

pulumi convert --from terraform --language typescript

まとめ

Pulumiの概要と基本操作をTerraformと対比しながら説明してきました。

  • 新規プロジェクトである程度複雑な処理をしたい。
  • プログラミング言語に精通している人がメンバーにいる。

そういった場合にはPulumiは良さそうに思えます。

しかしながら、ある程度Terraformで出来上がっているプロジェクトをPulumiに移行するのはそれなりに大変なので、プロジェクトの規模感とコストに見合うかを考えて導入するか考えると良いでしょう。

また、複雑なことをしたいというのは、本当に必要とされていることなのでしょうか?冗長でも簡易的な書き方をした方が望ましい場合もあるかと思います。そのあたりの目利きをちゃんと考えたいところです。

自分自身まだまだ使いこなせていないですし、追いきれてないPulumiのトピックもあるので、今後も選択肢の一つとして調べていきたいところです。

Terraformのsopsプロバイダーを使用するだけで機密情報は守られるのか

qiita.com

この記事は、3-shake Advent Calendar 2023の9日目の記事となります。

sops プロバイダーとは

sopsプロバイダーはMozilla sopsを使用して暗号化されたファイルから機密情報を取り出して、terraform上で使用できるようにしたものです。 暗号化の鍵をAWS KMS等を使うことにより、KMSキーを使う権限を持つ人だけ機密情報にアクセスできるようにするものです。 sopsで機密情報を暗号化することにより、平文で機密情報をgitレポジトリに保存することがなくなり安全ということになります。

機密情報を管理したい。でも平文では保存したくない。そういう用途にこちらは使用されます。

本当に安心?

SOPSを使って機密情報を暗号化することによりgitレポジトリには機密情報が平文で残らない。

これで安心と言われていますが、よく考えると機密情報をterraform実行時にはリソースに対して平文で与えているはずです。 つまり、tfstate上は機密情報が平文で保存されています。

例えば、tfstateがS3に保存されているとして、KMSキーへの権限がない人でもS3バケットにアクセスする権限があれば、平文の機密情報が見れてしまいます。 あまりないと思いますが、tfstateをlocalに保存するようにしていてそれをgit管理していてらなんのために暗号化しているのか。。。。ということになります。

こう考えると組織のポリシーによるが、sopsプロバイダーによる暗号化では不十分ではないかという疑問が生まれます。

ドキュメントを調べる

まずプロバイダードキュメントを当たってみます。

Docs overview | carlpett/sops | Terraform | Terraform Registry

To prevent plaintext secrets from being written to disk, you must use a secure remote state backend. See the official docs on Sensitive Data in State for more information.

これが意味してるのはバックエンドをlocalにした場合平文で機密情報が書かれるので、安全なリモートバックエンドを利用すべきということだと思います。

State: Sensitive Data | Terraform | HashiCorp Developer

参照しろと言われたドキュメントの該当部分を読んでみましょう。

ローカルディスクにtfstateを保存した場合は、機密情報が平文で保存されます。リモートにtfstateを保存する場合、保存時に暗号化されるかはバックエンドに依存します。

基本的にリモートステートを使うことを推奨しています。 例えば、Terraform Cloudを使う場合、tfstateは暗号化され、転送時もTLSで暗号化されます。 S3を使う場合もSSE-S3やSSE-KMS等でサーバサイド暗号化を有効にしておくことで、保管時の暗号化がされます。バケットポリシーでHTTPSを強制することで通信時の暗号化も保証することができます。

参考: 暗号化によるデータの保護 - Amazon Simple Storage Service

参考: Amazon S3 のセキュリティのベストプラクティス - Amazon Simple Storage Service

ところがですね。保存時、通信時の暗号化をしても、terraform state pullすると平文でtfstateが手に入ってしまうんですよ。。。後述します。

挙動を実験する

以下のような設定ファイルを作ります。sopsで暗号化したdb_userとdb_passwordをパラメータストアに設定するものになります。

tools-versions

terraform 1.5.5
sops 3.7.3

main.tf

terraform {
  required_version = "~> 1.5.5"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.15"
    }
    sops = {
      source  = "carlpett/sops"
      version = "~> 0.7.2"
    }
  }
  backend "s3" {
    region  = "ap-northeast-1"
    bucket  = "xxxxxxxxxx"
    key     = "test.tfstate"
  }
}

provider "sops" {
}

provider "aws" {
  region = "ap-northeast-1"
}

data "sops_file" "secrets" {
  source_file = "secrets.yaml"
}

resource "aws_ssm_parameter" "db_user" {
  type     = "String"
  name     = "/test/db_user"
  value    = data.sops_file.secrets.data.db_user
}

resource "aws_ssm_parameter" "db_password" {
  type     = "SecureString"
  name     = "/test/db_password"
  value    = data.sops_file.secrets.data.db_password
}

暗号化前の secrets.yaml

db_user: user
db_password: password

apply結果がこちらとなります。

terraform apply

% export SOPS_KMS_ARN=arn:aws:kms:ap-northeast-1:xxxxxxxxx:key/yyyyyyyyyyyyyyyyyy
% terraform apply
data.sops_file.secrets: Reading...
data.sops_file.secrets: Read complete after 1s [id=-]

Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_ssm_parameter.db_password will be created
  + resource "aws_ssm_parameter" "db_password" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/test/db_password"
      + tags_all       = (known after apply)
      + tier           = (known after apply)
      + type           = "SecureString"
      + value          = (sensitive value)
      + version        = (known after apply)
    }

  # aws_ssm_parameter.db_user will be created
  + resource "aws_ssm_parameter" "db_user" {
      + arn            = (known after apply)
      + data_type      = (known after apply)
      + id             = (known after apply)
      + insecure_value = (known after apply)
      + key_id         = (known after apply)
      + name           = "/test/db_user"
      + tags_all       = (known after apply)
      + tier           = (known after apply)
      + type           = "String"
      + value          = (sensitive value)
      + version        = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_ssm_parameter.db_password: Creating...
aws_ssm_parameter.db_user: Creating...
aws_ssm_parameter.db_user: Creation complete after 0s [id=/test/db_user]
aws_ssm_parameter.db_password: Creation complete after 0s [id=/test/db_password]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
terraform apply  8.91s user 0.78s system 124% cpu 7.811 total

state showするとパラメータストアなのでsensitive扱いになっていて、見れません。これはいけるか?

terraform state show

% terraform state show aws_ssm_parameter.db_password
# aws_ssm_parameter.db_password:
resource "aws_ssm_parameter" "db_password" {
    arn       = "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_password"
    data_type = "text"
    id        = "/test/db_password"
    key_id    = "alias/aws/ssm"
    name      = "/test/db_password"
    tags_all  = {}
    tier      = "Standard"
    type      = "SecureString"
    value     = (sensitive value)
    version   = 1
}

% terraform state show aws_ssm_parameter.db_user    
# aws_ssm_parameter.db_user:
resource "aws_ssm_parameter" "db_user" {
    arn       = "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_user"
    data_type = "text"
    id        = "/test/db_user"
    name      = "/test/db_user"
    tags_all  = {}
    tier      = "Standard"
    type      = "String"
    value     = (sensitive value)
    version   = 1
}

ここで、terraform state pullをしてみて、tfstateファイルをローカルにダウンロードします。

そのtfstateファイルの中の該当部分はこちらとなります。

    {
      "mode": "managed",
      "type": "aws_ssm_parameter",
      "name": "db_password",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "allowed_pattern": "",
            "arn": "arn:aws:ssm:ap-northeast-1:xxxxxxxxx:parameter/test/db_password",
            "data_type": "text",
            "description": "",
            "id": "/test/db_password",
            "insecure_value": null,
            "key_id": "alias/aws/ssm",
            "name": "/test/db_password",
            "overwrite": null,
            "tags": null,
            "tags_all": {},
            "tier": "Standard",
            "type": "SecureString",
            "value": "password",
            "version": 1
          },
          "sensitive_attributes": [
            [
              {
                "type": "get_attr",
                "value": "value"
              }
            ]
          ],
          "private": "bnVsbA==",
          "dependencies": [
            "data.sops_file.secrets"
          ]
        }
      ]
    },

tfstateファイルの中身をよく確認するとしっかり平文で見えています。残念。

"value": "password",

結論

sopsプロバイダーを使用することによりgitレポジトリ上に機密情報を平文で保存することはなくなります。 しかしながら、tfstateのデータ上では設定値が平文で保存されることを防ぐことはできません。terraform state pullする権限があれば、機密情報が見れてしまいます。

運用組織のポリシーで、tfstateへのアクセス権限を適切に権限管理することができるのであれば、選択肢としては取りうります。 暗号化のためのKMSキー、tfstateを保存するS3バケットを機密情報をアクセス可能な人のみ権限を与えることが徹底できればよいです。 しかしながら、機密情報をいかなる場合でもローカルに平文で保存することが許容されない組織であれば、機密情報は手動で設定することを選択したほうが望ましいと思います。

どうしても機密情報をterraformで管理したのであれば、クライアントサイドで暗号化した機密情報をterraformで管理し、アプリ等で使用時にクライアントサイドで復号を行う形も考えられます。

安全かどうかは、tfstateの保存場所、tfstateへのアクセス権限、暗号化鍵のアクセス権限それぞれが適切に設定されているかどうかが鍵となります。

他に何かうまい方法で機密情報を管理しているという方がいらっしゃれば、ご意見ください。

ワークアラウンド

これは自分がよく使う手段となります。

リソースの箱だけ作って、作成時にダミーの値を入れておき、実際の値は手動で設定するという手法です。 ignore_changesを入れておくことで、手動で値を変更しても、terraform的には差分ができないようにしています。 これにより、機密情報をterraformの外に追い出しつつも、機密情報を入れるリソース自体は監理するということが実現できます。

resource "aws_ssm_parameter" "db_password" {
  type     = "SecureString"
  name     = "/test/db_password"
  value    =  "Dummy"
  lifecycle {
    ignore_changes = [value]
  }
}

吉祥寺.pm35 でLTしてきました。 #kichijojipm

吉祥寺.pm こと 句会

吉祥寺.pm35 に参加して、LTしてきました。

kichijojipm.connpass.com

資料はこちら。

言いたいこととしてはベストプラクティスなんてないよ。一般的によりよいプラクティスやパターンはあるけど、どんなときには適用できる銀の弾丸的なものはないから、自身の組織とサービスに合わせてくみ上げていきましょうということ。

正解はひとつ!じゃない!!

その上で、ざっくりとどんな選択肢と選択するための観点を述べていきました。まだ全然ブラッシュアップできるのでどこかでまとめてブログに書きたいところです。

ちなみに最後に出てくる あなたらしく○○ は同僚のスライドのパロディです。

毎回時間オーバーするのでトークで申し込んだ方が良いのでは?というツッコミはごもっともです。

懇親会でもTerraformのお悩みとか短いですが話せて楽しかったです。また参加したいですね。

AWS Control Towerを調べる

これは 3-shake Advent Calendar 2022 10日目の記事です

仕事の中でAWSで複数のアカウントを管理したいという要件あり、その中でAWS Control Towerが使えないかなと調べたものをざっくりと書いていきます。

AWS Control Towerとは

AWS Control TowerとはLanding Zoneを実装するためのAWSのマネージドサービスです。そもそもLanding Zoneって何って話になりますね。

Landing Zoneとは

セキュリティとコンプライアンスのベストプラクティスに基づきアーキテクチャ設計とマルチアカウント環境を管理する仕組みを指します。

Landing Zoneは、下記機能から構成されます。

  • アカウントの発行
    • 必要な初期設定の済んだアカウントを作成
  • 管理用権限の発行
    • 対象アカウントを管理するための権限を作成
  • AWS ログの集約
    • 監査用ログをセキュアに一元保存
  • ガードレールの設置
    • 実施してはいけない操作の禁止
    • 危険な設定の監視

Landing Zoneの実装方法

AWS Control Tower

AWSサービスとして提供される Landing Zoneです。容易に利用可能ですが、カスタマイズするには制限があります。(必須のガードレールを外せなかったり)

主にこれからAWSを利用する場合に利用できます。既存アカウントにも適用可能です。

独自実装の Landing Zone

自組織で独自実装するパターンです。自組織の方針に従って自由にカスタマイズできるのが強みです。ただし、自由にカスタマイズはできますが、自身でメンテナンスしないといけないので、コストはかかります。

主に既存アカウントに適用する場合に利用できます。自組織でアカウント発行の仕組みや管理の仕組みができあがってる場合などです。

そもそもなんでマルチアカウントにするのか

AWSをマルチアカウントにする観点として以下のものが考えられます。

  • 環境の分離
    • 開発、テスト、本番を分離することによるセキュリティおよび統制の確保
  • 請求の分離
    • 部門やシステム単位でのコスト明確化
  • 権限の分離
    • 部門間での権限分離およびアカウントへの権限移譲
  • 複雑性の分離
    • アカウントの目的を明確に絞ることで、構成がシンプルになる

AWS Organizationsだけでもできること

マルチアカウント管理するだけならOrganizationだけでもある程度はできます。むしろAWS Control TowerはOrganizationの機能を利用しています。

AWS Control Towerだと何ができるのか

Control Towerで提供される機能として以下のものがあります。

  • Landing Zoneの提供
    • AWS Organizationを使用してマルチアカウントを作成
      • デフォルトでSandbox、SecurityのOUを作成
    • AWS IAM アイデンティティセンターを利用したID管理を提供
  • Account Factory
    • AWSアカウントのプロビジョニングの自動化
    • 設定可能なテンプレートを提供
  • CloudTrailとConfigログの保存
    • Log Archiveアカウント内のS3バケットに一元的に保存される
  • ガードレールの提供
    • 必須と任意の観点の2種類と予防的と発見的の2種類の組み合わせがありControl Towerにより管理下のアカウントに適用される
    • 参考: ガードレールの仕組み
    • 予防的ガードレール(Service Control Policy)
      • 禁止されたアクションの実行が拒否される仕組み
        • Control Tower管理下のアカウントは必須の予防的ガードレールで禁止されているアクションが不可能
    • 発見的ガードレール(Config)
      • 特定のイベントが発生したときにCloudTrailに記録される仕組み
  • ダッシュボード
    • OUやアカウント、ガードレール違反などが一覧表示できる

AWS Control Towerではできないこと

AWS Control Towerでは提供されてない機能もあります。GuardDutyやSecurity Hubなどのセキュリティ機能を組織全体適用するにはOrganizationsの機能を利用する必要があります。

AWS Control Towerの注意点、制約事項

いろいろ資料を見てみてこの辺注意が必要かなという点を書いていきます。

注意点

制限とクォータ

  • S3へのログの保存期間は、最大15年間保存可能(最近アップデートされた)
  • Security OU の共有アカウントの E メールアドレスは変更可能だが、これらの変更を AWS Control Tower コンソールで確認するには、Landing Zone を更新する必要がある
  • AWS Control Tower Landing zone の OU には、OU あたり5個のSCPの制限が適用される
  • 300超のアカウントを持つ既存の OU は、AWS Control Tower に登録することはできない
    • 300を超える場合はOUを分ける必要がある
  • OUのネストは2段階まで、孫OUを持つことはできない

AWS Control Towerを使うべきなのか

マルチアカウントを展開していくのであれば、AWSのベストプラクティスに乗れるので、使用するのが無難です。

ただし、独自のLanding Zoneをすでに構築しており、Account Factoryの仕組みも独自で構築できているのであれば、移行コストを鑑みてそのままでも問題ないです。

必須の予防的ガードレールが許容できない、OUなどの制限にひっかるなどの運用上の制約がある場合は使えないので、組織のポリシーを見直すか、独自でLanding Zoneを作るかを考える必要があります。

発展

もっと調査したかったが、時間が足りなかったことや今後調べたいことです。

コンソールからAccount Factory実行するとService Catalogの設定項目がありますが、Service Catalog自体の理解不足でどう扱うのかが把握できてないのでこの辺調べたいです。

Account Factory for Terraform(AFT)を使うとアカウント発行そのものもIaC化できるので試したい。

Customization for Control Tower(CfCT)を使うとアカウント発行のイベントをトリガーにCloudFormationを実行できるので、これも実験したい。

まとめ

Control Towerについて調べたことを書いていきました。

実運用自体はまだしてないので、これから触ってみて知見が溜まってきたらまたそれも共有できたらと思います。