forked from lobo/lambda-zig
environment variable support
This commit is contained in:
parent
183d2d912c
commit
ca110ec58d
3 changed files with 233 additions and 6 deletions
48
README.md
48
README.md
|
|
@ -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
|
||||
-----------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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}){};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue