Terrarformのtarget指定をちょっと楽にするシェルスクリプト

# はじめに

「Terraformのtarget指定がめんどくさい」「targetじゃなくてexclude的なオプションが欲しい」そんなことを考えていた私が過去に書いたshellのファイルを旧PC整理中に見つけたので供養しようと思います。

※注意１：[Terraform公式](https://learn.hashicorp.com/tutorials/terraform/resource-targeting?in=terraform/cli)では下記のような記載があり-targetの通常利用は推奨されていません。

>In a typical Terraform workflow, you apply the entire plan at once. Occasionally you may want to only apply part of a plan, such as situations where Terraform's state has become out of sync with your resources due to a network failure, a problem with the upstream cloud platform, or a bug in Terraform or its providers. To support this, Terraform allows you to target specific resources when you plan, apply, or destroy your infrastructure. Targeting individual resources can be useful for troubleshooting errors, but should not be part of your normal workflow.

※注意２：あくまで一発ネタなスクリプトです。思わぬ挙動をする可能性があるので、terraform apply前には必ずtargetの指定が正しいか確認してください。どのような結果になろうと責任は持てません。

※注意３：本スクリプトは2020年5月ごろに作成しました。公開前に少し手直しはしましたが、内容が古い可能性があります。

※注意４：筆者はterraformのproviderとしてAWS以外を利用したことはありません。他providerでの動作確認などはしておりません。


## 作成した背景
terraformをお使いの方は以下のような検索をしたことがある人もいるのではないでしょうか。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525823/53a1ffc2-8946-e28a-71f9-1b3382485754.png)

terraformではplanやapply時に-targetというオプションを使うことで、特定のリソースのみに変更を適用することが可能です。  

1人での開発であれば、あまり必要ないオプションだとは思うのですが、複数人で開発している際に他人が修正しているリソースが差分として表示されたり、お互いのapplyによってリソースの変更合戦が始まってしまったりと、何かと必要な場面があります。

実際のコマンドとしては、以下のように-targetでリソースをつないでいくことになるので、指定する数が増えると心が折れそうになります。そんなtarget指定を少しでも楽にできないかと思ったことが発端です。

```
# target指定の例
terraform plan -target=aws_cloudwatch_log_group.hoge -target=aws_eip.hoge -target=aws_internet_gateway.fuga
```

## スクリプトの基本情報
コマンド名は適当に tt-gen(terraform target generator) としています。好きに変えてください。

### オプション

```
-f 指定したファイルの中に含まれるリソースすべてをtargetの対象として出力します。
-i 指定した文字列を含むリソースをtargetの対象として出力します。
-e 指定した文字列を含まないリソースをtargetの対象として出力します。
```


### 動作例

適当な以下の3つのファイルをが存在するディレクトリを例に説明します。

```vpc.tf
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}
```

```subnet.tf
resource "aws_subnet" "public1" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

resource "aws_subnet" "public2" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.2.0/24"
}

resource "aws_subnet" "private1" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.3.0/24"
}

resource "aws_subnet" "private2" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.4.0/24"
}
```

```module.tf
module "consul" {
  source  = "hashicorp/consul/aws"
  version = "0.0.5"

  servers = 3
}
```

#### オプション指定なし

オプション指定がない場合、ディレクトリに含まれるリソースすべてがtargetとして出力されます。ほとんど使いません。  
いったんすべてのresourceを出力して、手動で不要なリソースを削除していくといった場合に利用します。

```
❯ tt-gen
-target=aws_subnet.public1 -target=aws_subnet.public2 -target=aws_subnet.private1 -target=aws_subnet.private2 -target=aws_vpc.main -target=module.consul
```

#### -fオプション(ファイル指定)

-fオプションを利用した場合、指定したファイルに含まれるリソースのみが出力されます。

```
❯ tt-gen -f subnet.tf
-target=aws_subnet.public1 -target=aws_subnet.public2 -target=aws_subnet.private1 -target=aws_subnet.private2
```

##### 複数ファイルを対象にする
複数のファイルを指定したり、特定のファイルを排除したい場合は `""` で囲ってファイル名を指定してください。

```sh
❯ tt-gen -f "vpc.tf module.tf"
-target=aws_vpc.main -target=module.consul
```

grepコマンドと組み合わせることでより柔軟にファイルを指定することができます。(`""`で囲うこと忘れずに)

```sh
# ファイル名にeを含むもののみ対象として出力する
❯ tt-gen -f "$(ls | grep e)"
-target=aws_subnet.public1 -target=aws_subnet.public2 -target=aws_subnet.private1 -target=aws_subnet.private2 -target=module.consul
```

##### 特定のファイルを対象外にする
grepコマンドと組み合わせることで特定のファイルを除外することも可能です。

```sh
# ファイル名にsubnetを含むファイルを対象外にする
❯ tt-gen -f "$(ls | grep -v subnet)"
-target=aws_vpc.main -target=module.consul
```

#### -iオプション(指定した文字列を含む)

-iオプションで指定した文字列を含むリソースが表示されます。
ここでいうリソース名とは `aws_subnet.public1` といったものを指します。  
〇〇関連のリソースだけapplyしたいといった場合に利用します。

```
❯ tt-gen -i public
-target=aws_subnet.public1 -target=aws_subnet.public2
❯ tt-gen -i sub
-target=aws_subnet.public1 -target=aws_subnet.public2 -target=aws_subnet.private1 -target=aws_subnet.private2
```

#### -eオプション(指定した文字列を含まない)

-e で指定した文字列を除いたリソースが表示されます。  
特定のリソースのみ対象外にしたい場合に利用します。

```
❯ tt-gen -e public
-target=aws_subnet.private1 -target=aws_subnet.private2 -target=aws_vpc.main -target=module.consul
```

#### オプションの複数指定

オプションは複数組み合わせることが可能です。
-iオプションに複数の引数を渡すといったことに対応していないことが心残りです。

```
❯ tt-gen -f subnet.tf -i public -e 1
-target=aws_subnet.public2
```

#### terraformコマンドと組み合わせる

以下のように記載することでワンライナーでterraformコマンドを実行することが可能です。

```sh
terraform plan $(tt-gen -i subnet)
```

## 導入

### 動作環境
以下環境で動作することを確認済みです。
grepとcatとechoくらいしかコマンドは使っていないので特別事前準備は必要ないと思います。
* Windows10 WSL2 Ubuntu2004
* macOS Catalina ver.10.15.6

### 導入方法
以下ファイルをコピーしてパスの通ったディレクトリに配置してください。  
ファイル名がコマンドとなるので、好きなファイル名で保存してください。

```sh
#!/bin/sh
set -Ceu

CMDNAME=`basename $0`
FLG_F="FALSE"
FLG_I="FALSE"
FLG_E="FALSE"

while getopts f:i:e: OPT
do
  case $OPT in
    "f" ) FLG_F="TRUE" ; VALUE_F="$OPTARG" ;;
    "i" ) FLG_I="TRUE" ; VALUE_I="$OPTARG" ;;
    "e" ) FLG_E="TRUE" ; VALUE_E="$OPTARG" ;;
      * ) echo "Usage: $CMDNAME [-f VALUE] [-i VALUE] [-e VALUE]" 1>&2
          exit 1 ;;
  esac
done

# ファイル指定がある場合は対象ファイル名の存在を確認し、ファイル名をセットする。
if [ "$FLG_F" = "TRUE" ]; then
  filename="$VALUE_F"
# ファイル指定がない場合は拡張子が.tfのファイルすべてを対象とする
else
  filename="*.tf"
fi

# コメント行の削除(#を含む行はすべて無視する)
content=$(cat $filename| grep -v "#")

# -iオプションが存在する場合は指定された単語が含まれる行を対象とする
if [ "$FLG_I" = "TRUE" ]; then
  content=$(echo "$content"| grep $VALUE_I)
fi

# -eオプションが存在する場合は指定された単語が含まれる行を除外する
if [ "$FLG_E" = "TRUE" ]; then
  content=$(echo "$content"| grep -v $VALUE_E)
fi

# resourceとmoduleで出力内容が異なるので処理を分ける
echo "$content"| grep -E 'resource '| tr -d '"'| awk '{ printf("-target=%s.%s ", $2, $3) }'
echo "$content"| grep -E 'module '| tr -d '"'| awk '{ printf("-target=%s.%s ", $1, $2) }'

exit 0
```

```sh
# 導入イメージ(各自の環境に読み替えてください)
mkdir ~/my-script
cd my-script
vim tt-gen # ここで上記の内容をコピペする

# パスを通す(.zshrcなどに記載)
export PATH="$PATH:$HOME/my-script"
# 権限エラーが出る場合は対象ファイルに実行権限を付与する
chmod 777 tt-gen
```

## 参考
* https://gist.github.com/mAster-rAdio/0749d320a3dffefad4df5629224cd808
* https://qiita.com/saki-engineering/items/57956af61c191b0e3282