Terraform でインフラの構成構築を行っている際に、既存の CognitoにLambdaトリガーを取り付けたい、というシーンがありました。
Cognito IDP の Data Sources(data
ブロックで表現される、Terraformの外部で定義された情報を参照できるリソース)を頼りにトリガーとなる Lambda の設定ができないかと Terraform のリファレンスを眺めていたのですが、残念ながら既存のCognitoに対して変更を行えそうなリソースが見つかりませんでした。
そこで、Terraform から AWS CLI のコマンドを実行して Lambda の取り付けができないかと考えたのですが、いくつか注意点があったのでその紹介です。
前提
Terraform 自体やシェルスクリプトの説明は行いません。
Lambdaの作成する準備
まず、検証用の Lambda 関数をデプロイするためのインフラコードを作成します。
CognitoへのLambdaの取り付けが確認できれば良いので、以下の Terraform のインフラコードで空っぽのファイルをzip圧縮したダミーのコードを持つ Lambda関数を作成します。
※ local.region はお使いのリージョンに読み替えてください。
locals { region = "us-west-2" } provider "aws" { region = local.region shared_credentials_files = ["~/.aws/credentials"] profile = "dev" } data "archive_file" "dummy" { type = "zip" output_path = "${path.module}/dummy.zip" source { content = "dummy" filename = "bootstrap" } } resource "aws_iam_role" "iam_for_lambda" { name = "iam_for_lambda" assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": "sts:AssumeRole", "Principal": { "Service": "lambda.amazonaws.com" }, "Effect": "Allow", "Sid": "" } ] } EOF } resource "aws_lambda_function" "cognito_trigger_lambda" { function_name = "cognito-trigger-lambda" role = aws_iam_role.iam_for_lambda.arn filename = data.archive_file.dummy.output_path runtime = "nodejs14.x" handler = "index.test" }
update-user-pool コマンド
AWS CLI のコマンド cognito-idp update-user-pool
でCognitoにLambdaトリガーを取り付けることができますが、注意が必要です。
https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cognito-idp/update-user-pool.html
If you don’t provide a value for an attribute, it will be set to the default value.
リファレンスには「属性の値を指定しない場合は、デフォルト値に設定される」とあります。
実際、単純に以下のようにコマンドを実行してしまうと、Lambdaトリガーの取り付けが行われるものの、既になされている他の設定がデフォルト値に戻ってしまいます。
aws cognito-idp update-user-pool --user-pool-id "<ユーザープールID>" --lambda-config PreSignUp="<Lambda関数のARN>"
このことは aws/aws-cli の issue としても挙がっています。
https://github.com/aws/aws-cli/issues/3302
解決策としては、JSONプロセッサ jq を駆使して以下のように現在の設定を —cli-input-json
で指定しつつ、新規に追加したい内容(ここではLambdaトリガー)を --lambda-config
で指定する方法が提示されています。
user_pool_id="<ユーザープールID>" function_arn="<Lambda関数のARN>" # 利用可能な設定のキーを取得する config_keys=$(aws cognito-idp update-user-pool --generate-cli-skeleton | jq 'keys') # 現在のユーザープールの設定値を取得し、更新用データを作成する # note: we delete AdminCreateUserConfig.UnusedAccountValidityDays because it is not supported when using Policies.PasswordPolicy.TemporaryPasswordValidityDays current_config=$(aws cognito-idp describe-user-pool --user-pool-id "${user_pool_id}" | jq --argjson whitelist "${config_keys}" '.UserPool | . + {UserPoolId:.Id} | with_entries(select(.key as $a | any($whitelist[]; . == $a))) | del(.AdminCreateUserConfig.UnusedAccountValidityDays)') # 現在のユーザープールの設定にLambdaトリガーの設定をマージする aws cognito-idp update-user-pool --lambda-config "PostConfirmation=${function_arn}" --cli-input-json "${current_config}"
この方法で良さそうに思えたのですが、このままでは、まだ少し課題があることに気が付きました。
上記で取り付けている関数以外に、事前に取り付けられているLambda関数の設定がある場合、事前の設定が消失してしまいます。
例えば、Cognitoにサインアップ前に動作するLambdaトリガー(PreSignUp)が取り付けられている状態で、上記のように PostConfirmation の Lambdaトリガーの設定を行うと、PreSignUp に取り付けられている Lambdaの設定は無くなってしまいます。
これを回避するために以下のように追加したい内容をJSONで表現するようにして、既存の設定のJSONにマージして設定を行うことで、他の設定に影響を出さずに新規で意図したLambdaトリガーの取り付けが行えるようになります。
user_pool_id="<ユーザープールID>" function_arn="<Lambda関数のARN>" # 利用可能な設定のキーを取得する config_keys=$(aws cognito-idp update-user-pool --generate-cli-skeleton | jq 'keys') # 現在のユーザープールの設定値を取得し、更新用データを作成する # note: we delete AdminCreateUserConfig.UnusedAccountValidityDays because it is not supported when using Policies.PasswordPolicy.TemporaryPasswordValidityDays current_config=$(aws cognito-idp describe-user-pool --user-pool-id "${user_pool_id}" | jq --argjson whitelist "${config_keys}" '.UserPool | . + {UserPoolId:.Id} | with_entries(select(.key as $a | any($whitelist[]; . == $a))) | del(.AdminCreateUserConfig.UnusedAccountValidityDays)') # 現在のユーザープールの設定にLambdaトリガーの設定をマージ input_json=$(echo ${current_config} | jq --arg func_arn ${function_arn} '. * {"LambdaConfig":{"PostConfirmation": $func_arn}}') aws cognito-idp update-user-pool --cli-input-json "${input_json}"
provisioner "local-exec"
ようやくAWS CLIのコマンドからLambdaトリガーの取り付けが行えるようになりましたが、今度はこれをTerraformから実行できるようにしてみたいと思います。
結論から書くと、冒頭の Lambda をデプロイするための Terraform のインフラコードに以下のコードを追加します。
locals { # Cognito ユーザープールID user_pool_id="<ユーザープールID>" # 新規に追加したいLambdaトリガーの定義 additional_cognito_settings = jsonencode({ "LambdaConfig" : { "PreSignUp" : "${resource.aws_lambda_function.cognito_trigger_lambda.arn}" } }) } resource "null_resource" "attach_cognito_lambda_trigger" { provisioner "local-exec" { # 実行したいコマンド command = <<-EOT config_keys=$(aws cognito-idp update-user-pool --generate-cli-skeleton | jq 'keys') current_config=$(aws cognito-idp describe-user-pool --user-pool-id ${local.user_pool_id} | jq --argjson whitelist "$config_keys" '.UserPool | . + {UserPoolId:.Id} | with_entries(select(.key as $a | any($whitelist[]; . == $a))) | del(.AdminCreateUserConfig.UnusedAccountValidityDays)') input_json=$(echo $current_config | jq '. * ${local.additional_cognito_settings}') aws cognito-idp update-user-pool --cli-input-json "$input_json" EOT # Windows環境ではデフォルトでcmd(コマンドプロンプト)でコマンドが実行されてしまうため # 明示的に bash を指定 interpreter=["bash", "-c"] # 作業ディレクトリは本モジュール(このインフラコード)と同じディレクトリに設定 working_dir=path.module } }
インスタンスの存在しないリソースの定義 null_resource
に provisioner を記述してTerraformを実行しているマシン上で任意のコマンドを実行するようにしています。
Provisioners
https://www.terraform.io/language/resources/provisioners/syntax
Provisioners can be used to model specific actions on the local machine or on a remote machine in order to prepare servers or other infrastructure objects for service.
私訳)プロビジョナーを使用すると、ローカルマシンまたはリモートマシンで特定のアクションをモデル化して、サービス用のサーバーまたはその他のインフラストラクチャオブジェクトを準備できます。
local-exec Provisioner
https://www.terraform.io/language/resources/provisioners/local-exec
The local-exec provisioner invokes a local executable after a resource is created. This invokes a process on the machine running Terraform, not on the resource.
私訳)local-exec Provisioner は、リソースの作成後にローカル実行可能ファイルを呼び出します。これはリソース上ではなく、Terraformを実行しているマシン上でプロセスを起動します。
以上で、TerraformからLambdaをデプロイしCognitoのLambdaトリガーとして取り付けるところまでをインフラコードにすることができました。
また、上記の例では記載していませんが、 null_resource
の triggers
を使用することで、コマンドが実行されるタイミングを制御することもできます。
特定のスクリプトファイルが更新されたタイミングでコマンドを実行したい場合は以下のように記述することで実現できるようです。
https://discuss.hashicorp.com/t/re-triggering-shell-script-in-terraform/2988/2
resource "null_resource" "run_script" { triggers = { script_hash = "${sha256(var.bash_script)}" } ... }