Terraform: створення Lambda-функцій у VPC

Автор |  26/09/2023
 

В пості Loki: збір логів з CloudWatch Logs з використанням Lambda Promtail описано як можна збирати логи з CloudWatch Logs за допомогою Lambda-функції з Promtail, який пересилає логи в Grafana Loki.

Що треба зробити зараз – це описати створення чотирьох таких функцій, по одній на кожен компонент проекту. Функції мають бути розміщені в приватних мережах VPC, щоб мати доступ до Ingress Loki у вигляді Internal Load Balancer.

Підготовка

Використаємо “flat-layout” – всі файли Terraform будуть в корні проекту, а значення змінних для Dev та Prod передамо через окремі файли tfvars, див. Terraform Dev/Prod – Helm-like “flat” approach.

У файлі providers.tf описуємо провайдер AWS:

provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      component   = var.component
      created-by  = "terraform"
      environment = var.environment
    }
  }
}

У файлі backend.tf – бекенд для стейт-файлу в S3:

terraform {
  backend "s3" {
    bucket         = "tf-state-backend-atlas-monitoring"
    region         = "us-east-1"
    encrypt        = true
  }
}

Значення key та dynamodb_table передамо під час виконання terraform init, бо для Dev і Prod вони будуть різними.

Додаємо файл variables.tf з поки що двома змінними:

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

variable "project_name" {
  description = "A project name to be used in resources"
  type        = string
  default     = "atlas-lambda"
}

variable "component" {
  description = "A team using this project (backend, web, ios, data, devops)"
  type        = string
}

variable "environment" {
  description = "Dev/Prod, will be used in AWS resources Name tag, and resources names"
  type        = string
}

variable "eks_version" {
  description = "Kubernetes version, will be used in AWS resources names and to specify which EKS version to create/update"
  type        = string
}

Значенням можно передати в defaults, але мені більш подобається задавати значення явно, а не в дефолтах, тому додаємо файл envs/dev/dev.tfvars:

aws_region   = "us-east-1"
environment  = "dev"
component    = "devops"
eks_version  = "1.27"

eks_version використовуємо, щоб створювати окремі ресурси під кожну версію EKS-кластеру, бо під час оновлення версій спокійніше буде створити новий кластер і мігрувати workloads, ніж оновлювати живий Production.

Додаємо файл versions.tf з версіями Terraform та провайдерів:

terraform {

  required_version = "~> 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.14"
    }
  }
}

Для аутентификації та авторизації в AWS маємо AWS CLI Profile, в якому виконується AssumeRole:

[profile work]
region = us-east-1
output = json

[profile tf-admin]
role_arn = arn:aws:iam::492***148:role/tf-admin
source_profile = work

Для Terraform – задаємо змінну AWS_PROFILE та виконємо terraform init з -backend-config:

$ terraform init -backend-config="key=test/atlas-monitoring-test.tfstate" -backend-config="dynamodb_table=tf-state-lock-atlas-monitoring-test"

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.14"...
- Installing hashicorp/aws v5.17.0...
- Installed hashicorp/aws v5.17.0 (signed by HashiCorp)

...

Terraform has been successfully initialized!

Створення Lambda-функції

Використовуємо модуль, знов від @Anton Babenkoterraform-aws-modules/lambda/aws.

Для запуску функції нам потрібно мати Docker-образ з Promtail – він вже є, зберігається у ECR-репозиторії.

Крім того нам потрібно мати параметри для VPC – їх отримаємо із outputs іншого Terraform-проекту, який у нас займається управлінням мережами, аналогічно тому, як робили в Terraform: terraform_remote_state – отримання outputs інших state-файлів.

І третє, що треба буде мати – це Security-група, яка дозволить трафік від та до цих Lambd у приватних Subnets нашої VPC. Для її створення візьмемо ще один модуль Антона – terraform-aws-modules/security-group/aws.

SecurityGroup та remote_state

Готуємо файл main.tf, описуємо terraform_remote_state:

data "terraform_remote_state" "vpc" {
  backend = "s3"
  config = {
    bucket         = "tf-state-backend-atlas-vpc"
    key            = "${var.environment}/atlas-vpc-${var.environment}.tfstate"
    region         = var.aws_region
    dynamodb_table = "tf-state-lock-atlas-vpc-${var.environment}"
  }
}

Додаємо локальні змінні:

locals {
  # create a name like 'atlas-monitorig-dev-1-27'
  env_name = "test-${var.project_name}-${var.environment}-${replace(var.eks_version, ".", "-")}"
  # 1.27 => 1-27
  env_version = replace(var.eks_version, ".", "-")
  # save 'outputs' from the VPC project
  vpc_out = data.terraform_remote_state.vpc.outputs
}

В outputs проекту з VPC маємо всі необхідні дані:

...
output "vpc_id" {
  value = module.vpc.vpc_id
}
...
output "vpc_private_subnets_cidrs" {
  value = module.vpc.private_subnets_cidr_blocks
}
...

І vpc_private_subnets_cidrs видається у формі list(string):

Описуємо SecurityGroup:

module "security_group_lambda" {
  source  = "terraform-aws-modules/security-group/aws"
  version = "~> 4.0"

  name        = "${local.env_name}-loki-logger-lambda-sg"
  description = "Security Group for Lambda Egress"

  vpc_id = local.vpc_out.vpc_id

  egress_cidr_blocks      = local.vpc_out.vpc_private_subnets_cidrs
  egress_ipv6_cidr_blocks = []

  ingress_cidr_blocks      = local.vpc_out.vpc_private_subnets_cidrs
  ingress_ipv6_cidr_blocks = []

  egress_rules  = ["https-443-tcp"]
  ingress_rules = ["https-443-tcp"]
}

В egress_cidr_blocks та ingress_cidr_blocks вносимо адреси приватних мереж, а в egress_rules та ingress_rules – значення з auto_groups, де вже маємо готовий набор правил.

Ще раз виконуємо terraform init, щоб додати модуль SecurityGroup, та деплоїмо:

$ terraform apply -var-file=envs/dev/dev.tfvars
...
module.security_group_lambda.aws_security_group.this_name_prefix[0]: Creating...
module.security_group_lambda.aws_security_group.this_name_prefix[0]: Creation complete after 3s [id=sg-006d09a7a0ff0beb5]
module.security_group_lambda.aws_security_group_rule.ingress_rules[0]: Creating...
module.security_group_lambda.aws_security_group_rule.egress_rules[0]: Creating...
module.security_group_lambda.aws_security_group_rule.ingress_rules[0]: Creation complete after 1s [id=sgrule-1358616028]
module.security_group_lambda.aws_security_group_rule.egress_rules[0]: Creation complete after 2s [id=sgrule-892435573]
Releasing state lock. This may take a few moments...

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Перевіряємо:

Lambda

Приклад є у файлі examples/with-vpc/main.tf.

До variables.tf додаємо ще дві змінних – список Components, та URL образу з Promtail:

...

variable "promtail_lambdas" {
  type = set(string)
}

variable "promtail_image" {
  type    = string
  default = "492***148.dkr.ecr.us-east-1.amazonaws.com/lambda-promtail:latest"
}

І значення promtail_lambdas в dev.tfvars:

...

promtail_lambdas = [
  "backend",
  "web",
  "ios",
  "eks"
]

До locals додаємо змінну з URL інстансу Grafana Loki:

...
locals {
  ...
  # save 'outputs' from the VPC project
  vpc_out = data.terraform_remote_state.vpc.outputs
  # build URL 'logger.1-27.dev.example.co'
  loki_write_address = "https://logger.${replace(var.eks_version, ".", "-")}.${var.environment}.example.co:443/loki/api/v1/push"
}
...

Додаємо модуль terraform-aws-modules/lambda/aws до нашого main.tf:

...

module "lambda_function_from_container_image" {
  source   = "terraform-aws-modules/lambda/aws"
  version  = "~> 6.0"
  for_each = var.promtail_lambdas

  function_name = "grafana-${local.env_name}-loki-logger-${each.value}"
  description   = "Promtail instance to colelct logs from CloudWatch Logs"

  create_package = false

  image_uri     = var.promtail_image
  package_type  = "Image"
  architectures = ["x86_64"]

  environment_variables = {
    EXTRA_LABELS             = "component,${each.value}"
    KEEP_STREAM              = "true"
    OMIT_EXTRA_LABELS_PREFIX = "true"
    PRINT_LOG_LINE           = "true"
    WRITE_ADDRESS            = local.loki_write_address
  }

  vpc_subnet_ids                     = local.vpc_out.vpc_private_subnets_ids
  vpc_security_group_ids             = [module.security_group_lambda.security_group_id]
  attach_network_policy              = true
}

Тут в циклі for_each перебираємо всі Components, для яких треба створити фукцію, в package_type вказуємо, що функція буде запускатись з Docker-образу, в environment_variables.EXTRA_LABELS задаємо лейблу component, яка буде додана до логів в Loki.

У vpc_subnet_ids знов використовуємо outputs проекту VPC, в vpc_security_group_ids вказуємо SecurityGroup, яку створили вище.

Параметр attach_network_policy підключить до IAM Role, яка буде підключена до функції, політику AWSLambdaENIManagementAccess, див. terraform-aws-lambda/blob/master/iam.tf.

Виконуємо terraform init, деплоїмо та перевіряємо:

Тестування логів

Додамо Subcription Filter до лог-групи кластеру EKS, щоб перевірити, що функція працює:

Метрики функції – дивимось Invocations та Error count and success rate (%):

І логи в Loki:

Готово.