Container Image
Status: Verified | Idempotent: Yes | Coverage: 95%+
Run Command
cargo run --example serverless_container_image
Code
//! # Recipe: Container Image for Lambda
//!
//! Contract: contracts/recipe-iiur-v1.yaml
//! **Category**: Serverless/Lambda
//! **Isolation Level**: Full
//! **Idempotency**: Guaranteed
//! **Dependencies**: None (default features)
//!
//! ## QA Checklist
//! 1. [x] `cargo run` succeeds (Exit Code 0)
//! 2. [x] `cargo test` passes
//! 3. [x] Deterministic output (Verified)
//! 4. [x] No temp files leaked
//! 5. [x] Memory usage stable
//! 6. [x] WASM compatible (N/A)
//! 7. [x] Clippy clean
//! 8. [x] Rustfmt standard
//! 9. [x] No `unwrap()` in logic
//! 10. [x] Proptests pass (100+ cases)
//!
//! ## Learning Objective
//! Package model as container image for Lambda deployment.
//!
//! ## Run Command
//! ```bash
//! cargo run --example serverless_container_image
//! ```
//!
//!
//! ## Format Variants
//! ```bash
//! apr run model.apr # APR native format
//! apr run model.gguf # GGUF (llama.cpp compatible)
//! apr run model.safetensors # SafeTensors (HuggingFace)
//! ```
//! ## References
//! - Schleier-Smith, J. et al. (2021). *What Serverless Computing Is and Should Become*. CACM. DOI: 10.1145/3406011
use apr_cookbook::prelude::*;
use serde::{Deserialize, Serialize};
fn main() -> Result<()> {
let mut ctx = RecipeContext::new("serverless_container_image")?;
println!("=== Recipe: {} ===", ctx.name());
println!("Container image packaging for Lambda");
println!();
// Define container layers
let layers = vec![
ContainerLayer {
name: "base".to_string(),
base_image: "public.ecr.aws/lambda/provided:al2".to_string(),
size_mb: 50,
},
ContainerLayer {
name: "runtime".to_string(),
base_image: String::new(),
size_mb: 20,
},
ContainerLayer {
name: "model".to_string(),
base_image: String::new(),
size_mb: 100,
},
ContainerLayer {
name: "application".to_string(),
base_image: String::new(),
size_mb: 5,
},
];
// Build container image
let mut builder = ContainerBuilder::new("fraud-detector-lambda");
println!("Building container layers:");
for layer in &layers {
builder.add_layer(layer.clone());
println!(" + {} ({}MB)", layer.name, layer.size_mb);
}
println!();
let image = builder.build()?;
ctx.record_metric("total_layers", image.layers.len() as i64);
ctx.record_metric("total_size_mb", i64::from(image.total_size_mb));
println!("Container Image:");
println!(" Name: {}", image.name);
println!(" Tag: {}", image.tag);
println!(" Total size: {}MB", image.total_size_mb);
println!(" Layers: {}", image.layers.len());
println!();
// Generate Dockerfile
let dockerfile = generate_dockerfile(&image);
println!("Generated Dockerfile:");
println!("{:-<50}", "");
for line in dockerfile.lines() {
println!(" {}", line);
}
println!("{:-<50}", "");
// Image optimization analysis
let analysis = analyze_image(&image);
println!();
println!("Optimization Analysis:");
println!(
" Base image overhead: {}MB ({:.1}%)",
analysis.base_overhead_mb, analysis.base_overhead_pct
);
println!(
" Model layer: {}MB ({:.1}%)",
analysis.model_size_mb, analysis.model_pct
);
println!(
" Cold start impact: {}ms (estimated)",
analysis.cold_start_impact_ms
);
ctx.record_float_metric("model_pct", analysis.model_pct);
// Save artifacts
let dockerfile_path = ctx.path("Dockerfile");
std::fs::write(&dockerfile_path, &dockerfile)?;
let config_path = ctx.path("container_config.json");
image.save(&config_path)?;
println!();
println!("Dockerfile saved to: {:?}", dockerfile_path);
println!("Config saved to: {:?}", config_path);
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ContainerLayer {
name: String,
base_image: String,
size_mb: u32,
}
#[derive(Debug, Serialize, Deserialize)]
struct ContainerImage {
name: String,
tag: String,
layers: Vec<ContainerLayer>,
total_size_mb: u32,
}
impl ContainerImage {
fn save(&self, path: &std::path::Path) -> Result<()> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| CookbookError::Serialization(e.to_string()))?;
std::fs::write(path, json)?;
Ok(())
}
}
#[derive(Debug)]
struct ContainerBuilder {
name: String,
layers: Vec<ContainerLayer>,
}
impl ContainerBuilder {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
layers: Vec::new(),
}
}
fn add_layer(&mut self, layer: ContainerLayer) {
self.layers.push(layer);
}
fn build(self) -> Result<ContainerImage> {
let total_size: u32 = self.layers.iter().map(|l| l.size_mb).sum();
Ok(ContainerImage {
name: self.name,
tag: "latest".to_string(),
layers: self.layers,
total_size_mb: total_size,
})
}
}
#[derive(Debug)]
struct ImageAnalysis {
base_overhead_mb: u32,
base_overhead_pct: f64,
model_size_mb: u32,
model_pct: f64,
cold_start_impact_ms: u32,
}
fn generate_dockerfile(image: &ContainerImage) -> String {
let base_layer = image.layers.first();
let base_image = base_layer.map_or("public.ecr.aws/lambda/provided:al2", |l| {
l.base_image.as_str()
});
let mut dockerfile = String::new();
dockerfile.push_str(&format!("FROM {}\n\n", base_image));
dockerfile.push_str("# Runtime dependencies\n");
dockerfile.push_str("COPY bootstrap /var/runtime/\n\n");
dockerfile.push_str("# Model artifacts\n");
dockerfile.push_str("COPY model.apr /opt/model/\n\n");
dockerfile.push_str("# Application binary\n");
dockerfile.push_str("COPY target/release/handler /var/task/\n\n");
dockerfile.push_str("# Set entrypoint\n");
dockerfile.push_str("ENTRYPOINT [\"/var/task/handler\"]\n");
dockerfile
}
fn analyze_image(image: &ContainerImage) -> ImageAnalysis {
let base_size = image.layers.first().map_or(0, |l| l.size_mb);
let model_size = image
.layers
.iter()
.find(|l| l.name == "model")
.map_or(0, |l| l.size_mb);
let total = f64::from(image.total_size_mb);
ImageAnalysis {
base_overhead_mb: base_size,
base_overhead_pct: (f64::from(base_size) / total) * 100.0,
model_size_mb: model_size,
model_pct: (f64::from(model_size) / total) * 100.0,
cold_start_impact_ms: image.total_size_mb * 2, // ~2ms per MB
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_container_builder() {
let mut builder = ContainerBuilder::new("test");
builder.add_layer(ContainerLayer {
name: "base".to_string(),
base_image: "alpine".to_string(),
size_mb: 10,
});
let image = builder.build().unwrap();
assert_eq!(image.name, "test");
assert_eq!(image.layers.len(), 1);
assert_eq!(image.total_size_mb, 10);
}
#[test]
fn test_total_size_calculation() {
let mut builder = ContainerBuilder::new("test");
builder.add_layer(ContainerLayer {
name: "a".to_string(),
base_image: "".to_string(),
size_mb: 10,
});
builder.add_layer(ContainerLayer {
name: "b".to_string(),
base_image: "".to_string(),
size_mb: 20,
});
let image = builder.build().unwrap();
assert_eq!(image.total_size_mb, 30);
}
#[test]
fn test_dockerfile_generation() {
let image = ContainerImage {
name: "test".to_string(),
tag: "latest".to_string(),
layers: vec![ContainerLayer {
name: "base".to_string(),
base_image: "alpine:latest".to_string(),
size_mb: 5,
}],
total_size_mb: 5,
};
let dockerfile = generate_dockerfile(&image);
assert!(dockerfile.contains("FROM alpine:latest"));
assert!(dockerfile.contains("ENTRYPOINT"));
}
#[test]
fn test_image_analysis() {
let image = ContainerImage {
name: "test".to_string(),
tag: "latest".to_string(),
layers: vec![
ContainerLayer {
name: "base".to_string(),
base_image: "".to_string(),
size_mb: 50,
},
ContainerLayer {
name: "model".to_string(),
base_image: "".to_string(),
size_mb: 100,
},
],
total_size_mb: 150,
};
let analysis = analyze_image(&image);
assert_eq!(analysis.base_overhead_mb, 50);
assert_eq!(analysis.model_size_mb, 100);
assert!((analysis.model_pct - 66.67).abs() < 1.0);
}
#[test]
fn test_save_image() {
let ctx = RecipeContext::new("test_container_save").unwrap();
let path = ctx.path("image.json");
let image = ContainerImage {
name: "test".to_string(),
tag: "v1".to_string(),
layers: vec![],
total_size_mb: 0,
};
image.save(&path).unwrap();
assert!(path.exists());
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_total_size_sums_layers(sizes in proptest::collection::vec(1u32..100, 1..10)) {
let mut builder = ContainerBuilder::new("test");
for (i, size) in sizes.iter().enumerate() {
builder.add_layer(ContainerLayer {
name: format!("layer-{}", i),
base_image: "".to_string(),
size_mb: *size,
});
}
let image = builder.build().unwrap();
let expected: u32 = sizes.iter().sum();
prop_assert_eq!(image.total_size_mb, expected);
}
#[test]
fn prop_layer_count_matches(n in 1usize..20) {
let mut builder = ContainerBuilder::new("test");
for i in 0..n {
builder.add_layer(ContainerLayer {
name: format!("layer-{}", i),
base_image: "".to_string(),
size_mb: 10,
});
}
let image = builder.build().unwrap();
prop_assert_eq!(image.layers.len(), n);
}
}
}