Skip to content

Commit c07864f

Browse files
committed
lambci as a base image
1 parent 497d374 commit c07864f

File tree

7 files changed

+70
-113
lines changed

7 files changed

+70
-113
lines changed

Dockerfile

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
1-
FROM python:3.6
1+
FROM python:3.6 as builder
22

3-
WORKDIR /srv
43

54
COPY requirements.txt requirements.txt
65
RUN pip install -r requirements.txt
76

8-
COPY . .
7+
COPY . /srv
8+
9+
10+
FROM lambci/lambda:python3.6
11+
12+
WORKDIR /srv
13+
14+
USER root
15+
16+
RUN yum install -y git
17+
18+
COPY --from=builder /usr/local/lib/python3.6/site-packages /var/lang/lib/python3.6/site-packages
19+
COPY --from=builder /srv /srv
20+
921

22+
ENTRYPOINT []
1023
CMD ["python", "-u", "app.py"]

app.py

+27-87
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,44 @@
11
import argparse
22
import asyncio
33
import hashlib
4-
import importlib
54
import json
65
import shutil
76
import subprocess
87
import sys
8+
import os
99

1010
import traceback
11-
import timeit
1211
import uuid
1312

1413
import logging
1514

16-
import math
1715
from functools import partial
1816

1917
from aiohttp import web
2018
from os import path
2119

22-
from lambda_local.main import load_lib
23-
from lambda_local.context import Context
2420

25-
SOURCES_DIR = "/src"
21+
SOURCES_DIR = "/var/task"
2622
SOURCES_REQUIREMENTS_NAME = path.join(SOURCES_DIR, "requirements.txt")
2723
PACKAGES_DIR = "/packages"
2824
PACKAGES_REQUIREMENTS_PATH = path.join(PACKAGES_DIR, "requirements.txt")
25+
LAMBDA_USER_ID = 496
2926

3027
logger = logging.getLogger('lambda')
3128

3229

30+
def demote(user_uid, user_gid):
31+
os.setgid(user_gid)
32+
os.setuid(user_uid)
33+
34+
3335
def jsonify(data, status_code=200):
3436
return web.Response(
3537
text=json.dumps(data),
3638
headers={'Content-Type': 'application/json'},
3739
status=status_code)
3840

3941

40-
import os
41-
import types
42-
import importlib
43-
44-
45-
def reload_package(package):
46-
assert(hasattr(package, "__package__"))
47-
fn = package.__file__
48-
fn_dir = os.path.dirname(fn) + os.sep
49-
module_visit = {fn}
50-
del fn
51-
52-
def reload_recursive_ex(module):
53-
importlib.reload(module)
54-
55-
for module_child in vars(module).values():
56-
if isinstance(module_child, types.ModuleType):
57-
fn_child = getattr(module_child, "__file__", None)
58-
if (fn_child is not None) and fn_child.startswith(fn_dir):
59-
if fn_child not in module_visit:
60-
# print("reloading:", fn_child, "from", module)
61-
module_visit.add(fn_child)
62-
reload_recursive_ex(module_child)
63-
64-
return reload_recursive_ex(package)
65-
66-
67-
def load(directory, module, handler_path):
68-
file_path = path.join(directory, module)
69-
file_directory = path.dirname(file_path)
70-
sys.path.append(file_directory)
71-
function_file, function_name = path.splitext(handler_path)
72-
mod = importlib.import_module(function_file)
73-
reload_package(mod)
74-
func = getattr(mod, function_name.strip('.'))
75-
return func
76-
77-
7842
async def parse_request(args, request):
7943
data = await request.json()
8044
arn_string = data.get('arn', '')
@@ -86,53 +50,30 @@ async def parse_request(args, request):
8650
if isinstance(event, str):
8751
event = json.loads(event)
8852

89-
context = Context(args.timeout, arn_string, version)
90-
func = load(args.directory, module, handler)
91-
return func, event, context
53+
return handler, event
9254

9355

94-
async def execute(func, event, context):
95-
loop = asyncio.get_event_loop()
96-
try:
97-
future = loop.run_in_executor(None, func, event, context)
98-
result = await asyncio.wait_for(future, context.timeout)
99-
except asyncio.TimeoutError as e:
100-
result = e
101-
except Exception:
102-
err = sys.exc_info()
103-
result = json.dumps({
104-
"errorMessage": str(err[1]),
105-
"stackTrace": traceback.format_tb(err[2]),
106-
"errorType": err[0].__name__
107-
}, indent=4, separators=(',', ': '))
108-
return result
109-
110-
111-
async def async_execute(request_id, func, event, context):
112-
logger.info("Event: {}".format(event))
113-
logger.info("START RequestId: {}".format(request_id))
114-
115-
start_time = timeit.default_timer()
116-
result = await execute(func, event, context)
117-
end_time = timeit.default_timer()
118-
119-
logger.info("END RequestId: {}".format(request_id))
120-
121-
output_func = logger.error if type(result) is Exception else logger.info
122-
output_func(f"RESULT:\n{result}")
123-
duration = (end_time - start_time) * 1000
124-
billed_duration = math.ceil(duration / 100) * 100
125-
logger.info(
126-
f"REPORT RequestId: {request_id}\t"
127-
f"Duration: {duration:.2f} ms\t"
128-
f"Billed Duration: {billed_duration:.2f} ms")
129-
return result
56+
async def async_execute(handler, event):
57+
process = await asyncio.create_subprocess_exec(
58+
"python", "bootstrap.py", handler, json.dumps(event),
59+
cwd='/var/runtime/awslambda/',
60+
env={**os.environ, 'PYTHONPATH': PACKAGES_DIR},
61+
preexec_fn=partial(demote, LAMBDA_USER_ID, LAMBDA_USER_ID),
62+
stdin=asyncio.subprocess.PIPE,
63+
stdout=asyncio.subprocess.PIPE,
64+
stderr=asyncio.subprocess.PIPE,
65+
)
66+
stdout, stderr = await process.communicate()
67+
if stdout:
68+
stdout = json.loads(stdout.decode('utf-8'))
69+
if stderr:
70+
stderr = stderr.decode('utf-8')
71+
return {'stdout': stdout, 'stderr': stderr}
13072

13173

13274
async def run_lambda(args, request):
133-
func, event, context = await parse_request(args, request)
134-
request_id = uuid.uuid4()
135-
result = await async_execute(request_id, func, event, context)
75+
handler, event = await parse_request(args, request)
76+
result = await async_execute(handler, event)
13677
return jsonify(data=result)
13778

13879

@@ -162,7 +103,6 @@ def install_requirements(args, force=False):
162103
shutil.copy(requirements_path, previous_requirements_path)
163104
else:
164105
logger.info("Requirements not changed, skipping update...")
165-
load_lib(PACKAGES_DIR)
166106

167107

168108
def init_logging(args):

docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ services:
66
- 8080:8080
77
volumes:
88
- .:/srv
9-
- ./test:/src/
9+
- ./test:/var/task

index.html

+24-11
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,14 @@ <h1 class="subtitle">Output</h1>
7575
<div v-if="loading">Loading...</div>
7676
<div v-else>
7777
<div v-if="tab === 'event'">
78-
{{ this.output }}
78+
<ul>
79+
<li>
80+
<strong>Output:</strong><pre>{{ this.responseData }}</pre>
81+
</li>
82+
<li>
83+
<strong>Logs:</strong><pre>{{ this.responseLog }}</pre>
84+
</li>
85+
</ul>
7986
</div>
8087
<div v-else>
8188
<ul>
@@ -89,6 +96,9 @@ <h1 class="subtitle">Output</h1>
8996
<strong>Body:</strong>
9097
<pre>{{ this.responseBody | prettify }}</pre>
9198
</li>
99+
<li>
100+
<strong>Logs:</strong><pre>{{ this.responseLog }}</pre>
101+
</li>
92102
</ul>
93103
</div>
94104
</div>
@@ -122,21 +132,24 @@ <h1 class="subtitle">Output</h1>
122132
this.loadState()
123133
},
124134
computed: {
125-
isJsonResponse: function () {
126-
return this.output && this.output.headers && this.output.headers['Content-Type'] === 'application/json'
135+
responseData: function () {
136+
return this.output && this.output.stdout
127137
},
128-
responseBody: function () {
129-
if(this.isJsonResponse) {
130-
return JSON.parse(this.output.body)
131-
} else {
132-
return this.output && this.output.body
133-
}
138+
responseLog: function () {
139+
return this.output && this.output.stderr
134140
},
135141
responseStatusCode: function () {
136-
return this.output && this.output.statusCode
142+
return this.responseData && this.responseData.statusCode
137143
},
138144
responseHeaders: function () {
139-
return this.output && this.output.headers
145+
return this.responseData && this.responseData.headers
146+
},
147+
responseBody: function () {
148+
try {
149+
return JSON.parse(this.responseData.body)
150+
} catch(e) {
151+
return this.responseData && this.responseData.body
152+
}
140153
}
141154
},
142155
filters: {

requirements.in

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
aiohttp
2-
python-lambda-local
1+
aiohttp

requirements.txt

-8
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,8 @@
77
aiohttp==3.1.1
88
async-timeout==2.0.1 # via aiohttp
99
attrs==17.4.0 # via aiohttp
10-
boto3==1.6.17 # via python-lambda-local
11-
botocore==1.9.17 # via boto3, s3transfer
1210
chardet==3.0.4 # via aiohttp
13-
docutils==0.14 # via botocore
1411
idna-ssl==1.0.1 # via aiohttp
1512
idna==2.6 # via idna-ssl, yarl
16-
jmespath==0.9.3 # via boto3, botocore
1713
multidict==4.1.0 # via aiohttp, yarl
18-
python-dateutil==2.6.1 # via botocore
19-
python-lambda-local==0.1.5
20-
s3transfer==0.1.13 # via boto3
21-
six==1.11.0 # via python-dateutil
2214
yarl==1.1.1 # via aiohttp

test/handler.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44
def handler(event, context):
55
response = requests.get('http://example.com')
66
return {
7-
"statusCode": response.status_code,
7+
"statusCode": 200,
88
"url": response.url
99
}

0 commit comments

Comments
 (0)