I have a rather simple test app:
import redis
import os
import logging
log = logging.getLogger()
log.setLevel(logging.DEBUG)
def test_redis(event, context):
redis_endpoint = None
if "REDIS" in os.environ:
redis_endpoint = os.environ["REDIS"]
log.debug("redis: " + redis_endpoint)
else:
log.debug("cannot read REDIS config environment variable")
return {
'statusCode': 500
}
redis_conn = None
try:
redis_conn = redis.StrictRedis(host=redis_endpoint, port=6379, db=0)
redis_conn.set("foo", "boo")
redis_conn.get("foo")
except:
log.debug("failed to connect to redis")
return {
'statusCode': 500
}
finally:
del redis_conn
return {
'statusCode': 200
}
which I have deployed as a HTTP endpoint with serverless
#
# For full config options, check the docs:
# docs.serverless.com
#
service: XXX
plugins:
- serverless-aws-documentation
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: true
provider:
name: aws
stage: dev
region: eu-central-1
runtime: python3.6
environment:
# our cache
REDIS: xx-xx-redis-001.xxx.euc1.cache.amazonaws.com
functions:
hello:
handler: hello/hello_world.say_hello
events:
- http:
path: hello
method: get
# private: true # <-- Requires clients to add API keys values in the `x-api-key` header of their request
# authorizer: # <-- An AWS API Gateway custom authorizer function
testRedis:
handler: test_redis/test_redis.test_redis
events:
- http:
path: test-redis
method: get
When I trigger the endpoint via API Gateway, the lambda just times out after about 7 seconds.
The environmental variable is read properly, no error message displayed.
I suppose there's a problem connecting to the redis, but the tutorial are quite explicit - not sure what the problem could be.
The problem might need the need to set up a NAT, not sure how to accomplish this task with serverless
I ran into this issue as well. For me, there were a few problems that had to be ironed out
The lambda needs VPC permissions.
The ElastiCache security group needs an inbound rule from the Lambda security group that allows communication on the Redis port. I thought they could just be in the same security group.
And the real kicker: I had turned on encryption in-transit. This meant that I needed to pass redis.RedisClient(... ssl=True). The redis-py page mentions that ssl_cert_reqs needs to be set to None for use with ElastiCache, but that didn't seem to be true in my case. I did however need to pass ssl=True.
It makes sense that ssl=True needed to be set but the connection was just timing out so I went round and round trying to figure out what the problem with the permissions/VPC/SG setup was.
Try having the lambda in the same VPC and security group as your elastic cluster
Related
I have Kinesis streams set up locally with Localstack, and Lambda (in Python) set up locally with Serverless Offline. I cannot set up event source between them due to 404 and 500 errors.
Kinesis is set up with Docker-compose:
version: '3'
services:
localstack:
container_name: "localstack"
image: localstack/localstack:latest
environment:
- DEFAULT_REGION=eu-central-1
- SERVICES=kinesis
- DOCKER_HOST=unix:///var/run/docker.sock
ports:
- "4566:4566" # LocalStack Gateway
- "4510-4559:4510-4559" # external services port range
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
Streams are set up with boto3:
import boto3
if __name__ == '__main__':
client = boto3.client(
"kinesis",
region_name="eu-central-1",
endpoint_url="http://localhost:4566"
)
client.create_stream(StreamName="audience_events_local", ShardCount=1)
client.create_stream(StreamName="audience_events_local_cache", ShardCount=1)
Lambda functions are set up with Serverless Offline: serverless offline --stage=local. Relevant part of serverless.yml:
serverless-offline:
httpPort: 3000 # HTTP port to listen on
lambdaPort: 3002 # Lambda HTTP port to listen on
I try to set up event sources with:
import boto3
def get_kinesis_stream_arns() -> list[str]:
client = boto3.client(
"kinesis",
region_name="eu-central-1",
endpoint_url="http://localhost:4566"
)
return [
client.describe_stream(StreamName=stream_name)["StreamDescription"]["StreamARN"]
for stream_name in ["audience_events_local", "audience_events_local_cache"]
]
def create_event_sources(stream_arns: list[str]) -> None:
client = boto3.client(
"lambda",
region_name="eu-central-1",
endpoint_url="http://localhost:4566"
)
for arn in stream_arns:
# example:
# arn:aws:kinesis:eu-central-1:000000000000:stream/audience_events_local
# -> function_name = audience-events-local
function_name = arn.split("/")[-1].replace("_", "-")
client.create_event_source_mapping(
EventSourceArn=arn,
FunctionName=function_name,
MaximumRetryAttempts=2
)
if __name__ == '__main__':
stream_arns = get_kinesis_stream_arns()
print("Stream ARNs:", stream_arns)
create_event_sources(stream_arns)
However, I get errors:
if I use endpoint_url="http://localhost:4566" in create_event_sources, I get botocore.exceptions.ClientError: An error occurred (500) when calling the CreateEventSourceMapping operation (reached max retries: 4):
if I use endpoint_url="http://localhost:3002", I get botocore.exceptions.ClientError: An error occurred (404) when calling the CreateEventSourceMapping operation: Not Found
How can I fix this?
I created a lambda function using serverless in a private subnets of the non default VPC. I wanted to restart the app server of elasticbeanstalk application at a schedule time. I used boto3 and here is the reference [https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/elasticbeanstalk.html][1]
The problem is that when i run the function locally it runs and restart the application server. But when i deploy using sls deploy, it is not working and i get null response back when i test it from the lambda console.
Here is the code:
import json
from logging import log
from loguru import logger
import boto3
from datetime import datetime
import pytz
def main(event, context):
try:
client = boto3.client("elasticbeanstalk", region_name="us-west-1")
applications = client.describe_environments()
current_hour = datetime.now(pytz.timezone("US/Eastern")).hour
for env in applications["Environments"]:
applicationname = env["EnvironmentName"]
if applicationname == "xxxxx-xxx":
response = client.restart_app_server(
EnvironmentName=applicationname,
)
logger.info(response)
print("restarted the application")
return {"statusCode": 200, "body": json.dumps("restarted the instance")}
except Exception as e:
logger.exception(e)
if __name__ == "__main__":
main("", "")
Here the serverless.yml file:
service: beanstalk-starter
frameworkVersion: '2'
provider:
name: aws
runtime: python3.8
lambdaHashingVersion: 20201221
profile: xxxx-admin
region: us-west-1
memorySize: 512
timeout: 15
vpc:
securityGroupIds:
- sg-xxxxxxxxxxx (open on all ports for inbound)
subnetIds:
- subnet-xxxxxxxxxxxxxxxx (private)
- subnet-xxxxxxxxxxxxxxxx (private)
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: non-linux
functions:
main:
handler: handler.main
events:
- schedule: rate(1 minute)
Response from lambda console:
The area below shows the result returned by your function execution. Learn more about returning results from your function.
null
Any help would be appreciated! Let me know what I'm missing here!
To solve this, I have to give these two permissions to my AWS lambda role from the AWS management console. You can also set the permission in the serverless.yml file.
AWSLambdaVPCAccessExecutionRole
AWSCodePipeline_FullAccess
(*Make sure you are using the least privileges while giving permission to a role.)
Thank you.
I am designing a web application where users can have trade bots running. So they will sign in, pay for membership then they will create a bot, enter the credentials and start the bot. The user can stop / start the trade bot.
I am trying to do this using kubernetes, so I will have everything running on kubernetes. I will create a namespace named bots and all bots for all clients will be running inside this bot namespace.
Stack is : python (django framework ) + mysql + aws + kubernetes
question: Is there a way to programmatically create a pod using python ? I want to integrate with the application code. So when user clicks on create new bot it will start a new pod running with all the parameters for the specific user.
Basically each pod will be a tenant. But a tenant can have multiple pods / bots.
So how do that ? Is there any kubernetes python lib that does it ? I did some online search but didn't find anything.
Thanks
As noted by Harsh Manvar, you can user the official Kubernetes Python client. Here is a short function which allows to do it.
from kubernetes import client, config, utils
from kubernetes.client.api import core_v1_api
config.load_incluster_config()
try:
c = Configuration().get_default_copy()
except AttributeError:
c = Configuration()
c.assert_hostname = False
Configuration.set_default(c)
self.core_v1 = core_v1_api.CoreV1Api()
def open_pod(self, cmd: list,
pod_name: str,
namespace: str='bots',
image: str=f'{repository}:{tag}',
restartPolicy: str='Never',
serviceAccountName: str='bots-service-account'):
'''
This method launches a pod in kubernetes cluster according to command
'''
api_response = None
try:
api_response = self.core_v1.read_namespaced_pod(name=pod_name,
namespace=namespace)
except ApiException as e:
if e.status != 404:
print("Unknown error: %s" % e)
exit(1)
if not api_response:
print(f'From {os.path.basename(__file__)}: Pod {pod_name} does not exist. Creating it...')
# Create pod manifest
pod_manifest = {
'apiVersion': 'v1',
'kind': 'Pod',
'metadata': {
'labels': {
'bot': current-bot
},
'name': pod_name
},
'spec': {
'containers': [{
'image': image,
'pod-running-timeout': '5m0s',
'name': f'container',
'args': cmd,
'env': [
{'name': 'env_variable', 'value': env_value},
]
}],
# 'imagePullSecrets': client.V1LocalObjectReference(name='regcred'), # together with a service-account, allows to access private repository docker image
'restartPolicy': restartPolicy,
'serviceAccountName': bots-service-account
}
}
print(f'POD MANIFEST:\n{pod_manifest}')
api_response = self.core_v1.create_namespaced_pod(body=pod_manifest, namespace=namespace)
while True:
api_response = self.core_v1.read_namespaced_pod(name=pod_name,
namespace=namespace)
if api_response.status.phase != 'Pending':
break
time.sleep(0.01)
print(f'From {os.path.basename(__file__)}: Pod {pod_name} in {namespace} created.')
return pod_name
For further investigation, refer to the examples in the official github repo: https://github.com/kubernetes-client/python/tree/master/examples
you can use the official Python Kubernetes client to create and manage the POD across the cluster programmatically.
https://github.com/kubernetes-client/python
You can keep one YAML file and replace the values into as per requirement like Deployment Name, Ports and apply the files to the cluster it will create the POD with base image.
I'm using zappa to deploy a python/django wsgi app to AWS API Gateway and Lambda.
I have all of these in my environment:
NEW_RELIC_CONFIG_FILE: /var/task/newrelic.ini
NEW_RELIC_LICENSE_KEY: redacted
NEW_RELIC_ENVIRONMENT: dev-zappa
NEW_RELIC_STARTUP_DEBUG: "on"
NEW_RELIC_ENABLED: "on"
I'm doing "manual agent start" in my wsgi.py as documented:
import newrelic.agent
# Will collect NEW_RELIC_CONFIG_FILE and NEW_RELIC_ENVIRONMENT from the environment
# Dear god why??!?!
# NB: Looks like this IS what makes it go
newrelic.agent.global_settings().enabled = True
newrelic.agent.initialize('/var/task/newrelic.ini', 'dev-zappa', log_file='stderr', log_level=logging.DEBBUG)
I'm not using #newrelic.agent.wsgi_application since django should be auto-magically detected
I've added a middleware to shutdown the agent before the lambda gets frozen, but the logging suggests that only the first request is being sent to New Relic. Without the shutdown, I get no logging from the New Relic agent, and there are no events in APM.
class NewRelicShutdownMiddleware(MiddlewareMixin):
"""Simple middleware that shutsdown the NR agent at the end of a request"""
def process_request(self, request):
pass
# really wait for the agent to register with collector
# Enabling this causes more log messages about starting data samplers, but only on the first request
# newrelic.agent.register_application(timeout=10)
def process_response(self, request, response):
newrelic.agent.shutdown_agent(timeout=2.5)
return response
def process_exception(self, request, exception):
pass
newrelic.agent.shutdown_agent(timeout=2.5)
In my newrelic.ini I have the following, but when I log newrelic.agent.global_settings() it contains the default App name (which did get created in APM) and enabled = False, which led to some of the hacks above (environment var, and just editing newrelic.agent.global_settings() before initialize :
[newrelic:dev-zappa]
app_name = DEV APP zappa
monitor_mode = true
TL;DR - Two questions:
How to get New Relic to read it's ini file when it doesn't want to?
How to get New Relic to record data for all requests in AWS lambda?
Zappa does not use your wsgi.py file (currently), so the hooks there aren't happening. Take a look at this PR which allows for it: https://github.com/Miserlou/Zappa/pull/1251
Trying to get authentication working with Django channels with a very simple websockets app that echoes back whatever the user sends over with a prefix "You said: ".
My processes:
web: gunicorn myproject.wsgi --log-file=- --pythonpath ./myproject
realtime: daphne myproject.asgi:channel_layer --port 9090 --bind 0.0.0.0 -v 2
reatime_worker: python manage.py runworker -v 2
I run all processes when testing locally with heroku local -e .env -p 8080, but you could also run them all separately.
Note I have WSGI on localhost:8080 and ASGI on localhost:9090.
Routing and consumers:
### routing.py ###
from . import consumers
channel_routing = {
'websocket.connect': consumers.ws_connect,
'websocket.receive': consumers.ws_receive,
'websocket.disconnect': consumers.ws_disconnect,
}
and
### consumers.py ###
import traceback
from django.http import HttpResponse
from channels.handler import AsgiHandler
from channels import Group
from channels.sessions import channel_session
from channels.auth import channel_session_user, channel_session_user_from_http
from myproject import CustomLogger
logger = CustomLogger(__name__)
#channel_session_user_from_http
def ws_connect(message):
logger.info("ws_connect: %s" % message.user.email)
message.reply_channel.send({"accept": True})
message.channel_session['prefix'] = "You said"
# message.channel_session['django_user'] = message.user # tried doing this but it doesn't work...
#channel_session_user_from_http
def ws_receive(message, http_user=True):
try:
logger.info("1) User: %s" % message.user)
logger.info("2) Channel session fields: %s" % message.channel_session.__dict__)
logger.info("3) Anything at 'django_user' key? => %s" % (
'django_user' in message.channel_session,))
user = User.objects.get(pk=message.channel_session['_auth_user_id'])
logger.info(None, "4) ws_receive: %s" % user.email)
prefix = message.channel_session['prefix']
message.reply_channel.send({
'text' : "%s: %s" % (prefix, message['text']),
})
except Exception:
logger.info("ERROR: %s" % traceback.format_exc())
#channel_session_user_from_http
def ws_disconnect(message):
logger.info("ws_disconnect: %s" % message.__dict__)
message.reply_channel.send({
'text' : "%s" % "Sad to see you go :(",
})
And then to test, I go into Javascript console on the same domain as my HTTP site, and type in:
> var socket = new WebSocket('ws://localhost:9090/')
> socket.onmessage = function(e) {console.log(e.data);}
> socket.send("Testing testing 123")
VM481:2 You said: Testing testing 123
And my local server log shows:
ws_connect: test#test.com
1) User: AnonymousUser
2) Channel session fields: {'_SessionBase__session_key': 'chnb79d91b43c6c9e1ca9a29856e00ab', 'modified': False, '_session_cache': {u'prefix': u'You said', u'_auth_user_hash': u'ca4cf77d8158689b2b6febf569244198b70d5531', u'_auth_user_backend': u'django.contrib.auth.backends.ModelBackend', u'_auth_user_id': u'1'}, 'accessed': True, 'model': <class 'django.contrib.sessions.models.Session'>, 'serializer': <class 'django.core.signing.JSONSerializer'>}
3) Anything at 'django_user' key? => False
4) ws_receive: test#test.com
Which, of course, makes no sense. Few questions:
Why would Django see message.user as an AnonymousUser but have the actual user id _auth_user_id=1 (this is my correct user ID) in the session?
I am running my local server (WSGI) on 8080 and daphne (ASGI) on 9090 (different ports). And I didn't include session_key=xxxx in my WebSocket connection - yet Django was able to read my browser's cookie for the correct user, test#test.com? According to Channels docs, this shouldn't be possible.
Under my setup, what is the best / simplest way to carry out authentication with Django channels?
Note: This answer is explicit to channels 1.x, channels 2.x uses a different auth mechanism.
I had a hard time with django channels too, i had to dig into the source code to better understand the docs ...
Question 1:
The docs mention this kind of long trail of decorators relying on each other (http_session, http_session_user ...) that you can use to wrap your message consumers, in the middle of that trail it states this:
Now, one thing to note is that you only get the detailed HTTP information during the connect message of a WebSocket connection (you can read more about that in the ASGI spec) - this means we’re not wasting bandwidth sending the same information over the wire needlessly.
This also means we’ll have to grab the user in the connection handler and then store it in the session;....
Its easy to get lost in all that, at least we both did ...
You just have to remember that this happens when you use channel_session_user_from_http:
It calls http_session_user
a. calls http_session which will parse the message and give us a message.http_session attribute.
b. Upon returning from the call, it initiates a message.user based on the information it got in message.http_session ( this will bite you later)
It calls channel_session which will initiate a dummy session in message.channel_session and ties it to the message reply channel.
Now it calls transfer_user which will move the http_session into the channel_session
This happens during the connection handling of a websocket, so on subsequent messages you won't have acces to detailed HTTP information, so what's happening after the connect is that you're calling channel_session_user_from_http again, which in this situation (post-connect messages) calls http_session_user which will attempt reading the Http information but fails resulting in setting message.http_session to None and overriding message.user to AnonymousUser.
That's why you need to use channel_session_user in this case.
Question 2:
Channels can use Django sessions either from cookies (if you’re running your websocket server on the same port as your main site, using something like Daphne), or from a session_key GET parameter, which works if you want to keep running your HTTP requests through a WSGI server and offload WebSockets to a second server process on another port.
Remember http_session, that decorator that gets us the message.http_session data? it appears that if it doesn't find a session_key GET parameter it fails to settings.SESSION_COOKIE_NAME, which is the regular sessionid cookie, so whether you provide session_key or not, you'll still get connected if you're logged in, of course that happens only when your ASGI and WSGI servers are on the same domain (127.0.0.1 in this case), the port difference doesn't matter.
I think the difference that the docs are trying to communicate but didn't expand on is that you need to setup session_key GET parameter when having your ASGI and WSGI servers on different domains since cookies are restricted by domain not port.
Due to that lack of explanation i had to test running ASGI and WSGI on same port and different port and the result was the same, i was still getting authenticated, changed one server domain to 127.0.0.2 instead of 127.0.0.1 and the authentication was gone, set the session_key get parameter and the authentication was back again.
Update: a rectification of the docs paragraph was just pushed to the channels repo, it was meant to mention domain instead of port like i mentioned.
Question 3:
my answer is the same as turbotux's but longer, you should use #channel_session_user_from_http on ws_connect and #channel_session_user on ws_receive and ws_disconnect, nothing from what you showed tells that it won't work if you do that change, maybe try removing http_user=True from your receive consumer? even thou i suspect it has no effect since its undocumented and intended only to be used by Generic Consumers...
Hope this helps!
To answer your first question you need to use the:
channel_session_user
decorator in the receive and disconnect calls.
channel_session_user_from_http
calls the transfer_user session during the connect method to transfer the http session to the channel session. This way all future calls may access the channel session to retrieve user information.
To your second question I believe what you are seeing is that default web socket library passes the browser cookies over the connection.
Third, I think your setup will be working quite well once have changed the decorators.
I ran into this problem and I found that it was due to a couple of issues that might be the cause. I'm not suggesting this will solve your issue, but might give you some insight. Keep in mind I am using rest framework. First I was overriding the User model. Second when I defined the application variable in my root routing.py I didn't use my own AuthMiddleware. I was using the docs suggested AuthMiddlewareStack. So, per the Channels docs, I defined my own custom authentication middleware, which takes my JWT value from the cookies, authenticates it and assigns it to the scope["user"] like so:
routing.py
from channels.routing import ProtocolTypeRouter, URLRouter
import app.routing
from .middleware import JsonTokenAuthMiddleware
application = ProtocolTypeRouter(
{
"websocket": JsonTokenAuthMiddleware(
(URLRouter(app.routing.websocket_urlpatterns))
)
}
middleware.py
from http import cookies
from django.contrib.auth.models import AnonymousUser
from django.db import close_old_connections
from rest_framework.authtoken.models import Token
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
class JsonWebTokenAuthenticationFromScope(BaseJSONWebTokenAuthentication):
def get_jwt_value(self, scope):
try:
cookie = next(x for x in scope["headers"] if x[0].decode("utf-8")
== "cookie")[1].decode("utf-8")
return cookies.SimpleCookie(cookie)["JWT"].value
except:
return None
class JsonTokenAuthMiddleware(BaseJSONWebTokenAuthentication):
def __init__(self, inner):
self.inner = inner
def __call__(self, scope):
try:
close_old_connections()
user, jwt_value =
JsonWebTokenAuthenticationFromScope().authenticate(scope)
scope["user"] = user
except:
scope["user"] = AnonymousUser()
return self.inner(scope)
Hope this helps this helps!