Infrastructure as code

Copy Markdown View Source

Mayfly ships working SAM, Terraform and CDK definitions in templates/ (also in the Hex package under deps/mayfly/templates). Each resolves the public Erlang layer for the deployment region from an embedded map covering all 16 regions × OTP 27/28/29 × both architectures; the map is re-rendered by the weekly layer workflow (layer/render-templates.sh), so the templates in main always carry current ARNs. The quickest way to get them is the generator:

mix lambda.new hello --iac sam                 # or terraform | cdk; --arch x86_64, --otp 28, --region us-east-1
mix lambda.new hello --iac terraform --http-api  # add an API Gateway HTTP API in front of the function

It creates the Mix project (handler with an HTTP branch, release config, .tool-versions pinned to the layer's exact OTP from the catalog, an ExUnit test through Mayfly.LocalRuntime, README) plus the IaC files with the function name, handler, architecture and OTP filled in. All three were deployed, invoked and destroyed from the generated projects on 2026-10-09.

Function URL or API Gateway?

By default every template exposes the function through a Function URL (IAM auth, BUFFERED or RESPONSE_STREAM): no extra service, no cost, streaming supported. --http-api adds an API Gateway HTTP API with a $default route, a JSON access log group and the Lambda permission: use it when you need custom domains, JWT authorizers, usage plans, WAF, or several functions behind one host. The handler code is identical (Mayfly.Events.HTTP decodes both payload shapes); only the IaC changes, and mix lambda.doctor reads whichever you generated. Verified: the SAM variant deployed, GET /hello?name=… and POST /items with a JSON body returned 200 through the API endpoint, access log line present.

AWS SAM (template.yaml, samconfig.toml, Makefile in the project root)

sam build        # BuildMethod: makefile -> MIX_ENV=prod mix release lambda, no Docker
sam deploy --guided   # afterwards: sam deploy
sam delete

AWS::Serverless::Function with provided.al2023, Architectures, LoggingConfig: JSON, Tracing: Active, a Function URL (parameters FunctionUrlAuthType, FunctionUrlInvokeMode), a log group with retention, and Layers chosen via Mappings.MayflyLayers[region][otp<major><arch>] (x86_64 is written x8664: mapping keys must be alphanumeric). Parameters Architecture and OtpMajor select the layer; OtpMajor must match the OTP you build with.

Terraform (infra/main.tf, variables.tf, locals.tf)

MIX_ENV=prod mix release lambda
cd infra && terraform init && terraform apply -auto-approve
terraform destroy -auto-approve

aws_lambda_function + IAM role with AWSLambdaBasicExecutionRole, log group, optional aws_lambda_function_url. locals.tf holds the static layer map (reproducible plans); var.layer_arn overrides it, and the file documents a data "http" variant that resolves the catalog at plan time.

CDK (infra/lib/mayfly-function.ts, bin/app.ts)

MIX_ENV=prod mix release lambda
cd infra && npm install && npx cdk bootstrap && npx cdk deploy --require-approval never
npx cdk destroy --force

MayflyFunction extends lambda.Function: runtime, architecture, handler, layer, JSON logs, tracing and sane memory/timeout defaults; every other FunctionProps passes through. With an explicit stack region the layer ARN is resolved at synth time; environment-agnostic stacks get a CloudFormation Mapping and Fn::FindInMap. MayflyFunction.layerArn(region, arch, otpMajor) is a plain lookup for other constructs.

Minimal hand-written snippets

# SAM
Hello:
  Type: AWS::Serverless::Function
  Properties:
    Runtime: provided.al2023
    Architectures: [arm64]
    Handler: MyApp.Handler
    CodeUri: _build/prod/rel/lambda/lambda.zip
    Layers: [arn:aws:lambda:eu-central-1:651236577491:layer:mayfly-erlang-27-arm64:4]
    LoggingConfig: { LogFormat: JSON }
# Terraform
resource "aws_lambda_function" "hello" {
  function_name    = "hello"
  runtime          = "provided.al2023"
  architectures    = ["arm64"]
  handler          = "MyApp.Handler"
  filename         = "_build/prod/rel/lambda/lambda.zip"
  source_code_hash = filebase64sha256("_build/prod/rel/lambda/lambda.zip")
  layers           = [var.mayfly_layer_arn]
  role             = aws_iam_role.lambda.arn
  logging_config { log_format = "JSON" }
}
// CDK
new lambda.Function(this, "Hello", {
  runtime: lambda.Runtime.PROVIDED_AL2023,
  architecture: lambda.Architecture.ARM_64,
  handler: "MyApp.Handler",
  code: lambda.Code.fromAsset("_build/prod/rel/lambda/lambda.zip"),
  layers: [lambda.LayerVersion.fromLayerVersionArn(this, "Erlang", mayflyLayerArn)],
  loggingFormat: lambda.LoggingFormat.JSON,
});

mix lambda.doctor checks the generated files: the OTP major parameter, the layer ARN the file selects for its architecture, the architecture and the runtime, against your toolchain, so a --otp 28 project built with OTP 27 fails doctor before it fails at boot.

Container images: PackageType: Image / package_type = "Image" / lambda.DockerImageFunction with the URI printed by mix lambda.build --image --push; no runtime, layers or handler setting (see Container image).