environment variable support
All checks were successful
Lambda-Zig Build / build (push) Successful in 35s

This commit is contained in:
Emil Lerch 2026-02-02 14:24:25 -08:00
parent 183d2d912c
commit ca110ec58d
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 233 additions and 6 deletions

View file

@ -18,6 +18,7 @@ Build options:
* **region**: AWS region for deployment and invocation
* **profile**: AWS profile to use for credentials
* **role-name**: IAM role name for the function (default: lambda_basic_execution)
* **env-file**: Path to environment variables file for the Lambda function
The Lambda function can be compiled for x86_64 or aarch64. The build system
automatically configures the Lambda architecture based on the target.
@ -25,6 +26,53 @@ automatically configures the Lambda architecture based on the target.
A sample project using this runtime can be found at
https://git.lerch.org/lobo/lambda-zig-sample
Environment Variables
---------------------
Lambda functions can be configured with environment variables during deployment.
This is useful for passing configuration, secrets, or credentials to your function.
### Using the build system
Pass the `-Denv-file` option to specify a file containing environment variables:
```sh
zig build awslambda_deploy -Dfunction-name=my-function -Denv-file=.env
```
### Using the CLI directly
The `lambda-build` CLI supports both `--env` flags and `--env-file`:
```sh
# Set individual variables
./lambda-build deploy --function-name my-fn --zip-file function.zip \
--env DB_HOST=localhost --env DB_PORT=5432
# Load from file
./lambda-build deploy --function-name my-fn --zip-file function.zip \
--env-file .env
# Combine both (--env values override --env-file)
./lambda-build deploy --function-name my-fn --zip-file function.zip \
--env-file .env --env DEBUG=true
```
### Environment file format
The environment file uses a simple `KEY=VALUE` format, one variable per line:
```sh
# Database configuration
DB_HOST=localhost
DB_PORT=5432
# API keys
API_KEY=secret123
```
Lines starting with `#` are treated as comments. Empty lines are ignored.
Using the Zig Package Manager
-----------------------------

View file

@ -34,6 +34,11 @@ pub fn configureBuild(
"payload",
"Lambda invocation payload",
) orelse "{}";
const env_file = b.option(
[]const u8,
"env-file",
"Path to environment variables file (KEY=VALUE format)",
) orelse null;
// Determine architecture for Lambda
const target_arch = exe.root_module.resolved_target.?.result.cpu.arch;
@ -88,6 +93,7 @@ pub fn configureBuild(
"--arch",
arch_str,
});
if (env_file) |ef| deploy_cmd.addArgs(&.{ "--env-file", ef });
deploy_cmd.step.dependOn(&package_cmd.step);
const deploy_step = b.step("awslambda_deploy", "Deploy the Lambda function");

View file

@ -1,12 +1,16 @@
//! Deploy command - deploys a Lambda function to AWS.
//!
//! Creates a new function or updates an existing one.
//! Supports setting environment variables via --env or --env-file.
const std = @import("std");
const aws = @import("aws");
const iam_cmd = @import("iam.zig");
const RunOptions = @import("main.zig").RunOptions;
// Get Lambda EnvironmentVariableKeyValue type from AWS SDK
const EnvVar = aws.services.lambda.EnvironmentVariableKeyValue;
pub fn run(args: []const []const u8, options: RunOptions) !void {
var function_name: ?[]const u8 = null;
var zip_file: ?[]const u8 = null;
@ -14,6 +18,17 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
var role_name: []const u8 = "lambda_basic_execution";
var arch: ?[]const u8 = null;
// Environment variables storage
var env_vars = std.StringHashMap([]const u8).init(options.allocator);
defer {
var it = env_vars.iterator();
while (it.next()) |entry| {
options.allocator.free(entry.key_ptr.*);
options.allocator.free(entry.value_ptr.*);
}
env_vars.deinit();
}
var i: usize = 0;
while (i < args.len) : (i += 1) {
const arg = args[i];
@ -37,6 +52,14 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
i += 1;
if (i >= args.len) return error.MissingArch;
arch = args[i];
} else if (std.mem.eql(u8, arg, "--env")) {
i += 1;
if (i >= args.len) return error.MissingEnvValue;
try parseEnvVar(args[i], &env_vars, options.allocator);
} else if (std.mem.eql(u8, arg, "--env-file")) {
i += 1;
if (i >= args.len) return error.MissingEnvFile;
try loadEnvFile(args[i], &env_vars, options.allocator);
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
printHelp(options.stdout);
try options.stdout.flush();
@ -68,10 +91,81 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
.role_arn = role_arn,
.role_name = role_name,
.arch = arch,
.env_vars = if (env_vars.count() > 0) &env_vars else null,
}, options);
}
fn printHelp(writer: *std.Io.Writer) void {
/// Parse a KEY=VALUE string and add to the env vars map
fn parseEnvVar(
env_str: []const u8,
env_vars: *std.StringHashMap([]const u8),
allocator: std.mem.Allocator,
) !void {
const eq_pos = std.mem.indexOf(u8, env_str, "=") orelse {
return error.InvalidEnvFormat;
};
const key = try allocator.dupe(u8, env_str[0..eq_pos]);
errdefer allocator.free(key);
const value = try allocator.dupe(u8, env_str[eq_pos + 1 ..]);
errdefer allocator.free(value);
// If key already exists, free the old value
if (env_vars.fetchRemove(key)) |old| {
allocator.free(old.key);
allocator.free(old.value);
}
try env_vars.put(key, value);
}
/// Load environment variables from a file (KEY=VALUE format, one per line)
fn loadEnvFile(
path: []const u8,
env_vars: *std.StringHashMap([]const u8),
allocator: std.mem.Allocator,
) !void {
const file = std.fs.cwd().openFile(path, .{}) catch |err| {
std.log.err("Failed to open env file '{s}': {}", .{ path, err });
return error.EnvFileNotFound;
};
defer file.close();
// Read entire file (env files are typically small)
var read_buffer: [4096]u8 = undefined;
var file_reader = file.reader(&read_buffer);
const content = file_reader.interface.allocRemaining(allocator, std.Io.Limit.limited(64 * 1024)) catch |err| {
std.log.err("Error reading env file: {}", .{err});
return error.EnvFileReadError;
};
defer allocator.free(content);
// Parse line by line
var line_start: usize = 0;
for (content, 0..) |c, idx| {
if (c == '\n') {
const line = content[line_start..idx];
line_start = idx + 1;
// Skip empty lines and comments
const trimmed = std.mem.trim(u8, line, " \t\r");
if (trimmed.len == 0 or trimmed[0] == '#') continue;
try parseEnvVar(trimmed, env_vars, allocator);
}
}
// Handle last line if no trailing newline
if (line_start < content.len) {
const line = content[line_start..];
const trimmed = std.mem.trim(u8, line, " \t\r");
if (trimmed.len > 0 and trimmed[0] != '#') {
try parseEnvVar(trimmed, env_vars, allocator);
}
}
}
fn printHelp(writer: anytype) void {
writer.print(
\\Usage: lambda-build deploy [options]
\\
@ -83,8 +177,19 @@ fn printHelp(writer: *std.Io.Writer) void {
\\ --role-arn <arn> IAM role ARN (optional - creates role if omitted)
\\ --role-name <name> IAM role name if creating (default: lambda_basic_execution)
\\ --arch <arch> Architecture: x86_64 or aarch64 (default: x86_64)
\\ --env <KEY=VALUE> Set environment variable (can be repeated)
\\ --env-file <path> Load environment variables from file (KEY=VALUE format)
\\ --help, -h Show this help message
\\
\\Environment File Format:
\\ The --env-file option reads a file with KEY=VALUE pairs, one per line.
\\ Lines starting with # are treated as comments. Empty lines are ignored.
\\
\\ Example .env file:
\\ # Database configuration
\\ DB_HOST=localhost
\\ DB_PORT=5432
\\
\\If the function exists, its code is updated. Otherwise, a new function
\\is created with the provided configuration.
\\
@ -97,6 +202,7 @@ const DeployOptions = struct {
role_arn: ?[]const u8,
role_name: []const u8,
arch: ?[]const u8,
env_vars: ?*const std.StringHashMap([]const u8),
};
fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
@ -141,12 +247,24 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
};
// Convert arch string to Lambda format
const lambda_arch = if (std.mem.eql(u8, arch_str, "aarch64") or std.mem.eql(u8, arch_str, "arm64"))
const lambda_arch: []const u8 = if (std.mem.eql(u8, arch_str, "aarch64") or std.mem.eql(u8, arch_str, "arm64"))
"arm64"
else
"x86_64";
const architectures: []const []const u8 = &.{lambda_arch};
// Use a mutable array so the slice type is [][]const u8, not []const []const u8
var architectures_arr = [_][]const u8{lambda_arch};
const architectures: [][]const u8 = &architectures_arr;
// Build environment variables for AWS API
const env_variables = try buildEnvVariables(deploy_opts.env_vars, options.allocator);
defer if (env_variables) |vars| {
for (vars) |v| {
options.allocator.free(v.key);
if (v.value) |val| options.allocator.free(val);
}
options.allocator.free(vars);
};
// Try to create the function first - if it already exists, we'll update it
std.log.info("Attempting to create function: {s}", .{deploy_opts.function_name});
@ -165,12 +283,13 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
const create_result = aws.Request(services.lambda.create_function).call(.{
.function_name = deploy_opts.function_name,
.architectures = @constCast(architectures),
.architectures = architectures,
.code = .{ .zip_file = base64_data },
.handler = "bootstrap",
.package_type = "Zip",
.runtime = "provided.al2023",
.role = role_arn,
.environment = if (env_variables) |vars| .{ .variables = vars } else null,
}, create_options) catch |err| {
defer create_diagnostics.deinit();
std.log.info("CreateFunction returned: error={}, HTTP code={}", .{ err, create_diagnostics.http_code });
@ -181,7 +300,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
const update_result = try aws.Request(services.lambda.update_function_code).call(.{
.function_name = deploy_opts.function_name,
.architectures = @constCast(architectures),
.architectures = architectures,
.zip_file = base64_data,
}, aws_options);
defer update_result.deinit();
@ -192,8 +311,14 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
}
try options.stdout.flush();
// Wait for function to be ready before returning
// Wait for function to be ready before updating configuration
try waitForFunctionReady(deploy_opts.function_name, aws_options);
// Update environment variables if provided
if (env_variables) |vars| {
try updateFunctionConfiguration(deploy_opts.function_name, vars, aws_options, options);
}
return;
}
@ -212,6 +337,54 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
try waitForFunctionReady(deploy_opts.function_name, aws_options);
}
/// Build environment variables in the format expected by AWS Lambda API
fn buildEnvVariables(
env_vars: ?*const std.StringHashMap([]const u8),
allocator: std.mem.Allocator,
) !?[]EnvVar {
const vars = env_vars orelse return null;
if (vars.count() == 0) return null;
var result = try allocator.alloc(EnvVar, vars.count());
errdefer allocator.free(result);
var idx: usize = 0;
var it = vars.iterator();
while (it.next()) |entry| {
result[idx] = .{
.key = try allocator.dupe(u8, entry.key_ptr.*),
.value = try allocator.dupe(u8, entry.value_ptr.*),
};
idx += 1;
}
return result;
}
/// Update function configuration (environment variables)
fn updateFunctionConfiguration(
function_name: []const u8,
env_variables: []EnvVar,
aws_options: aws.Options,
options: RunOptions,
) !void {
const services = aws.Services(.{.lambda}){};
std.log.info("Updating function configuration for: {s}", .{function_name});
const update_config_result = try aws.Request(services.lambda.update_function_configuration).call(.{
.function_name = function_name,
.environment = .{ .variables = env_variables },
}, aws_options);
defer update_config_result.deinit();
try options.stdout.print("Updated environment variables\n", .{});
try options.stdout.flush();
// Wait for configuration update to complete
try waitForFunctionReady(function_name, aws_options);
}
fn waitForFunctionReady(function_name: []const u8, aws_options: aws.Options) !void {
const services = aws.Services(.{.lambda}){};