Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

TerraformからLambda関数を実行する

Terraformでリソースを作成した後等に独自のスクリプトを実行したくなることがあります。
今回は、Lambda関数の実行を紹介します。

Terraformからスクリプト実行

Terraformからスクリプト実行する方法にはprovisionerブロックExternalプロバイダーがあります。
どちらも、terraformコマンドを実行しているローカル環境で任意のコマンドを実行することが可能です。
(provisionerブロックではSSHやWinRMでリモートホストに接続して実行することも可能なようです)
ただし、これらはTerraform実行環境に依存するため推奨されていません。

AWSプロバイダーによりAWS環境を利用している場合、TerraformからLambda関数を実行することが可能です。
実行したいスクリプトをLambda関数にする場合、たとえば以下のような利点があると考えられます。

  • Terraform実行環境に依存しない(AWS環境に接続できれば良い)
  • 実行権限をIAMでコントロールできる
  • 実行履歴やログをクラウドサービス上に残すことができる
  • VPC内で実行することができる

誰がいつ実行したかはCloudTrailにより記録され、実行時のログはCloudWatch Logsに保存されます。
VPC内で実行できるため、プライベートサブネットのリソースへの接続も可能ですし、その環境の固定IPアドレスから外部APIへ接続することも可能です。
実行用のEC2サーバ等を用意せずにこれらを実現可能です。

TerraformからLambda関数実行

AWSプロバイダーにはリソースのaws_lambda_invocationデータソースのaws_lambda_invocationがあります。
「どのような処理を行うのか?」「どのタイミングで実行したいか?」によってどちらを利用するか選ぶことができます。
リソースの構築に必要な情報を収集するために実行する場合はデータソース、情報や環境を更新したい場合はリソースを利用することになるのではないでしょうか。

なお、どちらで実行しても同期実行されて結果の取得をすることも可能です。

実行する設定

aws_lambda_invocationリソースもaws_lambda_invocationデータソースも、起動するLambda関数等の指定は同じです。

data "aws_lambda_invocation" "test_function" {
  function_name = local.test_function_name
  qualifier     = null # Default "$LATEST"

  input =jsonencode({
    id = random_uuid.test.result
    source = "data"
  })
}

resource "aws_lambda_invocation" "test_function" {
  function_name = local.test_function_name
  qualifier     = null # Default "$LATEST"

  input =jsonencode({
    id = random_uuid.test.result
    source = "resource"
  })
}

上の例にある設定項目3つのうち、function_nameinputが必須です。qualifierはオプションになります。

function_nameで起動するLambda関数の名前を指定します。
qualifierで起動する関数のバージョンを指定することもできます。デフォルトは$LATESTで、最新のものが起動されます。

inputはLambda関数への入力値です。JSON形式の文字列で、そのままLambdaのハンドラーに渡されます。

実行結果の参照

aws_lambda_invocationリソースもaws_lambda_invocationデータソースも、result属性で実行結果(関数のリターン値)を参照可能です。
以下はterraform consoleコマンドで確認した例です。(分かりやすいように改行を追加しています)

> aws_lambda_invocation.test_function.result
"{\"result\": \"OK\", \"event\": \"{\\\"id\\\": \\\"e6048908-82bd-4b2b-ba86-afaab809ead5\\\", \\\"source\\\": \\\"resource\\\"}\"}"

> data.aws_lambda_invocation.test_function.result
"{\"result\": \"OK\", \"event\": \"{\\\"id\\\": \\\"e6048908-82bd-4b2b-ba86-afaab809ead5\\\", \\\"source\\\": \\\"data\\\"}\"}"

> jsondecode(data.aws_lambda_invocation.test_function.result)
{
  "event" = "{\"id\": \"e6048908-82bd-4b2b-ba86-afaab809ead5\", \"source\": \"data\"}"
  "result" = "OK"
}

結果は文字列になっているので、必要に応じてパースしてください。(最後の例ではjsondecode関数でパースしています)

Lambda関数を起動できなかった場合やランタイムエラー等でLambda関数が異常終了したときは、terraformコマンド自体がエラーで終了します。
Lambdaが正常終了しない状態になるとterraform applyができなくなるケースがあるので気を付けてください。

データソースの実行タイミング

aws_lambda_invocationデータソースは、前述した実行の設定以外に固有の設定項目はありません。
データソースが読み込まれるタイミングで実行されるため、そのタイミングはTerraformの仕様によります。

データソースの実行タイミング(読み取りタイミング)はこちらのドキュメントに記載があります。
これによると、使われている値が決まっているときはplanでStateを更新するタイミングで実行されます。
これは-refresh=falseオプションを付けてterraformを実行している場合でも実行されるようです。
しかし、plan時に解決できない動的な値がある場合はapplyのときに実行されます。

前述の例では、入力値にrandom_uuid.test.resultというランダムに生成されたUUIDを利用しています。
3通りのパターンごとに実行されるタイミングを記述します。

パターン plan apply
初回作成時 ×
UUID更新なし ×
UUID更新あり ×

最初に作成するときはrandom_uuid.testが作成されておらず値が不明なため、planの段階では実行されずにapplyでrandom_uuid.testが作成された後に実行されます。

次にそのまま更新する場合(UUID更新なしの場合)はrandom_uuid.testに変更はないため、Terraformはplanの段階からinputの値を確定して実行できます。
この場合はLambda関数はplanの段階で実行されます。

最後のパターンは、random_uuid.testに変更を加えた場合(UUID更新ありの場合)です。
random_uuid.testの更新が完了するまでinputに渡す値が不明な状態になり、planでは実行されずapplyでrandom_uuid.testの更新後に実行されます。

データソースでは場合によって実行されるタイミングが異なりますが、planの段階でもLambda関数が実行されます。
何かを更新するようなLambda関数をデータソースで実行しないようにしましょう。

リソースの実行タイミング

データソースと違い、リソースが実際に作成されたり更新されたりするのはapplyのタイミングのみです。
変更が検知されてapplyしたときにのみ実行されることになります。

データソースのときと同じパターンによる実行タイミングを示すと以下のようになります。

パターン plan apply
初回作成時 ×
UUID更新なし × ×
UUID更新あり ×

ただし、aws_lambda_invocationリソースには前述した実行の設定以外にも設定項目があります。
これにより、実行タイミングをさらに細かく制御可能です。

resource "aws_lambda_invocation" "test_function" {
  function_name = local.test_function_name
  qualifier     = null # Default "$LATEST"

  input =jsonencode({
    id = random_uuid.test.result
    source = "resource"
  })

  lifecycle_scope = "CRUD"
  terraform_key   = "tf" # Default "tf"

  triggers = {
    tr1 = "test2"
  }
}

まずlifecycle_scopeterraform_keyについて説明します。
デフォルトでは、aws_lambda_invocationリソースはリソース作成時のみLambda関数を実行します。
ただし、inputの値が変わった時もリプレイス(replaced)となり、Terraform上でリソースが削除されて作成されるためLambda関数が実行されます。

lifecycle_scope = "CRUD"を設定すると、作成時のみでなく更新(updated in-place)や削除(destroy)でもLambda関数が実行されます。
このとき、inputの値が変わった時の動作もリプレイス(replaced)ではなく更新(updated in-place)になるようです。
Lambda関数には、terraform_keyで指定したキー(デフォルトはtf)で実行されたアクション(createupdatedelete)等の情報が伝えられます。
たとえば、Lambda関数へ実際に伝えられる情報は以下のようになります。

{
    "id": "00e42691-e9ab-a59f-ae2e-64e91d7d11fb",
    "source": "resource",
    "tf": {
        "action": "update",
        "prev_input": {
            "id": "513448cc-98f4-63bf-8c98-f8da6003f016",
            "source": "resource"
        }
    }
}

上記の例では、idsourceがリソースのinputに設定している値で、tfが自動的に追加された項目になります。
ここでは、idが変更されることで更新処理が行われていることが分かります。
なお、リプレイス(replaced)のときはdeletecreateでLambda関数が2回実行されるので注意してください。

次にaws_lambda_invocationリソースのtriggersです。
Map型で値を指定しておき、ここの値に変更があったときはinput等の変更が無くてもLambda関数が実行されます。
lifecycle_scope = "CRUD"の設定があるときもリプレイス(replaced)として検知されていました。
つまり、lifecycle_scope = "CRUD"を指定しているとdeletecreateで2回実行されるようです。

詳細や最新情報は公式のドキュメントを参照してください。

最後に

TerraformからLambda関数を実行する方法について紹介しました。
RDS構築後のユーザ作成やTerraformが対応していないSaaSサービス等に利用できるかと思います。