TerraformにおけるCloud FunctionsとAWS Lambda比較

## はじめに
こんにちは、SREエンジニアをやっています。@hayaosatoです。
今回はサーバレスなインフラを構築する上で、Cloud FunctionsとAWS Lambda(以下、Lambda)についてTerraformにて管理する観点で比較してみようと思います。
構築する構成はそれぞれ以下のようになります。
コードは[こちら](https://github.com/hayaosato/advent-calendar-2019/tree/master/server_less)

### Cloud Function
![terraform_cloud_functions.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/312728/28fa593b-6b19-c613-7253-cac983789ded.jpeg)

### Lambda
![torm_.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/312728/3303ce41-d0d3-37f7-3051-49f1e557e1c8.jpeg)

どちらの構成もバケットにファイルがアップロードされた事をトリガにSlack通知をするアプリケーションが動くようになっています。

## Cloud Functions
まずはCloud Functionを作成していきます。コードは以下の通り

```tf
resource "google_cloudfunctions_function" "default" {
  name        = var.service_name
  description = "Function of ${var.service_name}"
  runtime     = "python37"

  available_memory_mb   = var.available_memory_mb
  source_archive_bucket = google_storage_bucket.source.name
  source_archive_object = google_storage_bucket_object.source.name
  event_trigger {
    event_type = "google.storage.object.finalize"
    resource   = google_storage_bucket.trigger.name
  }
  entry_point = var.entry_point

  environment_variables = {
    SLACK_API_KEY = var.SLACK_API_KEY
  }
}
```
Cloud Storageからのトリガを受け取って起動するためには `event_trigger`を定義する必要があります。
その中で`event_type`にトリガのタイミングを指定します。今回はファイルがアップロードされた時に起動したいので`finalize`を指定しました。

## Cloud Storage
今回作成するCloud Storageは2つです。
一つはCloud Functionのソースコードzipファイルを格納するバケットでもう一つはCloud Functionのトリガのためのバケットです。
それぞれ見ていきましょう。

### ソースコード保存用バケット
ソースコード保存用バケットでは、Terraformにてソースコードのアップまでをやっておきましょう。

```tf
data "archive_file" "source" {
  type        = "zip"
  source_dir  = "src"
  output_path = "${var.service_name}.zip"
}

resource "google_storage_bucket" "source" {
  name     = "${var.service_name}_source"
  location = "${var.region}"
  labels = {
    service = var.service_name
    source  = "true"
  }
}

resource "google_storage_bucket_object" "source" {
  name   = "${var.service_name}.zip"
  bucket = google_storage_bucket.source.name
  source = "${var.service_name}.zip"
}
```
かなりすっきり書けて良い感じですね。
しかし、ここで気をつけなければいけないのがソースコードのアップデートです。
一度 `terraform apply`でCloud Functionを作成した後にCloud Storageのソースコードを更新したところで
ソースコードの更新をCloud Functionに反映することがTerraformでは出来ないです。
(TerraformでCloud Functionソースコード更新出来る方法知っている方いたら教えてください。。。)

### トリガ用バケット
```tf
resource "google_storage_bucket" "trigger" {
  name     = "${var.service_name}_trigger"
  location = "${var.region}"
  labels = {
    service = var.service_name
  }
  force_destroy = true
}
```

Cloud FunctionsではこのようにTerraformで定義することが出来ました。

## Lambda
続いてLambdaです。Lambdaの場合はIAMroleの指定が必須となるので、ロールもTerraformで管理するかどうかで大変さが変わりますが、
今回ではロールもTerraformで管理しようと思います。
また、ログの保存ためにCloud Watch Logも作成します。

### Lambda関数
```tf
resource "aws_iam_role" "default" {
  name               = var.service_name
  description        = "IAM Rolw for ${var.service_name}"
  assume_role_policy = file("policies/${var.service_name}-role.json")
}

resource "aws_iam_policy" "default" {
  name        = var.service_name
  description = "IAM Policy for ${var.service_name}"
  policy      = file("policies/${var.service_name}-policy.json")
}

resource "aws_iam_role_policy_attachment" "default" {
  role       = aws_iam_role.default.name
  policy_arn = aws_iam_policy.default.arn
}

resource "aws_cloudwatch_log_group" "default" {
  name              = "/aws/lambda/${var.service_name}"
  retention_in_days = 7
}

data archive_file "default" {
  type        = "zip"
  source_dir  = "src"
  output_path = "${var.service_name}.zip"
}

resource "aws_lambda_function" "default" {
  filename         = "${var.service_name}.zip"
  function_name    = var.service_name
  role             = aws_iam_role.default.arn
  handler          = "main.main"
  source_code_hash = data.archive_file.default.output_base64sha256
  runtime          = "python3.7"
  environment {
    variables = {
      SLACK_API_KEY = var.SLACK_API_KEY
    }
  }
}
```

AWS Lambdaの場合はIAMやCloudWatchもTerrraformで作成しないといけないので複数リソースを定義する必要がありますね
ただし、ソースコードはLambda自身が持っているのでソースコードの更新は`terraform apply`のみで再作成なしに更新することができます。
(`pip install -t .`をやらなければいけない辛さはありますが、、、)

### トリガ用バケット
```tf
resource "aws_lambda_permission" "default" {
  statement_id  = "AllowExecutionFromS3Bucket"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.default.function_name
  principal     = "s3.amazonaws.com"
  source_arn    = aws_s3_bucket.default.arn
}

resource "aws_s3_bucket" "default" {
  bucket = var.service_name
}

resource "aws_s3_bucket_notification" "default" {
  bucket = aws_s3_bucket.default.id

  lambda_function {
    lambda_function_arn = aws_lambda_function.default.arn
    events              = ["s3:ObjectCreated:*"]
  }
}
```
トリガ用バケットはこのようになります。バケットの作成自体は割とすっきりしているのですが、
定義するリソース数は多いなという印象を受けました。

## まとめ 
Cloud StorageとAWS Lambdaにて同等の機能を持ったアプリケーションを実装してみたのですが、
僕個人の感想としては、
### Cloud Storage
定義するリソースが必要最低限で済むので、わざわざ定義しなくてもStack Driverとかでログが見れたりするので非常に楽で入門しやすい。
ただし、ソースコードのアップデートがterraformからだと現状できなさそうなので、そこがとてもつらい。
### AWS Lambda
基本的に全部定義しなければいけないので、定義がめんどくさいしソースコードが長くなりがちだが、まぁそこまで辛くはなさそう。
ソースコードのアップデート自体は楽だが、(Lambda自体の話ですが)外部ライブラリを使う際の`pip install`がしんどい。

## 参考
- [Terraform Documentation](https://www.terraform.io/docs/index.html)