TerraformでCloudFront Functionsを環境ごとに有効化/無効化してみた

TerraformにおけるCloudFront Functionsの取り扱い方
2022.06.28

こんにちは、つくぼし(tsukuboshi0755)です!

最近CloudFront Functionsを、Terraformで書いてデプロイしています。

その際に、環境ごとにCloudFront Functionsを自在にON/OFFにしたいと考え、あれこれ調べてみたので共有します。

環境

$ terraform -v

Terraform v1.2.3
on darwin_arm64

きっかけ

とあるシステム構築で、検証環境と本番環境に対して、CloudFront + S3の静的Webサイト構成を、Terraformのmoduleを用いてデプロイしていました。

対象のフォルダ構造は、以下の通りです。(一部簡略化してます)

$ tree
.
├── env
│   ├── prod
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── variable.tf
│   └── test
│       ├── main.tf
│       ├── outputs.tf
│       └── variable.tf
└── modules
    ├── cloudfront
    │   ├── main.tf
    │   ├── outputs.tf
    │   └── variable.tf
    └── s3
        ├── main.tf
        ├── outputs.tf
        └── variable.tf

その内に、検証環境でBasic認証をCloudFront Functionsで実装する必要があり、以下の記事を参考にしながら検討を始めました。

Terraformでの実装

CloudFront Functionsを実装し、ディストリビューションに紐づけるTerraformコードは以下となります。  

terraform/modules/cloudfront/main.tf

resource "aws_cloudfront_function" "example" {
  name    = "example"
  runtime = "cloudfront-js-1.0"
  comment = "example function"
  publish = true 
  code    = file("../../src/basic_auth.js")
}

resource "aws_cloudfront_distribution" "example" {

  default_cache_behavior {
    # ... other configuration ...

    function_association {
      event_type   = "viewer-request"
      function_arn = aws_cloudfront_function.example.arn
    }
  }
}

terraform/src/basic_auth.js

function handler(event) {
  var request = event.request;
  var headers = request.headers;

  // echo -n user:pass | base64
  var authString = "Basic Y2xhc3NtZXRob2Q6MDkxMmNt";

  if (
    typeof headers.authorization === "undefined" ||
    headers.authorization.value !== authString
  ) {
    return {
      statusCode: 401,
      statusDescription: "Unauthorized",
      headers: { "www-authenticate": { value: "Basic" } }
    };
  }

  return request;
}

今回検証環境でしかBasic認証を使わないため、「検証環境ではディストリビューションにFunctionsを紐づけ、本番環境ではディストリビューションにFunctionsを紐付けない」という動作を、同じCloudFront Moduleを用いて実現しようと考えました。

以上の動作を実現するために、当初は以下の順番での実装を計画しました。

aws_cloudfront_functionをFunctions Moduleに分離し、検証環境のみFunctions Moduleを呼び出すように設定する
②CloudFrontのmodule内に存在するfunction_associationについて、検証環境では呼び出し、本番環境では無視するように設定する

問題点の発覚

しかし実装を進めた所、この②のfunction_associationが予想以上に曲者である事が分かりました。

というのも、aws_cloudfront_distributionのように、resource単位であればmoduleとして分離する事が可能です。

しかしfunction_associationaws_cloudfront_distribution内に他の設定と一緒に存在しているため、moduleとして分離する事ができません。

terraform/modules/cloudfront/main.tf

# 以下のresource全体であれば、moduleとして分離可能
resource "aws_cloudfront_distribution" "example" {

  default_cache_behavior {
    # ... other configuration ...

        # 以下のようにresourceの一部分だけの場合、moduleとしては分離不可
    function_association {
      event_type   = "viewer-request"
      function_arn = aws_cloudfront_function.example.arn
    }
  }
}

また、terraformにはif文の代わりに三項演算子があり、単一の変数であれば以下のような形で条件分岐させる事が可能です。

terraform/modules/ec2/main.tf

resource "aws_instance" "example" {
    # ... other configuration ...
  instance_type = "${var.env == "test" ? "t3.large" : "m6i.large"}"
}

しかしfunction_associationについては、対象内にevent_typefunction_arnといった複数の変数が存在するため、三項演算子だけではON/OFFにする事ができません。

Terraform公式でfunction_associationを1つのresourceとして切り出してもらえれば、moduleを分離するだけで済むので悩む必要がないんですけどね。。。

とはいえ何とか今のバージョンのTerraform(1.2.3)で対応できないか検討し、対応方法を見つけました!

対処方法

ずばりDynamic Blockを用いる事で、環境ごとのディストリビューションにFunctionsを紐づけるor紐付けない事を選択できます。

具体的には、以下のようなコードを書けばOKです!

terraform/modules/cloudfront/main.tf

resource "aws_cloudfront_distribution" "example" {

  default_cache_behavior {
    # ... other configuration ...

    dynamic "function_association" {
      for_each = var.environment == "test" ? { sample : "cm" } : {}
      content {
        event_type   = var.cf_event_type
        function_arn = var.cf_function_arn
      }
    }
  }
}

terraform/modules/cloudfront/variable.tf

variable "environment" {}

variable "cf_function_arn" {
  default = null
}

variable "cf_event_type" {
  default = null
}

terraform/modules/functions/main.tf

resource "aws_cloudfront_function" "example" {
  name    = "example"
  runtime = "cloudfront-js-1.0"
  comment = "example function"
  publish = true 
  code    = file("../../src/basic_auth.js")
}

terraform/env/test/main.tf

module "cloudfront" {
  source                = "../../modules/cloudfront"
  environment           = "test"
  # ... other configuration ...
  cf_event_type         = "viewer-request"
  cf_function_arn       = module.functions.cf_function_arn
}

module "functions" {
  source         = "../../modules/functions"
  # ... other configuration ...
}

terraform/env/prod/main.tf

module "cloudfront" {
  source                = "../../modules/cloudfront"
  environment           = "prod"
  # ... other configuration ...
  # cf_event_type         = "viewer-request"
  # cf_function_arn       = module.functions.cf_function_arn
}

# 本番環境ではFunctions Moduleは記載しない

上記を実装する事で、Terraformを適用した際に、検証/本番環境ごとに以下のような動作となります。

検証環境の場合

変数environmentの値がtestで設定されています。

そのためCloudFront ModuleにおけるDynamic Block内のvar.environment == "test"の条件文を満たし、key valueが1組のmapである{ sample : "cm" }が返されます。

for_eachに1組のmapが返される事で、function_associationが1個だけ作成されます。

その際に、content内に存在するevent_type及びfunction_arnを適切に設定する事で、Basic認証を実行するCloudFront Functionsがディストリビューションに紐づけられます。

本番環境の場合

変数environmentの値がprodで設定されています。

そのためCloudFront ModuleにおけるDynamic Block内のvar.environment == "test"の条件文を満たさず、空のmapである{}が返されます。

for_eachに空のmapが返される事で、function_associationは作成されません。

content内に存在するevent_type及びfunction_arnnullで設定されているため、CloudFront Functionsはディストリビューションに紐づけられません。


こんな感じで、環境ごとにCloudFront FunctionsをON/OFFに切り替える事ができます!

ただし、Dynamic Blockを使用しすぎると、コードの可読性が落ちてしまいがちなのでご注意ください。

最後に

TerraformのDynamic Blockって今まであまり使った事がなかったのですが、このような場面で使用できるのは便利ですね。

今回の使い方以外にも、本来Dynamic Blockはresource内のブロックを動的に定義する事ができるので、今後もどんどん使用していきたいと思います!

以上、つくぼしでした!