lambda-microvms 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lambda
4
+ module MicroVMs
5
+ # Represents one running, suspended, or terminated Lambda MicroVM session.
6
+ class MicroVM
7
+ attr_reader :client, :id, :data
8
+
9
+ def self.from_response(client:, response:)
10
+ id = Util.extract(response, :microvm_id, :microvm_arn, :id, :arn)
11
+ new(client: client, id: id, data: response)
12
+ end
13
+
14
+ def initialize(client:, id:, data: nil)
15
+ @client = client
16
+ @id = id
17
+ @data = data
18
+ end
19
+
20
+ def refresh
21
+ fresh = client.get_microvm(microvm_id: id)
22
+ @data = fresh.data
23
+ self
24
+ end
25
+
26
+ def state
27
+ Util.normalize_state(Util.extract(@data, :state, :status, :microvm_state))
28
+ end
29
+
30
+ def running?
31
+ state == :running
32
+ end
33
+
34
+ def suspended?
35
+ state == :suspended
36
+ end
37
+
38
+ def terminated?
39
+ state == :terminated
40
+ end
41
+
42
+ def pending?
43
+ state == :pending
44
+ end
45
+
46
+ def endpoint_url
47
+ endpoint = Util.extract(@data, :endpoint, :endpoint_url, :url)
48
+ return endpoint if endpoint.is_a?(String)
49
+
50
+ Util.extract(endpoint, :url, :endpoint_url) if endpoint
51
+ end
52
+
53
+ def auth_token(ports: nil, ttl_seconds: nil, **params)
54
+ request = params.merge(microvm_id: id)
55
+ request[:ports] = ports if ports
56
+ request[:ttl_seconds] = ttl_seconds if ttl_seconds
57
+ response = client.create_auth_token(**request)
58
+ Util.extract(response, :token, :auth_token, :microvm_auth_token)
59
+ end
60
+
61
+ def endpoint(token: nil, **token_params)
62
+ Endpoint.new(url: endpoint_url, token: token || auth_token(**token_params))
63
+ end
64
+
65
+ def get(path, token: nil, **)
66
+ endpoint(token: token).get(path, **)
67
+ end
68
+
69
+ def post(path, json: nil, body: nil, token: nil, **)
70
+ endpoint(token: token).post(path, json: json, body: body, **)
71
+ end
72
+
73
+ def suspend(**params)
74
+ client.suspend_microvm(**params, microvm_id: id)
75
+ self
76
+ end
77
+
78
+ def resume(**params)
79
+ response = client.resume_microvm(**params, microvm_id: id)
80
+ @data = response if response
81
+ self
82
+ end
83
+
84
+ def terminate(**params)
85
+ client.terminate_microvm(**params, microvm_id: id)
86
+ self
87
+ end
88
+
89
+ def wait_until_running(delay: Waiter::DEFAULT_DELAY, timeout: Waiter::DEFAULT_TIMEOUT)
90
+ Waiter.new(delay: delay, timeout: timeout).wait(message: "MicroVM #{id} to be running") do
91
+ refresh.running?
92
+ end
93
+ self
94
+ end
95
+
96
+ def wait_until_suspended(delay: Waiter::DEFAULT_DELAY, timeout: Waiter::DEFAULT_TIMEOUT)
97
+ Waiter.new(delay: delay, timeout: timeout).wait(message: "MicroVM #{id} to be suspended") do
98
+ refresh.suspended?
99
+ end
100
+ self
101
+ end
102
+
103
+ def wait_until_terminated(delay: Waiter::DEFAULT_DELAY, timeout: Waiter::DEFAULT_TIMEOUT)
104
+ Waiter.new(delay: delay, timeout: timeout).wait(message: "MicroVM #{id} to be terminated") do
105
+ refresh.terminated?
106
+ end
107
+ self
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'open3'
5
+ require 'pathname'
6
+
7
+ module Lambda
8
+ module MicroVMs
9
+ # Creates a deployable source artifact for Lambda MicroVM image creation.
10
+ class Packager
11
+ DEFAULT_EXCLUDES = ['.git/*', 'tmp/*', 'vendor/bundle/*', '*.gem'].freeze
12
+
13
+ attr_reader :project
14
+
15
+ def initialize(project)
16
+ @project = project
17
+ end
18
+
19
+ def package(output: project.artifact_path)
20
+ FileUtils.mkdir_p(File.dirname(output))
21
+ relative_output = begin
22
+ Pathname.new(output).relative_path_from(Pathname.new(project.root)).to_s
23
+ rescue StandardError
24
+ output
25
+ end
26
+ excludes = DEFAULT_EXCLUDES + [relative_output]
27
+ command = ['zip', '-q', '-r', output, '.'] + excludes.flat_map { |pattern| ['-x', pattern] }
28
+ stdout, stderr, status = Open3.capture3(*command, chdir: project.root)
29
+ raise CommandError, "zip failed: #{stderr.empty? ? stdout : stderr}" unless status.success?
30
+
31
+ output
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+
7
+ module Lambda
8
+ module MicroVMs
9
+ # Reads lambda-microvms project configuration from microvm.yml.
10
+ class Project
11
+ DEFAULT_CONFIG = 'microvm.yml'
12
+
13
+ attr_reader :root, :config_path, :config
14
+
15
+ def self.load(path = DEFAULT_CONFIG)
16
+ config_path = File.expand_path(path)
17
+ root = File.dirname(config_path)
18
+ new(root: root, config_path: config_path,
19
+ config: YAML.safe_load_file(config_path, permitted_classes: [Symbol], aliases: true) || {})
20
+ end
21
+
22
+ def initialize(root:, config_path:, config:)
23
+ @root = root
24
+ @config_path = config_path
25
+ @config = stringify_keys(config)
26
+ end
27
+
28
+ def name = fetch('name', default: File.basename(root))
29
+
30
+ def region = fetch('region', default: ENV.fetch('AWS_REGION', nil))
31
+
32
+ def profile = fetch('profile', default: ENV.fetch('AWS_PROFILE', nil))
33
+
34
+ def role_arn = fetch('role_arn', 'role', default: nil)
35
+
36
+ def image_arn = fetch('image_arn', 'image.arn', default: nil)
37
+
38
+ def image_name = fetch('image.name', default: name)
39
+
40
+ def dockerfile = File.expand_path(fetch('build.dockerfile', default: 'Dockerfile'), root)
41
+
42
+ def build_context = File.expand_path(fetch('build.context', default: '.'), root)
43
+
44
+ def artifact_path = File.expand_path(fetch('build.artifact', default: "tmp/#{name}-microvm.zip"), root)
45
+
46
+ def s3_bucket = fetch('deployment.bucket', 's3.bucket', default: nil)
47
+
48
+ def s3_prefix = fetch('deployment.prefix', 's3.prefix', default: "lambda-microvms/#{name}")
49
+
50
+ def lifecycle_after = fetch('runtime.after', default: 'suspend').to_sym
51
+
52
+ def payload
53
+ fetch('runtime.payload', default: {}) || {}
54
+ end
55
+
56
+ def create_image_params(artifact_uri:)
57
+ params = fetch('image.create', default: {}) || {}
58
+ params = stringify_keys(params)
59
+ symbolized = params.to_h { |key, value| [key.to_sym, value] }
60
+ symbolized[:name] ||= image_name
61
+ symbolized[:code_artifact] ||= artifact_uri
62
+ symbolized
63
+ end
64
+
65
+ def run_params
66
+ params = stringify_keys(fetch('runtime.run', default: {}) || {})
67
+ symbolized = params.to_h { |key, value| [key.to_sym, value] }
68
+ symbolized[:role_arn] ||= role_arn if role_arn
69
+ symbolized[:payload] ||= payload unless payload.empty?
70
+ symbolized
71
+ end
72
+
73
+ def require!(field, value)
74
+ return value if value && value != ''
75
+
76
+ raise ConfigurationError, "missing required project configuration: #{field}"
77
+ end
78
+
79
+ private
80
+
81
+ def fetch(*paths, default:)
82
+ paths.each do |path|
83
+ current = @config
84
+ path.to_s.split('.').each do |part|
85
+ current = current[part] if current.respond_to?(:[])
86
+ end
87
+ return current unless current.nil?
88
+ end
89
+ default
90
+ end
91
+
92
+ def stringify_keys(value)
93
+ case value
94
+ when Hash
95
+ value.to_h { |key, child| [key.to_s, stringify_keys(child)] }
96
+ when Array
97
+ value.map { |child| stringify_keys(child) }
98
+ else
99
+ value
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module Lambda
6
+ module MicroVMs
7
+ # Generates Ruby Lambda MicroVM starter projects.
8
+ class Scaffold
9
+ attr_reader :name, :directory
10
+
11
+ def initialize(name, directory: name, force: false)
12
+ @name = name
13
+ @directory = directory
14
+ @force = force
15
+ end
16
+
17
+ def create
18
+ ensure_target!
19
+ FileUtils.mkdir_p(directory)
20
+ write('Gemfile', gemfile)
21
+ write('Dockerfile', dockerfile)
22
+ write('app.rb', app_rb)
23
+ write('microvm.yml', microvm_yml)
24
+ write('README.md', readme)
25
+ write('.env.example', env_example)
26
+ directory
27
+ end
28
+
29
+ private
30
+
31
+ def ensure_target!
32
+ return if @force || !File.exist?(directory) || Dir.empty?(directory)
33
+
34
+ raise ConfigurationError, "target directory is not empty: #{directory}"
35
+ end
36
+
37
+ def write(path, content)
38
+ File.write(File.join(directory, path), content)
39
+ end
40
+
41
+ def gemfile
42
+ <<~GEMFILE
43
+ source "https://rubygems.org"
44
+
45
+ gem "aws_lambda_ric"
46
+ gem "json"
47
+ GEMFILE
48
+ end
49
+
50
+ def dockerfile
51
+ <<~DOCKERFILE
52
+ FROM ruby:3.4-slim
53
+
54
+ ENV LAMBDA_TASK_ROOT=/var/task
55
+ WORKDIR ${LAMBDA_TASK_ROOT}
56
+
57
+ RUN apt-get update \\
58
+ && apt-get install -y --no-install-recommends build-essential \\
59
+ && rm -rf /var/lib/apt/lists/*
60
+
61
+ COPY Gemfile ${LAMBDA_TASK_ROOT}/Gemfile
62
+ RUN bundle install
63
+
64
+ COPY app.rb ${LAMBDA_TASK_ROOT}/app.rb
65
+
66
+ ENTRYPOINT ["bundle", "exec", "aws_lambda_ric"]
67
+ CMD ["app.App::Handler.process"]
68
+ DOCKERFILE
69
+ end
70
+
71
+ def app_rb
72
+ <<~RUBY
73
+ # frozen_string_literal: true
74
+
75
+ require "json"
76
+
77
+ module App
78
+ class Handler
79
+ def self.process(event:, context:)
80
+ {
81
+ ok: true,
82
+ message: "Hello from #{name}",
83
+ event: event,
84
+ request_id: context.respond_to?(:aws_request_id) ? context.aws_request_id : nil
85
+ }
86
+ end
87
+ end
88
+ end
89
+ RUBY
90
+ end
91
+
92
+ def microvm_yml
93
+ <<~YAML
94
+ name: #{name}
95
+ region: us-east-1
96
+
97
+ # Optional: AWS profile to use for CLI-driven commands.
98
+ # profile: default
99
+
100
+ # IAM role used when running the MicroVM.
101
+ role_arn: arn:aws:iam::123456789012:role/lambda-microvm-runtime
102
+
103
+ build:
104
+ dockerfile: Dockerfile
105
+ context: .
106
+ artifact: tmp/#{name}-microvm.zip
107
+
108
+ deployment:
109
+ bucket: replace-me-lambda-microvm-artifacts
110
+ prefix: lambda-microvms/#{name}
111
+
112
+ image:
113
+ name: #{name}
114
+ # arn: arn:aws:lambda:us-east-1:123456789012:microvm-image:#{name}
115
+ create:
116
+ # Keep this hash close to the AWS Lambda MicroVM create image API shape.
117
+ # lambda-microvms deploy injects code_artifact after S3 upload.
118
+ name: #{name}
119
+
120
+ runtime:
121
+ after: suspend
122
+ payload:
123
+ source: lambda-microvms
124
+ YAML
125
+ end
126
+
127
+ def readme
128
+ <<~README
129
+ # #{name}
130
+
131
+ Ruby AWS Lambda MicroVM application scaffold generated by `lambda-microvms`.
132
+
133
+ This project uses `aws_lambda_ric` inside the image so the Ruby handler can speak the Lambda Runtime API.
134
+
135
+ ## Local setup
136
+
137
+ ```bash
138
+ bundle install
139
+ ```
140
+
141
+ ## Build / deploy from the parent toolkit
142
+
143
+ ```bash
144
+ lambda-microvms doctor
145
+ lambda-microvms package
146
+ lambda-microvms deploy
147
+ lambda-microvms run
148
+ ```
149
+
150
+ Edit `microvm.yml` before deploying.
151
+ README
152
+ end
153
+
154
+ def env_example
155
+ <<~ENV
156
+ AWS_REGION=us-east-1
157
+ AWS_PROFILE=default
158
+ ENV
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lambda
4
+ module MicroVMs
5
+ # Convenience API for run/use/cleanup MicroVM sessions.
6
+ module Session
7
+ module_function
8
+
9
+ def session(image_arn:, role_arn:, after: :suspend, client: Client.new, **run_options)
10
+ vm = client.image(image_arn).run(role_arn: role_arn, **run_options)
11
+ vm.wait_until_running
12
+ yield vm
13
+ ensure
14
+ cleanup(vm, after) if vm
15
+ end
16
+
17
+ def cleanup(vm, after) # rubocop:disable Naming/MethodParameterName
18
+ case after
19
+ when :keep, nil
20
+ nil
21
+ when :suspend
22
+ vm.suspend
23
+ when :terminate
24
+ vm.terminate
25
+ else
26
+ raise ArgumentError, "unknown cleanup policy: #{after.inspect}"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lambda
4
+ module MicroVMs
5
+ # Internal helpers shared by resource objects.
6
+ module Util
7
+ module_function
8
+
9
+ def extract(value, *keys)
10
+ keys.each do |key|
11
+ if value.respond_to?(key)
12
+ result = value.public_send(key)
13
+ return result unless result.nil?
14
+ end
15
+
16
+ next unless value.respond_to?(:[])
17
+
18
+ begin
19
+ result = value[key]
20
+ return result unless result.nil?
21
+ rescue KeyError, TypeError
22
+ nil
23
+ end
24
+ end
25
+
26
+ nil
27
+ end
28
+
29
+ def normalize_state(value)
30
+ value.to_s.downcase.to_sym
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lambda
4
+ module MicroVMs
5
+ VERSION = '0.0.0'
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lambda
4
+ module MicroVMs
5
+ # Polls a resource until a desired lifecycle state is reached.
6
+ class Waiter
7
+ DEFAULT_DELAY = 1.0
8
+ DEFAULT_TIMEOUT = 60.0
9
+
10
+ def initialize(delay: DEFAULT_DELAY, timeout: DEFAULT_TIMEOUT, sleeper: Kernel)
11
+ @delay = delay
12
+ @timeout = timeout
13
+ @sleeper = sleeper
14
+ end
15
+
16
+ def wait(message: 'condition')
17
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
18
+
19
+ loop do
20
+ value = yield
21
+ return value if value
22
+
23
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
+ break if now >= deadline
25
+
26
+ @sleeper.sleep([@delay, deadline - now].min)
27
+ end
28
+
29
+ raise WaitTimeoutError, "timed out waiting for #{message}"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'microvms/version'
4
+ require_relative 'microvms/error'
5
+ require_relative 'microvms/util'
6
+ require_relative 'microvms/waiter'
7
+ require_relative 'microvms/endpoint'
8
+ require_relative 'microvms/client'
9
+ require_relative 'microvms/image'
10
+ require_relative 'microvms/microvm'
11
+ require_relative 'microvms/session'
12
+ require_relative 'microvms/project'
13
+ require_relative 'microvms/scaffold'
14
+ require_relative 'microvms/packager'
15
+ require_relative 'microvms/deployer'
16
+ require_relative 'microvms/doctor'
17
+
18
+ module Lambda
19
+ # Idiomatic Ruby lifecycle helpers for AWS Lambda MicroVMs.
20
+ module MicroVMs
21
+ module_function
22
+
23
+ def session(...)
24
+ Session.session(...)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,153 @@
1
+ module Lambda
2
+ module MicroVMs
3
+ VERSION: String
4
+
5
+ def self.session: (image_arn: String, role_arn: String, ?after: Symbol, ?client: Client, **untyped) { (MicroVM) -> untyped } -> untyped
6
+
7
+ class Error < StandardError
8
+ end
9
+
10
+ class WaitTimeoutError < Error
11
+ end
12
+
13
+ class UnsupportedOperationError < Error
14
+ end
15
+
16
+ class ConfigurationError < Error
17
+ end
18
+
19
+ class CommandError < Error
20
+ end
21
+
22
+ class EndpointError < Error
23
+ attr_reader status: Integer
24
+ attr_reader body: String?
25
+ def initialize: (String message, status: Integer, body: String?) -> void
26
+ end
27
+
28
+ class Client
29
+ attr_reader sdk: untyped
30
+ def initialize: (?region: String?, ?profile: String?, ?sdk: untyped, **untyped) -> void
31
+ def image: (String arn) -> Image
32
+ def microvm: (String id_or_arn) -> MicroVM
33
+ def create_image: (**untyped) -> Image
34
+ def get_image: (**untyped) -> Image
35
+ def delete_image: (**untyped) -> untyped
36
+ def run: (**untyped) -> MicroVM
37
+ def run_microvm: (**untyped) -> MicroVM
38
+ def get_microvm: (**untyped) -> MicroVM
39
+ def list_microvms: (**untyped) -> untyped
40
+ def suspend_microvm: (**untyped) -> untyped
41
+ def resume_microvm: (**untyped) -> untyped
42
+ def terminate_microvm: (**untyped) -> untyped
43
+ def create_auth_token: (**untyped) -> untyped
44
+ def create_microvm_auth_token: (**untyped) -> untyped
45
+ def call_sdk: (Symbol operation, **untyped) -> untyped
46
+ end
47
+
48
+ class Image
49
+ attr_reader client: Client
50
+ attr_reader arn: String?
51
+ attr_reader data: untyped
52
+ def self.from_response: (client: Client, response: untyped) -> Image
53
+ def initialize: (client: Client, arn: String?, ?data: untyped) -> void
54
+ def refresh: () -> Image
55
+ def state: () -> Symbol
56
+ def ready?: () -> bool
57
+ def wait_until_ready: (?delay: Numeric, ?timeout: Numeric) -> Image
58
+ def run: (role_arn: String, ?payload: untyped, **untyped) -> MicroVM
59
+ end
60
+
61
+ class MicroVM
62
+ attr_reader client: Client
63
+ attr_reader id: String?
64
+ attr_reader data: untyped
65
+ def self.from_response: (client: Client, response: untyped) -> MicroVM
66
+ def initialize: (client: Client, id: String?, ?data: untyped) -> void
67
+ def refresh: () -> MicroVM
68
+ def state: () -> Symbol
69
+ def running?: () -> bool
70
+ def suspended?: () -> bool
71
+ def terminated?: () -> bool
72
+ def pending?: () -> bool
73
+ def endpoint_url: () -> String?
74
+ def auth_token: (?ports: untyped, ?ttl_seconds: Integer?, **untyped) -> untyped
75
+ def endpoint: (?token: String?, **untyped) -> Endpoint
76
+ def get: (String path, ?token: String?, **untyped) -> untyped
77
+ def post: (String path, ?json: untyped, ?body: String?, ?token: String?, **untyped) -> untyped
78
+ def suspend: (**untyped) -> MicroVM
79
+ def resume: (**untyped) -> MicroVM
80
+ def terminate: (**untyped) -> MicroVM
81
+ def wait_until_running: (?delay: Numeric, ?timeout: Numeric) -> MicroVM
82
+ def wait_until_suspended: (?delay: Numeric, ?timeout: Numeric) -> MicroVM
83
+ def wait_until_terminated: (?delay: Numeric, ?timeout: Numeric) -> MicroVM
84
+ end
85
+
86
+ class Endpoint
87
+ attr_reader url: String
88
+ def initialize: (url: String, token: String?, ?http: untyped) -> void
89
+ def get: (String path, ?headers: Hash[String, String]) -> untyped
90
+ def post: (String path, ?json: untyped, ?body: String?, ?headers: Hash[String, String]) -> untyped
91
+ def request: (singleton(Net::HTTPGenericRequest) klass, String path, ?json: untyped, ?body: String?, ?headers: Hash[String, String]) -> untyped
92
+ end
93
+
94
+ class Project
95
+ DEFAULT_CONFIG: String
96
+ attr_reader root: String
97
+ attr_reader config_path: String
98
+ attr_reader config: Hash[String, untyped]
99
+ def self.load: (?String path) -> Project
100
+ def initialize: (root: String, config_path: String, config: Hash[untyped, untyped]) -> void
101
+ def name: () -> String
102
+ def region: () -> String?
103
+ def profile: () -> String?
104
+ def role_arn: () -> String?
105
+ def image_arn: () -> String?
106
+ def image_name: () -> String
107
+ def dockerfile: () -> String
108
+ def build_context: () -> String
109
+ def artifact_path: () -> String
110
+ def s3_bucket: () -> String?
111
+ def s3_prefix: () -> String
112
+ def lifecycle_after: () -> Symbol
113
+ def payload: () -> Hash[String, untyped]
114
+ def create_image_params: (artifact_uri: String) -> Hash[Symbol, untyped]
115
+ def run_params: () -> Hash[Symbol, untyped]
116
+ def require!: (String field, untyped value) -> untyped
117
+ end
118
+
119
+ class Scaffold
120
+ attr_reader name: String
121
+ attr_reader directory: String
122
+ def initialize: (String name, ?directory: String, ?force: bool) -> void
123
+ def create: () -> String
124
+ end
125
+
126
+ class Packager
127
+ DEFAULT_EXCLUDES: Array[String]
128
+ attr_reader project: Project
129
+ def initialize: (Project project) -> void
130
+ def package: (?output: String) -> String
131
+ end
132
+
133
+ class Deployer
134
+ attr_reader project: Project
135
+ attr_reader client: Client
136
+ attr_reader s3: untyped
137
+ def initialize: (project: Project, ?client: untyped, ?s3: untyped) -> void
138
+ def package: () -> String
139
+ def upload: (String path) -> String
140
+ def deploy: () -> Image
141
+ def run: () -> MicroVM
142
+ end
143
+
144
+ class Doctor
145
+ Check: untyped
146
+ attr_reader project: Project
147
+ attr_reader runner: untyped
148
+ def initialize: (project: Project, ?runner: untyped) -> void
149
+ def checks: () -> Array[untyped]
150
+ def ok?: () -> bool
151
+ end
152
+ end
153
+ end