[アップデート] AWS SAM CLI のTerraform統合機能が任意のplanの結果の読み込みに対応しTerraform Cloudを活用できるようになりました

2023.09.07

初めに

先日リリースされたAWS SAM CLIのv1.96.0でAWS SAM CLIのTerraform統合の利用において任意のplan結果を読み込ませてローカルでテストできるようになりました。

執筆の順番が前後してしまいましたがGAとなったv1.97.0ではなくv1.96.0からの対応です。

過去に書いた記事で述べたことがありますが、SAMでのTerraformの利用についてはSAM側でネイティブにTerraform相当の処理をしているのではなく内部的に実際にterraformコマンドを実行してその出力を元にCloudFormationテンプレートに変換を行うものとなっております。

以前までこの一連の処理でterraform plan実行されそれを読み込んでビルドする形になっていましたが、今回はそれを自前でどうにかできるようになったアップデートです(sam build自体は対応しておらず直接sam local系のコマンドに指定する形にはなっている)。

Terraform普段使わないため任意のplan結果を読み込ませて嬉しいケースというのがあまり明確に出てこなかったのですが、例外処理で出力されるメッセージやAWSのドキュメント上でTerraform Cloudの利用について言及されており目的としてはこちらの利用のためとなりそうです。

※31行目あたり

Terraform Cloudを活用してのローカルでの実行試してみます。

準備

Terraformを利用したプロジェクトは以前検証したものがあるのでそちらを使っています。

$ tree sam-app-terraform/
.
├── hello_world                 ## lambdaのアプリコード(terraformの構成とは関係ない)
│   ├── __init__.py
│   └── app.py
└── terraform                   ## ここ配下がterraform関連のファイル
    ├── environments
    │   └── dev
    │       ├── main.tf         ## これがメインのファイル
    │       └── samconfig.toml
    └── modules
        └── hello_world
            └── main.tf

今回会員登録から行っていますが、メールアドレス等の情報を入れるだけなので省略します。

登録後(ログイン後)にローカル環境でterraform loginを実行すると実行の確認がくるのでyesを入力し、その後トークンの発行画面がブラウザで開かれるので有効期限等を入力しトークンを発行、それをプロンプトに貼り付けます。

% terraform login
Terraform will request an API token for app.terraform.io using your browser.

If login is successful, Terraform will store the token in plain text in
the following file for use by subsequent commands:
    /Users/xxxxxx/.terraform.d/credentials.tfrc.json

Do you want to proceed?
  Only 'yes' will be accepted to confirm.

  Enter a value: yes


---------------------------------------------------------------------------------

Terraform must now open a web browser to the tokens page for app.terraform.io.

If a browser does not open this automatically, open the following URL to proceed:
    https://app.terraform.io/app/settings/tokens?source=terraform-login


---------------------------------------------------------------------------------

Generate a token using your browser, and copy-paste it into this prompt.

Terraform will store the token in plain text in the following file
for use by subsequent commands:
    /Users/xxxxxx/.terraform.d/credentials.tfrc.json

# ここでAPIキー発行画面がブラウザで開くので対応して発行したAPIキーを入力
Token for app.terraform.io:
  Enter a value: 


Retrieved token for user xxxxxx


---------------------------------------------------------------------------------

                                          -                                
                                          -----                           -
                                          ---------                      --
                                          ---------  -                -----
                                           ---------  ------        -------
                                             -------  ---------  ----------
                                                ----  ---------- ----------
                                                  --  ---------- ----------
   Welcome to Terraform Cloud!                     -  ---------- -------
                                                      ---  ----- ---
   Documentation: terraform.io/docs/cloud             --------   -
                                                      ----------
                                                      ----------
                                                       ---------
                                                           -----
                                                               -


   New to TFC? Follow these steps to instantly apply an example configuration:

   $ git clone https://github.com/hashicorp/tfc-getting-started.git
   $ cd tfc-getting-started
   $ scripts/setup.sh

実行時に読み込まれるtfファイルにハイライト部分の内容があれば良いようです。今回はmain.tf1ファイルにまとめてます。

1 ワークスペース = 1 Stateの考えになるとのことなので、複数の環境を作る場合はそれごとに別のワークスペースを指定することとなりそうです(佐藤雅樹さんにご教示いただきました。ありがとうございます)。

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

terraform {
  cloud {
    organization = "xxxxxxxxxx"

    workspaces {
      name = "example-workspace"
    }
  }
}

module "hello_world" {
  source = "../../modules/hello_world"
}

ワークスペースの環境変数としてTerraformの実行に利用するユーザのアクセスキーを設定します。現在ではIAMロールの指定が可能なオプションもあるようですが今回は利用していません。

Terraform Cloudを利用した実行には各種コマンド自体をCloud側で実行するRemoteモードと、ローカルで実行してStateをCloud側で管理するLocalモードがありますが今回はリモート側で実行させたplan結果を利用したいのでRemoteで実行します。現時点ではデフォルトがRemoteなので特に変更する必要はありません。

これで準備完了で設定した環境でコマンドを実行するとTerraform Cloud上で処理が行われるようです。

plan結果の生成

ここでのイメージとしては現状SAM側で内部で実行されているような処理を手動で実行する形になります。

$ terraform init
Initializing modules...
- hello_world in ../../modules/hello_world

Initializing Terraform Cloud...

Initializing provider plugins...
- Finding latest version of hashicorp/aws...
- Finding latest version of hashicorp/null...
- Installing hashicorp/aws v5.15.0...
- Installed hashicorp/aws v5.15.0 (signed by HashiCorp)
- Installing hashicorp/null v3.2.1...
- Installed hashicorp/null v3.2.1 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform Cloud has been successfully initialized!

You may now begin working with Terraform Cloud. Try running "terraform plan" to
see any changes that are required for your infrastructure.

If you ever set or change modules or Terraform Settings, run "terraform init"
again to reinitialize your working directory.

terraform planを実行します。

% terraform plan
Running plan in Terraform Cloud. Output will stream here. Pressing Ctrl-C
will stop streaming the logs, but will not stop the plan running remotely.

Preparing the remote plan...

The remote workspace is configured to work with configuration at
environments/dev/ relative to the target repository.

Terraform will upload the contents of the following directory,
excluding files or directories as defined by a .terraformignore file
at /Users/xxxx/git/sam-app-terraform/terraform/.terraformignore (if it is present),
in order to capture the filesystem context the remote workspace expects:
    /Users/xxxx/git/sam-app-terraform/terraform

To view this run in a browser, visit:
https://app.terraform.io/app/xxxxxx/example-workspace/runs/run-xxxxx

Waiting for the plan to start...

Terraform v1.3.2
on linux_amd64
Initializing plugins and modules...

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:

  # module.hello_world.aws_api_gateway_integration.hello_world will be created
  + resource "aws_api_gateway_integration" "hello_world" {
      + cache_namespace      = (known after apply)
      + connection_type      = "INTERNET"
      + http_method          = "GET"
      + id                   = (known after apply)
      + passthrough_behavior = (known after apply)
      + resource_id          = (known after apply)
      + rest_api_id          = (known after apply)
      + timeout_milliseconds = 29000
      + type                 = "AWS_PROXY"
      + uri                  = (known after apply)
    }
##長いので省略

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

結果の取得と実行

変更されるリソースは画面上に表示されますがplan実行結果のファイルはTerraform Cloud側が保持しているのでAPIで取得します。

TOKENには先ほど発行したAPIトークンをRUN_IDには先ほどの実行に単一のIDのようなものが割り当てられているのでそれを設定します(RUN_IDは先ほどのplan実行の17行目やTerraform Cloudの画面より取得する)。

$ export TOKEN=xxxxx
$ export RUN_ID=run-xxxx
$ curl -O -L \      
   -H "Authorization: Bearer ${TOKEN}" \
   -H "Content-Type: application/vnd.api+json" \
   https://app.terraform.io/api/v2/runs/${RUN_ID}/plan/json-output

APIの出力はterraform plan -out xxxの結果バイナリ相当ものではなくそれをterraform show -json xxxで実行した相当のもの(jsonファイル)となるようです。

この結果を--terraform-plan-fileオプションに指定してsam local系のコマンドを動かすとその結果を元に処理してくれます。

## 現状sam build単品では対応していない模様
% sam build --hook-name terraform --terraform-plan-file json-output 
Usage: sam build [OPTIONS] [RESOURCE_LOGICAL_ID]
Try 'sam build -h' for help.

Error: No such option: --terraform-plan-file Did you mean --template-file?
## 実行前に一時ファイル系を削除したが特に問題なく実行できる
$ sam local invoke --hook-name terraform --terraform-plan-file json-output 
Running Prepare Hook to prepare the current application                                                                                                                     
Executing prepare hook of hook "terraform"                                                                                                                                  
Using provided plan file: json-output                                                                                                                                       
Generating metadata file                                                                                                                                                    
                                                                                                                                                                            
Unresolvable attributes discovered in project, run terraform apply to resolve them.                                                                                         
                                                                                                                                                                            
Finished generating metadata file. Storing in /Users/xxxx/git/sam-app-terraform/terraform/environments/dev/.aws-sam-iacs/iacs_metadata/template.json                
Prepare hook completed and metadata file generated at: /Users/xxxx/git/sam-app-terraform/terraform/environments/dev/.aws-sam-iacs/iacs_metadata/template.json       
Invoking app.lambda_handler (python3.11)                                                                                                                                    
Local image is up-to-date                                                                                                                                                   
Using local image: public.ecr.aws/lambda/python:3.11-rapid-arm64.                                                                                                           
                                                                                                                                                                            
Mounting /Users/xxxx/git/sam-app-terraform/hello_world as /var/task:ro,delegated, inside runtime container                                                          
START RequestId: c90765a7-47a3-4905-b801-2aa307db73d9 Version: $LATEST
END RequestId: c90765a7-47a3-4905-b801-2aa307db73d9
REPORT RequestId: c90765a7-47a3-4905-b801-2aa307db73d9  Init Duration: 0.29 ms  Duration: 82.82 ms      Billed Duration: 83 ms  Memory Size: 128 MB     Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"hello word\"}"}%

なおこの場合.aws-sam/buildは生成されないようです。

total 64
drwxr-xr-x  8 xxxx  staff    256  9  7 19:03 .
drwxr-xr-x  3 xxxx  staff     96  9  5 21:50 ..
drwxr-xr-x  3 xxxx  staff     96  9  7 19:03 .aws-sam-iacs
-rw-r--r--  1 xxxx  staff   2422  9  7 14:58 .terraform.lock.hcl
-rw-r--r--  1 xxxx  staff  13786  9  7 18:54 json-output
-rw-r--r--  1 xxxx  staff     63  9  7 18:08 main.tf
-rw-r--r--  1 xxxx  staff    714  9  7 19:12 samconfig.toml
## CFnに変換された結果やビルドに必要なものは生成されてる
$ tree .aws-sam-iacs
...
└── iacs_metadata
    ├── Makefile
    ├── copy_terraform_built_artifacts.py
    ├── template.json
    ├── z_samcli_backend_override
    └── zip.py

なお今回Terraform統合利用時のこの辺りの自分の理解が怪しいのでアプリ側がビルドなしで通るものを採用していますが、ビルドが必要なものとなると挙動がイメージできていないところがあります。この点については後日検証と記事執筆を予定しています。

sam buildを呼ぶと

Terraform Cloudを使用する設定をした状態でsam buildを実行するとエラーとなります。

% sam build
                                                                                                                                                                                       
Experimental features are enabled for this session.                                                                                                                                    
Visit the docs page to learn more about the AWS Beta terms https://aws.amazon.com/service-terms/.                                                                                      
                                                                                                                                                                                       
Running Prepare Hook to prepare the current application                                                                                                                                
Executing prepare hook of hook "terraform"                                                                                                                                             
Initializing Terraform application                                                                                                                                                     
.........
Creating terraform plan and getting JSON output                                                                                                                                        
.....
Error: Terraform Cloud does not currently support generating local plan files that AWS SAM CLI uses to parse the Terraform project.
To use AWS SAM CLI with Terraform Cloud applications, provide a plan file using the --terraform-plan-file flag.

これについては以下で言及されていますがTerraform Cloudを利用する場合planの直接の結果(バイナリの方)には機密情報が含まれるため取得できないためのようです。

sam buildで内部的に実行されるterraform plan -out outputを直接実行してみるとサポートされていない旨が表示されエラーとなります。

 % terraform plan -out output
╷
│ Error: Saving a generated plan is currently not supported
│ 
│ Terraform Cloud does not support saving the generated execution plan locally at this time.
╵

SAM側ではおそらくこのエラーを引っ掛けて先ほどのsam buildのようなメッセージを出しているものと思われます。

終わりに

さて今回はSAMのローカルテストで任意のplanファイルが利用可能となったことと、それにより可能となった作業としてTerraform Cloudを併用した実行をご紹介させていただきました。

最初見た時はTerraform Cloudがどういうものか知らなかったので、実行までいくつもコマンドが必要でめんどくさい!1コマンドでできるようにし欲しいと思っていました。

ただ実際に触ってみるとそもそもTerraform Cloudは結果を複数人で共有するようなものであることがわかり実際のユースケースを考えてみると、利用者的にはおそらくCIツールや管理者側でterraform planが実行された状態からスタートするので作業的にはcurlでのplanの結果の取得と実際の実行のみでまとめるほどのものでもないなと気づきました。

それに気づくと結構面白そうな機能でGithubと連携したapplyやplanの実行が以下の記事で紹介されていますが、デプロイ前の最終確認としてそのplanを取得し動作確認ということや別の何かと連携してテストをしたり、デプロイ後のバグ発生時にAPIで環境を取得してローカルでも再現できるかの確認等色々なことができそうです。

興味のある方はぜひ一度触ってみてはいかがでしょうか。