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
|
* **region**: AWS region for deployment and invocation
|
||||||
* **profile**: AWS profile to use for credentials
|
* **profile**: AWS profile to use for credentials
|
||||||
* **role-name**: IAM role name for the function (default: lambda_basic_execution)
|
* **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
|
The Lambda function can be compiled for x86_64 or aarch64. The build system
|
||||||
automatically configures the Lambda architecture based on the target.
|
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
|
A sample project using this runtime can be found at
|
||||||
https://git.lerch.org/lobo/lambda-zig-sample
|
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
|
Using the Zig Package Manager
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ pub fn configureBuild(
|
||||||
"payload",
|
"payload",
|
||||||
"Lambda invocation payload",
|
"Lambda invocation payload",
|
||||||
) orelse "{}";
|
) orelse "{}";
|
||||||
|
const env_file = b.option(
|
||||||
|
[]const u8,
|
||||||
|
"env-file",
|
||||||
|
"Path to environment variables file (KEY=VALUE format)",
|
||||||
|
) orelse null;
|
||||||
|
|
||||||
// Determine architecture for Lambda
|
// Determine architecture for Lambda
|
||||||
const target_arch = exe.root_module.resolved_target.?.result.cpu.arch;
|
const target_arch = exe.root_module.resolved_target.?.result.cpu.arch;
|
||||||
|
|
@ -88,6 +93,7 @@ pub fn configureBuild(
|
||||||
"--arch",
|
"--arch",
|
||||||
arch_str,
|
arch_str,
|
||||||
});
|
});
|
||||||
|
if (env_file) |ef| deploy_cmd.addArgs(&.{ "--env-file", ef });
|
||||||
deploy_cmd.step.dependOn(&package_cmd.step);
|
deploy_cmd.step.dependOn(&package_cmd.step);
|
||||||
|
|
||||||
const deploy_step = b.step("awslambda_deploy", "Deploy the Lambda function");
|
const deploy_step = b.step("awslambda_deploy", "Deploy the Lambda function");
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
//! Deploy command - deploys a Lambda function to AWS.
|
//! Deploy command - deploys a Lambda function to AWS.
|
||||||
//!
|
//!
|
||||||
//! Creates a new function or updates an existing one.
|
//! Creates a new function or updates an existing one.
|
||||||
|
//! Supports setting environment variables via --env or --env-file.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const aws = @import("aws");
|
const aws = @import("aws");
|
||||||
const iam_cmd = @import("iam.zig");
|
const iam_cmd = @import("iam.zig");
|
||||||
const RunOptions = @import("main.zig").RunOptions;
|
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 {
|
pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
var function_name: ?[]const u8 = null;
|
var function_name: ?[]const u8 = null;
|
||||||
var zip_file: ?[]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 role_name: []const u8 = "lambda_basic_execution";
|
||||||
var arch: ?[]const u8 = null;
|
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;
|
var i: usize = 0;
|
||||||
while (i < args.len) : (i += 1) {
|
while (i < args.len) : (i += 1) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
|
|
@ -37,6 +52,14 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i >= args.len) return error.MissingArch;
|
if (i >= args.len) return error.MissingArch;
|
||||||
arch = args[i];
|
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")) {
|
} else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||||
printHelp(options.stdout);
|
printHelp(options.stdout);
|
||||||
try options.stdout.flush();
|
try options.stdout.flush();
|
||||||
|
|
@ -68,10 +91,81 @@ pub fn run(args: []const []const u8, options: RunOptions) !void {
|
||||||
.role_arn = role_arn,
|
.role_arn = role_arn,
|
||||||
.role_name = role_name,
|
.role_name = role_name,
|
||||||
.arch = arch,
|
.arch = arch,
|
||||||
|
.env_vars = if (env_vars.count() > 0) &env_vars else null,
|
||||||
}, options);
|
}, 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(
|
writer.print(
|
||||||
\\Usage: lambda-build deploy [options]
|
\\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-arn <arn> IAM role ARN (optional - creates role if omitted)
|
||||||
\\ --role-name <name> IAM role name if creating (default: lambda_basic_execution)
|
\\ --role-name <name> IAM role name if creating (default: lambda_basic_execution)
|
||||||
\\ --arch <arch> Architecture: x86_64 or aarch64 (default: x86_64)
|
\\ --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
|
\\ --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
|
\\If the function exists, its code is updated. Otherwise, a new function
|
||||||
\\is created with the provided configuration.
|
\\is created with the provided configuration.
|
||||||
\\
|
\\
|
||||||
|
|
@ -97,6 +202,7 @@ const DeployOptions = struct {
|
||||||
role_arn: ?[]const u8,
|
role_arn: ?[]const u8,
|
||||||
role_name: []const u8,
|
role_name: []const u8,
|
||||||
arch: ?[]const u8,
|
arch: ?[]const u8,
|
||||||
|
env_vars: ?*const std.StringHashMap([]const u8),
|
||||||
};
|
};
|
||||||
|
|
||||||
fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
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
|
// 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"
|
"arm64"
|
||||||
else
|
else
|
||||||
"x86_64";
|
"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
|
// 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});
|
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(.{
|
const create_result = aws.Request(services.lambda.create_function).call(.{
|
||||||
.function_name = deploy_opts.function_name,
|
.function_name = deploy_opts.function_name,
|
||||||
.architectures = @constCast(architectures),
|
.architectures = architectures,
|
||||||
.code = .{ .zip_file = base64_data },
|
.code = .{ .zip_file = base64_data },
|
||||||
.handler = "bootstrap",
|
.handler = "bootstrap",
|
||||||
.package_type = "Zip",
|
.package_type = "Zip",
|
||||||
.runtime = "provided.al2023",
|
.runtime = "provided.al2023",
|
||||||
.role = role_arn,
|
.role = role_arn,
|
||||||
|
.environment = if (env_variables) |vars| .{ .variables = vars } else null,
|
||||||
}, create_options) catch |err| {
|
}, create_options) catch |err| {
|
||||||
defer create_diagnostics.deinit();
|
defer create_diagnostics.deinit();
|
||||||
std.log.info("CreateFunction returned: error={}, HTTP code={}", .{ err, create_diagnostics.http_code });
|
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(.{
|
const update_result = try aws.Request(services.lambda.update_function_code).call(.{
|
||||||
.function_name = deploy_opts.function_name,
|
.function_name = deploy_opts.function_name,
|
||||||
.architectures = @constCast(architectures),
|
.architectures = architectures,
|
||||||
.zip_file = base64_data,
|
.zip_file = base64_data,
|
||||||
}, aws_options);
|
}, aws_options);
|
||||||
defer update_result.deinit();
|
defer update_result.deinit();
|
||||||
|
|
@ -192,8 +311,14 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||||
}
|
}
|
||||||
try options.stdout.flush();
|
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);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -212,6 +337,54 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void {
|
||||||
try waitForFunctionReady(deploy_opts.function_name, aws_options);
|
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 {
|
fn waitForFunctionReady(function_name: []const u8, aws_options: aws.Options) !void {
|
||||||
const services = aws.Services(.{.lambda}){};
|
const services = aws.Services(.{.lambda}){};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue