すきま風

勉強したことのメモとか

Lambdaをどう管理する?

Terraformでインフラコードを管理しているとLambdaの管理は割と悩ましい問題になる。 Terraformで管理したいのはインフラコードであってアプリケーションコードではないのだが、 TerraformでLambdaのresourceを定義するためにはアプリケーションコードが必要になってしまうからだ。

当初考えていた手段は以下の通り


  1. terraform repositoryにlambda zip fileを置く
  2. lambda zip fileをs3で管理する
  3. terraform repositoryでapplication codeも管理してしまう
  4. terraformを諦めてSAM CLIを利用する
  5. etc ...


できるだけterraformでの管理を行いたかったので、当初 02. を検討していたが、2020/08時点でterraformにある既知の不具合のせいで s3上のzip fileに変更がない場合でも必ずapplyの対象になってしまうので都合が悪い。

色々考えた結果、上述の手段を一旦忘れることにして、lambda_functionだけは AWS CLI で管理して その他のresourceをterraformで管理するということに落ち着いた。 ただ、できる限り自動化したかったので、aws cli自体はcodebuild上で行うようにしている。


以下、備忘録としてコードを残しておく (ところどころ省略しています)。cloudwatch logsにlogを出力するだけのlambdaを想定した。

1. lambda用のcloudwatch logs, iamを用意する

resource "aws_cloudwatch_log_group" "this" {
  # 名称は /aws/lambda/function_nameとなること
  name              = "/aws/lambda/${local.lambda_function_name}"
  retention_in_days = 7
}
 
# lambda execution
data "aws_iam_policy_document" "lambda_execution_role_policy" {
  statement {
    effect = "Allow"
    actions = [
      "logs:PutLogEvents",
      "logs:CreateLogStream",
      "logs:DescribeLogStreams",
    ]
 
    resources = [
      "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:${aws_cloudwatch_log_group.this.name}",
      "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:${aws_cloudwatch_log_group.this.name}:log-stream:*",
    ]
  }
}

# 実践Terraformで紹介されているmoduleをそのまま利用している 
module "lambda_execution_role" {
  source     = "./modules/iam-role"
  identifier = "lambda.amazonaws.com"
  name       = "${local.account_name}-lambda-function"
  policy     = data.aws_iam_policy_document.lambda_execution_role_policy.json
}

2. aws cli create function

# console上で実行。Makefileにbuild && zipするようなコードを書いている
$ make zip-service 

# 1.で作成したroleを使ってlambda functionを作成する
$ aws lambda create-function --function-name load --runtime go1.x --zip-file fileb://load.zip --handler load --role arn:aws:iam::999999999999:role/my-lambda-function

3. Lambda permission, alias, notification 等を定義

# s3 eventで発火するようにしている
resource "aws_s3_bucket_notification" "s3_notification" {
  bucket = module.s3_bucket_for_csv.this_s3_bucket_id
 
  lambda_function {
    lambda_function_arn = aws_lambda_alias.lambda_alias.arn
    events              = ["s3:ObjectCreated:*"]
    filter_prefix       = "CSV_FILES/"
    filter_suffix       = ".csv"
  }
 
  depends_on = [aws_lambda_permission.lambda_load_permission]
}
 
resource "aws_lambda_permission" "lambda_load_permission" {
  statement_id  = "AllowExecutionFromS3Bucket"
  action        = "lambda:InvokeFunction"
  function_name = local.lambda_function_name
  principal     = "s3.amazonaws.com"
  qualifier     = aws_lambda_alias.lambda_alias.name
  source_arn    = module.s3_bucket_for_csv.this_s3_bucket_arn
}
 
resource "aws_lambda_alias" "lambda_alias" {
  name             = "load-lambda-alias"
  description      = "alias for load lambda"
  function_name    = local.lambda_function_name
  function_version = "$LATEST" 
  # routing configでversionごとに重み付けをすることも可能
  /**
  routing_config {
    additional_version_weights = {
      "1" = 0
    }
  }
  */
}

4. buildspec.yaml

version: 0.2
 
phases:
  install:
    runtime-versions:
      golang: 1.14
  build:
    commands:
      - echo "`date` build start"
      - echo "change directory | $APPLICATION_DIRECTORY"
      - cd $APPLICATION_DIRECTORY
      - make zip-service
  post_build:
    commands:
      - echo "`date` build finish"
      - echo "publish Lambda"
      - aws lambda update-function-code --function-name load --zip-file fileb://load.zip --publish

5. codeBuild, codePipeline

# iam等、一部省略
resource "aws_codebuild_project" "application" {
  name = "${local.account_name}-codebuild-application"

  service_role = module.codebuild_role.iam_role_arn

  artifacts {
    type = "CODEPIPELINE"
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = local.build_spec_template_path
  }

  cache {
    type = "NO_CACHE"
  }

  logs_config {
    cloudwatch_logs {
      status     = "ENABLED"
      group_name = aws_cloudwatch_log_group.codebuild.name
    }
    s3_logs {
      status = "DISABLED"
    }
  }

  environment {
    # https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-available.html
    image        = "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
    type         = "LINUX_CONTAINER"
    compute_type = "BUILD_GENERAL1_SMALL"
    # 特権を付与する
    privileged_mode = true

    # buildspec.yamlに渡す環境変数
    environment_variable {
      name  = "APPLICATION_DIRECTORY"
      value = "./lambda-function"
    }
  }

  tags = local.tags
}

resource "aws_codepipeline" "this" {
  name = local.codepipeline_name

  role_arn = module.codepipeline_role.iam_role_arn

  artifact_store {
    location = module.s3_bucket_for_codepipeline_artifact.this_s3_bucket_id
    type = "S3"
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "ThirdParty"
      provider         = "GitHub"
      version          = "1"
      run_order        = 1
      output_artifacts = ["Source"]

      configuration = {
        Owner                = "foo"
        Repo                 = "bar"
        Branch               = "master"
        OAuthToken           = "dummy"
        PollForSourceChanges = false
      }
    }
  }

  stage {
    name = "Build"

    action {
      name             = "BuildApplication"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      run_order        = 1
      input_artifacts  = ["Source"]
      output_artifacts = ["BuildApplication"]

      configuration = {
        ProjectName = aws_codebuild_project.application.name
      }
    }
  }

  lifecycle {
    ignore_changes = [
      # GitHubの接続設定はCodePipeline Web GUIで上書きする
      stage[0].action[0].configuration,
    ]
  }
}

カナリアリリースを行いたい場合、別途code deployを記述すれば可能だと思われるが、現時点で業務で利用する予定がないので取り敢えずここまで。