Technical Documentation: aws/chalice
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
Source Api (`docs/source/api.rst`)
Chalice
.. class:: Chalice(app_name)
This class represents a chalice application. It provides:
- The ability to register routes using the :meth:
routemethod. - Within a view function, the ability to introspect the current
request using thecurrent_requestattribute which is an instance
of the :class:Requestclass.
.. attribute:: app_name
The name of the Chalice app. This corresponds to the value provided
when instantiating a ``Chalice`` object... attribute:: current_request
An object of type :class:`Request`. This value is only set when
a view function is being called. This attribute can be used to
introspect the current HTTP request... attribute:: api
An object of type :class:`APIGateway`. This attribute can be used to control
how apigateway interprets ``Content-Type`` headers in both requests and
responses... attribute:: lambda_context
A Lambda context object that is passed to the invoked view by AWS
Lambda. You can find out more about this object by reading the
`lambda context object documentation <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_.
.. note::
This is only set on ``@app.route`` handlers. For other handlers it
will be ``None``. Instead the event parameter will have a ``context``
property. For example :attr:`S3Event.context`... attribute:: debug
A boolean value that enables debugging. By default, this value is
``False``. If debugging is true, then internal errors are returned back
to the client. Additionally, debug log messages generated by the
framework will show up in the cloudwatch logs. Example usage:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name="appname")
app.debug = True.. attribute:: websocket_api
An object of type :class:`WebsocketAPI`. This attribute can be used to
send messages to websocket clients connected through API Gateway... method:: route(path, **options)
Register a view function for a particular URI path. This method
is intended to be used as a decorator for a view function. For example:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name="appname")
@app.route('/resource/{value}', methods=['PUT'])
def viewfunction(value):
pass
:param str path: The path to associate with the view function. The
``path`` should only contain ``[a-zA-Z0-9._-]`` chars and curly
braces for parts of the URL you would like to capture. The path
should not end in a trailing slash, otherwise a validation error
will be raised during deployment.
:param list methods: Optional parameter that indicates which HTTP methods
this view function should accept. By default, only ``GET`` requests
are supported. If you only wanted to support ``POST`` requests, you
would specify ``methods=['POST']``. If you support multiple HTTP
methods in a single view function (``methods=['GET', 'POST']``), you
can check the :attr:`app.current_request.method <Request.method>`
attribute to see which HTTP method was used when making the request.
You can provide any HTTP method supported by API Gateway, which
includes: ``GET``, ``POST``, ``PUT``, ``PATCH``, ``HEAD``,
``OPTIONS``, and ``DELETE``.
:param str name: Optional parameter to specify the name of the view
function. You generally do not need to set this value. The name
of the view function is used as the default value for the view name.
:param Authorizer authorizer: Specify an authorizer to use for this
view. Can be an instance of :class:`CognitoUserPoolAuthorizer`,
:class:`CustomAuthorizer` or :class:`IAMAuthorizer`.
:param str content_types: A list of content types to accept for
this view. By default ``application/json`` is accepted. If
this value is specified, then chalice will reject any incoming request
that does not match the provided list of content types with a
415 Unsupported Media Type response.
:param boolean api_key_required: Optional parameter to specify whether
the method required a valid API key.
:param cors: Specify if CORS is supported for this view. This can either
by a boolean value, ``None``, or an instance of :class:`CORSConfig`.
Setting this value is set to ``True`` gives similar behavior to enabling
CORS in the AWS Console. This includes injecting the
``Access-Control-Allow-Origin`` header to have a value of ``*`` as well
as adding an ``OPTIONS`` method to support preflighting requests. If
you would like more control over how CORS is configured, you can provide
an instance of :class:`CORSConfig`... method:: authorizer(name, **options)
Register a built-in authorizer.
.. code-block:: python
from chalice import Chalice, AuthResponse
app = Chalice(app_name="appname")
@app.authorizer(ttl_seconds=30)
def my_auth(auth_request):
# Validate auth_request.token, and then:
return AuthResponse(routes=['/'], principal_id='username')
@app.route('/', authorizer=my_auth)
def viewfunction(value):
pass
:param ttl_seconds: The number of seconds to cache this response.
Subsequent requests that require this authorizer will use a
cached response if available. The default is 300 seconds.
:param execution_role: An optional IAM role to specify when invoking
the Lambda function associated with the built-in authorizer.
:param header: The header where the auth token will be specified.
The default is ``Authorization``.. method:: schedule(expression, name=None)
Register a scheduled event that's invoked on a regular schedule.
This will create a lambda function associated with the decorated
function. It will also schedule the lambda function to be invoked
with a scheduled CloudWatch Event.
See :ref:`scheduled-events` for more information.
.. code-block:: python
@app.schedule('cron(15 10 ? * 6L 2002-2005)')
def cron_handler(event):
pass
@app.schedule('rate(5 minutes)')
def rate_handler(event):
pass
@app.schedule(Rate(5, unit=Rate.MINUTES))
def rate_obj_handler(event):
pass
@app.schedule(Cron(15, 10, '?', '*', '6L', '2002-2005'))
def cron_obj_handler(event):
pass
:param expression: The schedule expression to use for the CloudWatch
event rule. This value can either be a string value or an
instance of type ``ScheduleExpression``, which is either a
:class:`Cron` or :class:`Rate` object. If a string value is
provided, it will be provided directly as the ``ScheduleExpression``
value in the `PutRule <https://docs.aws.amazon.com/AmazonCloudWatchEvents/latest/APIReference/API_PutRule.html#API_PutRule_RequestSyntax>`__ API
call.
:param name: The name of the function to use. This name is combined
with the chalice app name as well as the stage name to create the
entire lambda function name. This parameter is optional. If it is
not provided, the name of the python function will be used... method:: on_cw_event(pattern, name=None)
Create a lambda function and configure it to be invoked whenever
an event that matches the given pattern flows through CloudWatch Events
or Event Bridge.
:param pattern: The event pattern to use to filter subscribed events.
See the CloudWatch Events docs for examples https://amzn.to/2OlqZso
:param name: The name of the function to create. This name is combined
with the chalice app name as well as the stage name to create the
entire lambda function name. This parameter is optional. If it is
not provided, the name of the python function will be used... method:: on_s3_event(bucket, events=None, prefix=None, suffix=None, name=None)
Create a lambda function and configure it to be automatically invoked
whenever an event happens on an S3 bucket.
.. warning::
You can't use the ``chalice package`` command when using the
``on_s3_event`` decorator. This is because CFN does not support
configuring an existing S3 bucket.
See :ref:`s3-events` for more information.
This example shows how you could implement an image resizer that's
triggered whenever an object is uploaded to the ``images/`` prefix
of an S3 bucket (e.g ``s3://mybucket/images/house.jpg``).
.. code-block:: python
@app.on_s3_event('mybucket', events=['s3:ObjectCreated:Put'],
prefix='images/', suffix='.jpg')
def resize_image(event):
with tempfile.NamedTemporaryFile('w') as f:
s3.download_file(event.bucket, event.key, f.name)
resize_image(f.name)
s3.upload_file(event.bucket, 'resized/%s' % event.key, f.name)
:param bucket: The name of the S3 bucket. This bucket must already exist.
:param events: A list of strings indicating the events that should trigger
the lambda function. See `Supported Event Types <https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html#supported-notification-event-types>`__
for the full list of strings you can provide. If this option is not
provided, a default of ``['s3:ObjectCreated:*']`` is used, which will
configure the lambda function to be invoked whenever a new object
is created in the S3 bucket.
:param prefix: An optional key prefix. This specifies that
the lambda function should only be invoked if the key starts with
this prefix (e.g. ``prefix='images/'``). Note that this value
is not a glob (e.g. ``images/*``), it is a literal string match
for the start of the key.
:param suffix: An optional key suffix. This specifies that the
lambda function should only be invoked if the key name ends with
this suffix (e.g. ``suffix='.jpg'``). Note that this value is
not a glob (e.g. ``*.txt``), it is a literal string match for
the end of the key.
:param name: The name of the function to use. This name is combined
with the chalice app name as well as the stage name to create the
entire lambda function name. This parameter is optional. If it is
not provided, the name of the python function will be used... method:: on_sns_message(topic, name=None)
Create a lambda function and configure it to be automatically invoked
whenever an SNS message is published to the specified topic.
See :ref:`sns-events` for more information.
This example prints the subject and the contents of the message whenever
something publishes to the sns topic of ``mytopic``. In this example,
the input parameter is of type :class:`SNSEvent`.
.. code-block:: python
app.debug = True
@app.on_sns_message(topic='mytopic')
def handler(event):
app.log.info("SNS subject: %s", event.subject)
app.log.info("SNS message: %s", event.message)
:param topic: The name or ARN of the SNS topic you want to subscribe to.
:param name: The name of the function to use. This name is combined
with the chalice app name as well as the stage name to create the
entire lambda function name. This parameter is optional. If it is
not provided, the name of the python function will be used... method:: on_sqs_message(queue, batch_size=1, name=None, queue_arn=None, maximum_batching_window_in_seconds=0, maximum_concurrency=None)
Create a lambda function and configure it to be automatically invoked
whenever a message is published to the specified SQS queue.
The lambda function must accept a single parameter which
is of type :class:`SQSEvent`.
If the decorated function returns without raising any exceptions
then Lambda will automatically delete the SQS messages associated
with the :class:`SQSEvent`. You don't need to manually delete
messages. If any exception is raised, Lambda won't delete any messages,
and the messages will become available once the visibility timeout
has been reached. Note that for batch sizes of more than one, either
the entire batch succeeds and all the messages in the batch are
deleted by Lambda, or the entire batch fails. The default batch size
is 1. See the
`Using AWS Lambda with Amazon SQS <https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html>`__
for more information on how Lambda integrates with SQS.
See the :ref:`sqs-events` topic guide for more information on using SQS
in Chalice.
.. code-block:: python
app.debug = True
@app.on_sqs_message(queue='myqueue')
def handler(event):
app.log.info("Event: %s", event.to_dict())
for record in event:
app.log.info("Message body: %s", record.body)
:param queue: The name of the SQS queue you want to subscribe to.
This is the name of the queue, not the ARN or Queue URL.
:param batch_size: The maximum number of messages to retrieve
when polling for SQS messages. The event parameter can have
multiple SQS messages associated with it. This is why the
event parameter passed to the lambda function is iterable. The
batch size controls how many messages can be in a single event.
:param name: The name of the function to use. This name is combined
with the chalice app name as well as the stage name to create the
entire lambda function name. This parameter is optional. If it is
not provided, the name of the python function will be used.
:param queue_arn: The ARN of the SQS queue you want to subscribe to.
This argument is mutually exclusive with the ``queue`` parameter.
This is useful if you already know the exact ARN or when integrating
with the AWS CDK to create your SQS queue.
:param maximum_batching_window_in_seconds: The maximum amount of time,
in seconds, to gather records before invoking the function.
:param maximum_concurrency: The maximum number of concurrent functions
that the event source can invoke... method:: on_kinesis_record(stream, batch_size=100, starting_position='LATEST', name=None, maximum_batching_window_in_seconds=0)
Create a lambda function and configure it to be automatically invoked
whenever data is published to the specified Kinesis stream.
The lambda function must accept a single parameter which
is of type :class:`KinesisEvent`.
If the decorated function raises an exception, Lambda retries the
batch until processing succeeds or the data expires.
See
`Using AWS Lambda with Amazon Kinesis <https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html>`__
for more information on how Lambda integrates with Kinesis.
.. code-block:: python
app.debug = True
@app.on_kinesis_record(stream='mystream')
def handler(event):
app.log.info("Event: %s", event.to_dict())
for record in event:
app.log.info("Message body: %s", record.data)
:param stream: The name of the Kinesis stream you want to subscribe to.
This is the name of the data stream, not the ARN.
:param batch_size: The maximum number of messages to retrieve
when polling for Kinesis messages. The event parameter can have
multiple Kinesis records associated with it. This is why the
event parameter passed to the lambda function is iterable. The
batch size controls how many messages can be in a single event.
:param starting_position: Specifies where to start processing records.
This can have the following values:
* ``LATEST`` - Process new records that are added to the stream.
* ``TRIM_HORIZON`` - Process all records in the stream.
:param name: The name of the function to use. This name is combined
with the chalice app name as well as the stage name to create the
entire lambda function name. This parameter is optional. If it is
not provided, the name of the python function will be used.
:param maximum_batching_window_in_seconds: The maximum amount of time,
in seconds, to gather records before invoking the function... method:: on_dynamodb_record(stream_arn, batch_size=100, starting_position='LATEST', name=None, maximum_batching_window_in_seconds=0)
Create a lambda function and configure it to be automatically invoked
whenever data is written to a DynamoDB stream.
The lambda function must accept a single parameter which
is of type :class:`DynamoDBEvent`.
If the decorated function raises an exception, Lambda retries the
batch until processing succeeds or the data expires.
See
`Using AWS Lambda with Amazon DynamoDB <https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html>`__
for more information on how Lambda integrates with DynamoDB Streams.
.. code-block:: python
app.debug = True
@app.on_dynamodb_record(stream_arn='arn:aws:dynamodb:...:stream')
def handler(event):
app.log.info("Event: %s", event.to_dict())
for record in event:
app.log.info("New: %s", record.new_image)
:param stream_arn: The name of the DynamoDB stream ARN you want to
subscribe to. Note that, unlike other event handlers that accept
the resource name, you must provide the stream ARN when subscribing
to the DynamoDB stream ARN.
:param batch_size: The maximum number of messages to retrieve
when polling for DynamoDB messages. The event parameter can have
multiple DynamoDB records associated with it. This is why the
event parameter passed to the lambda function is iterable. The
batch size controls how many messages can be in a single event.
:param starting_position: Specifies where to start processing records.
This can have the following values:
* ``LATEST`` - Process new records that are added to the stream.
* ``TRIM_HORIZON`` - Process all records in the stream.
:param name: The name of the function to use. This name is combined
with the chalice app name as well as the stage name to create the
entire lambda function name. This parameter is optional. If it is
not provided, the name of the python function will be used.
:param maximum_batching_window_in_seconds: The maximum amount of time,
in seconds, to gather records before invoking the function... method:: lambda_function(name=None)
Create a pure lambda function that's not connected to anything.
See :doc:`topics/purelambda` for more information.
:param name: The name of the function to use. This name is combined
with the chalice app name as well as the stage name to create the
entire lambda function name. This parameter is optional. If it is
not provided, the name of the python function will be used... method:: register_blueprint(blueprint, name_prefix=None, url_prefix=None)
Register a :class:`Blueprint` to a Chalice app.
See :doc:`topics/blueprints` for more information.
:param blueprint: The :class:`Blueprint` to register to the app.
:param name_prefix: An optional name prefix that's added to all the
resources specified in the blueprint.
:param url_prefix: An optional url prefix that's added to all the
routes defined the Blueprint. This allows you to set the root mount
point for all URLs in a Blueprint... method:: on_ws_connect(event)
Create a Websocket API connect event handler.
:param event: The :class:`WebsocketEvent` received to indicate a new
connection has been registered with API Gateway. The identifier of this
connection is under the :attr:`WebsocketEvent.connection_id` attribute.
see :doc:`topics/websockets` for more information... method:: on_ws_message(event)
Create a Websocket API message event handler.
:param event: The :class:`WebsocketEvent` received to indicate API Gateway
received a message from a connected client. The identifier of the
client that sent the message is under the
:attr:`WebsocketEvent.connection_id` attribute. The content of the
message is available in the :attr:`WebsocketEvent.body` attribute.
see :doc:`topics/websockets` for more information... method:: on_ws_disconnect(event)
Create a Websocket API disconnect event handler.
:param event: The :class:`WebsocketEvent` received to indicate an existing
connection has been disconnected from API Gateway. The identifier of this
connection is under the :attr:`WebsocketEvent.connection_id` attribute.
see :doc:`topics/websockets` for more information... method:: middleware(event_type='all')
Register a middleware with a Chalice application.
This decorator will register a function as Chalice middleware, which
will be automatically invoked as part of the request/response cycle for
a Lambda invocation. You can provide the ``event_type`` argument to
indicate what type of lambda events you want to register with. The
default value, ``all``, indicates that the middleware will be called
for all Lambda functions defined in your Chalice app. Supported
values are:
* ``all`` - ``Any``
* ``s3`` - :class:`S3Event`
* ``sns`` - :class:`SNSEvent`
* ``sqs`` - :class:`SQSEvent`
* ``cloudwatch`` - :class:`CloudWatchEvent`
* ``scheduled`` - :class:`CloudWatchEvent`
* ``websocket`` - :class:`WebsocketEvent`
* ``http`` - :class:`Request`
* ``pure_lambda`` - :class:`LambdaFunctionEvent`
The decorated function must accept two arguments, ``event`` and
``get_response``. The ``event`` is the input event associated with
the Lambda invocation, and ``get_response`` is a callable that takes
an input event and will invoke the next middleware in the chain,
and eventually the original Lambda handler. Below is a noop middleware
that shows the minimum needed to write middleware:
.. code-block:: python
@app.middleware('all')
def mymiddleware(event, get_response):
return get_response(event)
See :doc:`topics/middleware` for more information on writing middleware... method:: register_middleware(func, event_type='all')
Register a middleware with a Chalice application. This is the same
behavior as the :meth:`Chalice.middleware` decorator and is useful
if you want to register middleware for pre-existing functions:
.. code-block:: python
import thirdparty
app.register_middleware(thirdparty.func, 'all').. class:: ConvertToMiddleware(lambda_wrapper)
This class is used to convert a function that wraps/proxies a Lambda
function into middleware. This allows this wrapper to automatically
be applied to every function in your app. For example, if you had the
following logging decorator:
.. code-block:: python
def log_invocation(func):
def wrapper(event, context):
logger.debug("Before lambda function.")
response = func(event, context)
logger.debug("After lambda function.")
return wrapper
@app.lambda_function()
@log_invocation
def myfunction(event, context):
logger.debug("In myfunction().") Rather than decorate every Lambda function with the @log_invocation
decorator, you can instead use ConvertToMiddleware to automatically
apply this wrapper to every Lambda function in your app.
.. code-block:: python
app.register_middleware(ConvertToMiddleware(log_invoation))Request
.. class:: Request
A class that represents the current request. This is mapped to
the app.current_request object.
.. code-block:: python
@app.route('/objects/{key}', methods=['GET', 'PUT'])
def myobject(key):
request = app.current_request # type: Request
if request.method == 'PUT':
# handle PUT request
pass
elif request.method == 'GET':
# handle GET request
pass.. attribute:: path
The path of the HTTP request... attribute:: query_params
A MultiDict of the query params for the request. This value is ``None``
if no query params were provided in the request. The MultiDict acts like a
normal dictionary except that you can call the method ``getlist()`` to get
multiple keys from the same query string parameter
.. code-block:: python
request = app.current_request
# Raises an exception if key doesn't exist, usual Python behavior.
single_param = request.query_params['single']
# None if key doesn't exist, usual Python behavior
another_param = request.query_params.get('another_param')
# A List of all parameters named multi_param, Throws an exception if
# key doesn't exist
multi_param_list = request.query_params.getlist('multi_param').. attribute:: headers
A dict of the request headers... attribute:: uri_params
A dict of the captured URI params. This value is ``None`` if no
URI params were provided in the request... attribute:: method
The HTTP method as a string... attribute:: json_body
The parsed JSON body (``json.loads(raw_body)``). This value will only
be non-None if the Content-Type header is ``application/json``, which
is the default content type value in chalice... attribute:: raw_body
The raw HTTP body as bytes. This is useful if you need to
calculate a checksum of the HTTP body... attribute:: context
A dict of additional context information... attribute:: stage_vars
A dict of configuration for the API Gateway stage... attribute:: lambda_context
A Lambda context object that is passed to the invoked view by AWS
Lambda. You can find out more about this object by reading the
`lambda context object documentation <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_... method:: to_dict()
Convert the :class:`Request` object to a dictionary. This is useful
for debugging purposes. This dictionary is guaranteed to be JSON
serializable so you can return this value from a chalice view.Response
.. class:: Response(body, headers=None, status_code=200)
A class that represents the response for the view function. You
can optionally return an instance of this class from a view function if you
want complete control over the returned HTTP response.
.. code-block:: python
from chalice import Chalice, Response
app = Chalice(app_name='custom-response')
@app.route('/')
def index():
return Response(body='hello world!',
status_code=200,
headers={'Content-Type': 'text/plain'}).. versionadded:: 0.6.0
.. attribute:: body
The HTTP response body to send back. This value must be a string... attribute:: headers
An optional dictionary of HTTP headers to send back. This is a dictionary
of header name to header value, e.g ``{'Content-Type': 'text/plain'}``.. attribute:: status_code
The integer HTTP status code to send back in the HTTP response.Authorization
Each of these classes below can be provided using the authorizer argument
for an @app.route(authorizer=...) call:
.. code-block:: python
authorizer = CognitoUserPoolAuthorizer(
'MyPool', header='Authorization',
provider_arns=['arn:aws:cognito:...:userpool/name'])
@app.route('/user-pools', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"secure": True}.. class:: CognitoUserPoolAuthorizer(name, provider_arns, header='Authorization')
.. versionadded:: 0.8.1
.. attribute:: name
The name of the authorizer... attribute:: provider_arns
The Cognito User Pool arns to use... attribute:: header
The header where the auth token will be specified... class:: IAMAuthorizer()
.. versionadded:: 0.8.3
.. class:: CustomAuthorizer(name, authorizer_uri, ttl_seconds, header='Authorization')
.. versionadded:: 0.8.1
.. attribute:: name
The name of the authorizer... attribute:: authorizer_uri
The URI of the lambda function to use for the custom authorizer. This
usually has the form
``arn:aws:apigateway:{region}:lambda:path/2015-03-31/functions/{lambda_arn}/invocations``... attribute:: ttl_seconds
The number of seconds to cache the returned policy from a custom
authorizer... attribute:: header
The header where the auth token will be specified.Built-in Authorizers
These classes are used when defining built-in authorizers in Chalice.
.. class:: AuthRequest(auth_type, token, method_arn)
An instance of this class is passed as the first argument
to an authorizer defined via @app.authorizer(). You
generally do not instantiate this class directly.
.. attribute:: auth_type
The type of authentication.. attribute:: token
The authorization token. This is usually the value of the
``Authorization`` header... attribute:: method_arn
The ARN of the API gateway being authorized... class:: AuthResponse(routes, principal_id, context=None)
.. attribute:: routes
A list of authorized routes. Each element in the list
can either by a string route such as `"/foo/bar"` or
an instance of ``AuthRoute``. If you specify the URL as
a string, then all supported HTTP methods will be authorized.
If you want to specify which HTTP methods are allowed, you
can use ``AuthRoute``. If you want to specify that all
routes and HTTP methods are supported you can use the
wildcard value of ``"*"``: ``AuthResponse(routes=['*'], ...)``.. attribute:: principal_id
The principal id of the user... attribute:: context
An optional dictionary of key value pairs. This dictionary
will be accessible in the ``app.current_request.context``
in all subsequent authorized requests for this user... class:: AuthRoute(path, methods)
This class be used in the routes attribute of a
:class:AuthResponse instance to get fine grained control
over which HTTP methods are allowed for a given route.
.. attribute:: path
The allowed route specified as a string.. attribute:: methods
A list of allowed HTTP methods.APIGateway
.. class:: APIGateway()
This class is used to control
how API Gateway interprets Content-Type headers in both requests and
responses.
There is a single instance of this class attached to each
:class:Chalice object under the api attribute.
.. attribute:: cors
Global cors configuration. If a route-level cors configuration is not
provided, or is ``None`` then this configuration will be used. By
default it is set to ``False``. This can either be ``True``, ``False``,
or an instance of the ``CORSConfig`` class. This makes it easy to enable
CORS for your entire application by setting ``app.api.cors = True``.
.. versionadded:: 1.12.1.. attribute:: default_binary_types
The value of ``default_binary_types`` are the ``Content-Types`` that are
considered binary by default. This value should not be changed, instead
you should modify the ``binary_types`` list to change the behavior of a
content type. Its value is: ``application/octet-stream``,
``application/x-tar``, ``application/zip``, ``audio/basic``,
``audio/ogg``, ``audio/mp4``, ``audio/mpeg``, ``audio/wav``,
``audio/webm``, ``image/png``, ``image/jpg``, ``image/jpeg``,
``image/gif``, ``video/ogg``, ``video/mpeg``, ``video/webm``... attribute:: binary_types
The value of ``binary_types`` controls how API Gateway interprets
requests and responses as detailed below.
If an incoming request has a ``Content-Type`` header value that is
present in the ``binary_types`` list it will be assumed that its body is
a sequence of raw bytes. You can access these bytes by accessing the
``app.current_request.raw_body`` property.
If an outgoing response from ``Chalice`` has a header ``Content-Type``
that matches one of the ``binary_types`` its body must be a ``bytes``
type object. It is important to note that originating request must have
the ``Accept`` header for the same type as the ``Content-Type`` on the
response. Otherwise a ``400`` error will be returned.
This value can be modified to change what types API Gateway treats as
binary. The easiest way to do this is to simply append new types to
the list.
.. code-block:: python
app.api.binary_types.append('application/my-binary-data')
Keep in mind that there can only be a total of 25 binary types at a time
and Chalice by default has a list of 16 types. It is recommended if you
are going to make extensive use of binary types to reset the list to
the exact set of content types you will be using. This can easily be
done by reassigning the whole list.
.. code-block:: python
app.api.binary_types = [
'application/octet-stream',
'application/my-binary-data',
]
**Implementation Note**: API Gateway and Lambda communicate through a
JSON event which is encoded using ``UTF-8``. The raw bytes are
temporarily encoded using base64 when being passed between API Gateway
and Lambda. In the worst case this encoding can cause the binary body
to be inflated up to ``4/3`` its original size. Lambda only accepts an
event up to ``6mb``, which means even if your binary data was not quite
at that limit, with the base64 encoding it may exceed that limit. This
will manifest as a ``502`` Bad Gateway error.WebsocketAPI
.. class:: WebsocketAPI
This class is used to send messages to websocket clients connected to an API
Gateway Websocket API.
.. attribute:: session
A boto3 Session that will be used to send websocket messages to
clients. Any custom configuration can be set through a botocore
``session``. This **must** be manually set before websocket features can
be used.
.. code-block:: python
import botocore
from boto3.session import Session
from chalice import Chalice
app = Chalice('example')
session = botocore.session.Session()
session.set_config_variable('retries', {'max_attempts': 0})
app.websocket_api.session = Session(botocore_session=session).. method:: configure(domain_name, stage)
Configure prepares the :class:`WebsocketAPI` to call the :meth:`send`
method. Without first calling this method calls to :meth:`send` will fail
with the message ``WebsocketAPI needs to be configured before sending
messages.``. This is because a boto3 ``apigatewaymanagementapi`` client
must be created from the :attr:`session` with a custom endpoint in order
to properly communicate with our API Gateway WebsocketAPI. This method is
called on your behalf before each of the websocket handlers:
``on_ws_connect``, ``on_ws_message``, ``on_ws_disconnect``. This ensures
that the :meth:`send` method is available in each of those handlers... _websocket-send:
.. method:: send(connection_id, message)
*requires* ``boto3>=1.9.91``
Method to send a message to a client. The ``connection_id`` is the unique
identifier of the socket to send the ``message`` to. The ``message`` must
be a utf-8 string.
If the socket is disconnected it raises a :class:`WebsocketDisconnectedError`
error... method:: close(connection_id)
*requires* ``boto3>=1.9.221``
Method to close a WebSocket connection. The ``connection_id`` is the
unique identifier of the socket to close.
If the socket is already disconnected it raises a
:class:`WebsocketDisconnectedError` error... method:: info(connection_id)
*requires* ``boto3>=1.9.221``
Method to get info about a WebSocket. The ``connection_id`` is the unique
identifier of the socket to get info about.
The following is an example of the format this method returns::
{
'ConnectedAt': datetime(2015, 1, 1),
'Identity': {
'SourceIp': 'string',
'UserAgent': 'string'
},
'LastActiveAt': datetime(2015, 1, 1)
}
If the socket is disconnected it raises a :class:`WebsocketDisconnectedError`
error... class:: WebsocketDisconnectedError
An exception raised when a message is sent to a websocket that has disconnected.
.. attribute:: connection_id
The unique identifier of the websocket that was disconnected.CORS
.. class:: CORSConfig(allow_origin='*', allow_headers=None, expose_headers=None, max_age=None, allow_credentials=None)
CORS configuration to attach to a route, or globally on app.api.cors.
.. code-block:: python
from chalice import CORSConfig
cors_config = CORSConfig(
allow_origin='https://foo.example.com',
allow_headers=['X-Special-Header'],
max_age=600,
expose_headers=['X-Special-Header'],
allow_credentials=True
)
@app.route('/custom_cors', methods=['GET'], cors=cors_config)
def supports_custom_cors():
return {'cors': True}.. versionadded:: 0.8.1
.. attribute:: allow_origin
The value of the ``Access-Control-Allow-Origin`` to send in the response.
Keep in mind that even though the ``Access-Control-Allow-Origin`` header
can be set to a string that is a space separated list of origins, this
behavior does not work on all clients that implement CORS. You should only
supply a single origin to the ``CORSConfig`` object. If you need to supply
multiple origins you will need to define a custom handler for it that
accepts ``OPTIONS`` requests and matches the ``Origin`` header against a
whitelist of origins. If the match is successful then return just their
``Origin`` back to them in the ``Access-Control-Allow-Origin`` header... attribute:: allow_headers
The list of additional allowed headers. This list is added to list of
built in allowed headers: ``Content-Type``, ``X-Amz-Date``,
``Authorization``, ``X-Api-Key``, ``X-Amz-Security-Token``... attribute:: expose_headers
A list of values to return for the ``Access-Control-Expose-Headers``:.. attribute:: max_age
The value for the ``Access-Control-Max-Age``.. attribute:: allow_credentials
A boolean value that sets the value of
``Access-Control-Allow-Credentials``.Event Sources
.. versionadded:: 1.0.0b1
.. class:: Rate(value, unit)
An instance of this class can be used as the expression value
in the :meth:Chalice.schedule method:
.. code-block:: python
@app.schedule(Rate(5, unit=Rate.MINUTES))
def handler(event):
passExamples:
.. code-block:: python
# Run every minute.
Rate(1, unit=Rate.MINUTES)
# Run every 2 hours.
Rate(2, unit=Rate.HOURS).. attribute:: value
An integer value that presents the amount of time to wait
between invocations of the scheduled event... attribute:: unit
The unit of the provided ``value`` attribute. This can be
either ``Rate.MINUTES``, ``Rate.HOURS``, or ``Rate.DAYS``... attribute:: MINUTES, HOURS, DAYS
These values should be used for the ``unit`` attribute... class:: Cron(minutes, hours, day_of_month, month, day_of_week, year)
An instance of this class can be used as the expression value
in the :meth:Chalice.schedule method.
.. code-block:: python
@app.schedule(Cron(15, 10, '?', '*', '6L', '2002-2005'))
def handler(event):
pass It provides more capabilities than the :class:Rate
class. There are a few limits:
- You can't specify
day_of_monthandday_of_weekfields in
the same Cron expression. If you specify a value in one of the
fields, you must use a?in the other. - Cron expressions that lead to rates faster than 1 minute are not
supported.
For more information, see the API
docs page <https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#CronExpressions>__.
Examples:
.. code-block:: python
# Run at 10:00am (UTC) every day.
Cron(0, 10, '*', '*', '?', '*')
# Run at 12:15pm (UTC) every day.
Cron(15, 12, '*', '*', '?', '*')
# Run at 06:00pm (UTC) every Monday through Friday.
Cron(0, 18, '?', '*', 'MON-FRI', '*')
# Run at 08:00am (UTC) every 1st day of the month.
Cron(0, 8, 1, '*', '?', '*')
# Run every 15 minutes.
Cron('0/15', '*', '*', '*', '?', '*')
# Run every 10 minutes Monday through Friday.
Cron('0/10', '*', '?', '*', 'MON-FRI', '*')
# Run every 5 minutes Monday through Friday between
# 08:00am and 5:55pm (UTC).
Cron('0/5', '8-17', '?', '*', 'MON-FRI', '*').. class:: CloudWatchEvent()
This is the input argument for a scheduled or CloudWatch events.
.. code-block:: python
@app.schedule('rate(1 hour)')
def every_hour(event: CloudWatchEvent):
pass In the code example above, the event argument is of
type CloudWatchEvent, which will have the following
attributes.
.. attribute:: version
By default, this is set to 0 (zero) in all events... attribute:: account
The 12-digit number identifying an AWS account... attribute:: region
Identifies the AWS region where the event originated... attribute:: detail
For CloudWatch events this will be the event payload.
For scheduled events, this will be an empty dictionary... attribute:: detail_type
For scheduled events, this value will be ``"Scheduled Event"``... attribute:: source
Identifies the service that sourced the event. All events sourced from
within AWS will begin with "aws." Customer-generated events can have any
value here as long as it doesn't begin with "aws." We recommend the use
of java package-name style reverse domain-name strings.
For scheduled events, this will be ``aws.events``... attribute:: time
The event timestamp, which can be specified by the service originating
the event. If the event spans a time interval, the service might choose
to report the start time, so this value can be noticeably before the time
the event is actually received... attribute:: event_id
A unique value is generated for every event. This can be helpful in
tracing events as they move through rules to targets, and are processed... attribute:: resources
This JSON array contains ARNs that identify resources that are involved
in the event. Inclusion of these ARNs is at the discretion of the
service.
For scheduled events, this will include the ARN of the CloudWatch
rule that triggered this event... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda. This is useful if you need
the AWS request ID for tracing, or any other data in the context object... method:: to_dict()
Return the original event dictionary provided
from Lambda. This is useful if you need direct
access to the lambda event, for example if a
new key is added to the lambda event that has not
been mapped as an attribute to the ``CloudWatchEvent``
object. Example::
{'account': '123457940291',
'detail': {},
'detail-type': 'Scheduled Event',
'id': '12345678-b9f1-4667-9c5e-39f98e9a6113',
'region': 'us-west-2',
'resources': ['arn:aws:events:us-west-2:123457940291:rule/testevents-dev-every_minute'],
'source': 'aws.events',
'time': '2017-06-30T23:28:38Z',
'version': '0'}.. class:: S3Event()
This is the input argument for an S3 event.
.. code-block:: python
@app.on_s3_event(bucket='mybucket')
def event_handler(event: S3Event):
app.log.info("Event received for bucket: %s, key %s",
event.bucket, event.key) In the code example above, the event argument is of
type S3Event, which will have the following
attributes.
.. attribute:: bucket
The S3 bucket associated with the event... attribute:: key
The S3 key name associated with the event.
The original key name in the S3 event payload
is URL encoded. However, this ``key`` attribute automatically
URL decodes the key name for you. If you need
access to the original URL encoded key name, you can
access it through the ``to_dict()`` method... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda. This is useful if you need
the AWS request ID for tracing, or any other data in the context object... method:: to_dict()
Return the original event dictionary provided
from Lambda. This is useful if you need direct
access to the lambda event, for example if a
new key is added to the lambda event that has not
been mapped as an attribute to the ``S3Event``
object. Note that this event is not modified in any way.
This means that the key name of the S3 object is URL
encoded, which is the way that S3 sends this value
to Lambda... class:: SNSEvent()
This is the input argument for an SNS event handler.
.. code-block:: python
@app.on_sns_message(topic='mytopic')
def event_handler(event: SNSEvent):
app.log.info("Message received with subject: %s, message: %s",
event.subject, event.message) In the code example above, the event argument is of
type SNSEvent, which will have the following
attributes.
.. attribute:: subject
The subject of the SNS message that was published... attribute:: message
The string value of the SNS message that was published... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda. This is useful if you need
the AWS request ID for tracing, or any other data in the context object... method:: to_dict()
Return the original event dictionary provided
from Lambda. This is useful if you need direct
access to the lambda event, for example if a
new key is added to the lambda event that has not
been mapped as an attribute to the ``SNSEvent``
object... class:: SQSEvent()
This is the input argument for an SQS event handler.
.. code-block:: python
@app.on_sqs_message(queue='myqueue')
def event_handler(event: SQSEvent):
app.log.info("Event: %s", event.to_dict()) In the code example above, the event argument is of
type SQSEvent. An SQSEvent can have multiple
sqs messages associated with it. To access the multiple
messages, you can iterate over the SQSEvent.
.. method:: iter()
Iterate over individual SQS messages associated with
the event. Each element in the iterable is of type
:class:`SQSRecord`... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda. This is useful if you need
the AWS request ID for tracing, or any other data in the context object... method:: to_dict()
Return the original event dictionary provided
from Lambda. This is useful if you need direct
access to the lambda event, for example if a
new key is added to the lambda event that has not
been mapped as an attribute to the ``SQSEvent``
object... class:: SQSRecord()
Represents a single SQS record within an :class:SQSEvent.
.. attribute:: body
The body of the SQS message... attribute:: receipt_handle
The receipt handle associated with the message. This is useful
if you need to manually delete an SQS message to account for
partial failures... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda... method:: to_dict()
Return the original dictionary associated with the given
message. This is useful if you need direct
access to the lambda event... class:: KinesisEvent()
This is the input argument for a Kinesis data stream event handler.
.. code-block:: python
@app.on_kinesis_record(stream='mystream')
def event_handler(event: KinesisEvent):
app.log.info("Event: %s", event.to_dict()) In the code example above, the event argument is of
type KinesisEvent. A KinesisEvent can have multiple
messages associated with it. To access the multiple
messages, you can iterate over the KinesisEvent.
.. method:: iter()
Iterate over individual Kinesis records associated with
the event. Each element in the iterable is of type
:class:`KinesisRecord`... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda. This is useful if you need
the AWS request ID for tracing, or any other data in the context object... method:: to_dict()
Return the original event dictionary provided
from Lambda. This is useful if you need direct
access to the lambda event, for example if a
new key is added to the lambda event that has not
been mapped as an attribute to the ``SQSEvent``
object... class:: KinesisRecord()
Represents a single Kinesis record within a :class:KinesisEvent.
.. attribute:: data
The payload data for the Kinesis record. This data is automatically
base64 decoded for you and will be a ``bytes`` type... attribute:: sequence_number
The unique identifier of the record within its shard... attribute:: partition_key
Identifies which shard in the stream the data record is assigned to... attribute:: schema_version
Schema version for the record... attribute:: timestamp
The approximate time that the record was inserted into the stream. This
is automatically converted to a ``datetime.datetime`` object... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda... method:: to_dict()
Return the original dictionary associated with the given
message. This is useful if you need direct
access to the lambda event... class:: DynamoDBEvent()
This is the input argument for a DynamoDB stream event handler.
.. code-block:: python
@app.on_dynamodb_record(stream_arn='arn:aws:us-west-2:.../stream')
def event_handler(event: DynamoDBEvent):
app.log.info("Event: %s", event.to_dict())
for record in event:
app.log.info(record.to_dict()) In the code example above, the event argument is of
type DynamoDBEvent. A DynamoDBEvent can have multiple
messages associated with it. To access the multiple
messages, you can iterate over the DynamoDBEvent.
.. method:: iter()
Iterate over individual DynamoDB records associated with
the event. Each element in the iterable is of type
:class:`DynamoDBRecord`... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda. This is useful if you need
the AWS request ID for tracing, or any other data in the context object... method:: to_dict()
Return the original event dictionary provided
from Lambda. This is useful if you need direct
access to the lambda event, for example if a
new key is added to the lambda event that has not
been mapped as an attribute to the ``SQSEvent``
object... class:: DynamoDBRecord()
Represents a single DynamoDB record within a :class:DynamoDBEvent.
.. attribute:: timestamp
The approximate time that the record was the stream record was created.
This is automatically converted to a ``datetime.datetime`` object... attribute:: keys
The primary key attribute(s) for the DynamoDB item that was modified... attribute:: new_image
The item in the DynamoDB table as it appeared after it was modified... attribute:: old_image
The item in the DynamoDB table as it appeared before it was modified... attribute:: sequence_number
The sequence number of the stream record... attribute:: size_bytes
The size of the stream record, in bytes... attribute:: stream_view_type
The type of data from the modified DynamoDB item... attribute:: aws_region
The region associated with the event... attribute:: event_id
A unique identifier for the event... attribute:: event_name
The type of data modification that was performed on the DynamoDB table.
This can be: ``INSERT``, ``MODIFY``, or ``DELETE``... attribute:: event_source_arn
The ARN of the DynamoDB stream... attribute:: table_name
The name of the DynamoDB table associated with the stream. This value is
computed from the ``event_source_arn`` parameter and will be an empty string
if Chalice is unable to parse the table name from the event source ARN... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda... method:: to_dict()
Return the original dictionary associated with the given
message. This is useful if you need direct
access to the lambda event... class:: LambdaFunctionEvent()
This is the input argument of middleware registered to a
pure Lambda function (@app.lambda_function()).
.. attribute:: event
The original input event dictionary... attribute:: context
A `Lambda context object <https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html>`_
that is passed to the handler by AWS Lambda.Blueprints
.. class:: Blueprint(import_name)
An object used for grouping related handlers together.
This is primarily used as a mechanism for organizing your lambda
handlers. Any decorator methods defined in the :class:Chalice
object are also defined on a Blueprint object. You can register
a blueprint to a Chalice app using the :meth:Chalice.register_blueprint
method.
The import_name is the module in which the Blueprint is defined.
It is used to construct the appropriate handler string when creating
the Lambda functions associated with a Blueprint. This is typically
the __name__ attribute:mybp = Blueprint(__name__).
See :doc:topics/blueprints for more information.
.. code-block:: python
# In ./app.py
from chalice import Chalice
from chalicelib import myblueprint
app = Chalice(app_name='blueprints')
app.register_blueprint(myblueprint)
# In chalicelib/myblueprint.py
from chalice import Blueprint
myblueprint = Blueprint(__name__)
@myblueprint.route('/')
def index():
return {'hello': 'world'}Websockets
.. _websocket-api:
.. class:: WebsocketEvent()
Event object event that is passed as the sole argument to any handler
function decorated with one of the three websocket related handlers:
on_ws_connect, on_ws_disconnect, on_ws_message.
.. attribute:: domain_name
The domain name of the endpoint for the API Gateway Websocket API... attribute:: stage
The API Gateway stage of the Websocket API... attribute:: connection_id
A handle that uniquely identifies a connection with API Gateway... attribute:: body
The message body received. This is only populated on the ``on_ws_message``
otherwise it will be set to ``None``... attribute:: json_body
The parsed JSON body (``json.loads(body)``) of the message. If the body is
not JSON parsable then using this attribute will raise a ``ValueError``. See :doc:topics/websockets for more information.
.. _testing-api:
Testing
.. class:: Client(app, stage_name='dev', project_dir='.')
A test client used to write tests for Chalice apps. It allows you to
test Lambda function invocation as well as REST APIs. Depending
on what you want to test, you'll access the various attributes
of this class. You can use this class as a context manager.
When entering the context manager, any environment variables
specified for your function will be set. The original environment
variables are put back when the block is exited:
.. code-block:: python
from chalice.test import Client
with Client(app) as client:
result = client.http.post("/my-data") See the :doc:topics/testing documentation for more details on testing
your Chalice app.
.. attribute:: lambda_
Returns the Lambda test client :class:`TestLambdaClient`... attribute:: http
Returns the test client for REST APIs :class:`TestHTTPClient`... attribute:: events
Returns the test client for generating Lambda events
:class:`TestEventsClient`... class:: TestLambdaClient(import_name)
Test client for invoking Lambda functions. This class should not be
instantiated directly, and instead should be accessed via the
Client.lambda_ attribute:
.. code-block:: python
@app.lambda_function()
def myfunction(event, context):
return {"hello": "world"}
with Client(app) as client:
result = client.lambda_.invoke("myfunction")
assert result.payload == {"hello": "world"}.. method:: invoke(function_name, payload=None)
Invoke a Lambda function by name. The name should match
the resource name of the function. This is typically the
name of the python function unless an explicit ``name=``
kwarg is provided when registering the function.
Returns an :class:`InvokeResponse` instance... class:: TestHTTPClient(import_name)
Test client for REST APIs. This class should not be
instantiated directly, and instead should be accessed via the
Client.http attribute:
.. code-block:: python
with Client(app) as client:
response = client.http.get("/my-route").. method:: request(method, path, headers=None, body=b'')
Makes a test HTTP request to your REST API. Returns
an :class:`HTTPResponse`. You can also use the methods below
to make a request with a specific HTTP method instead of using
this method directly, e.g. ``client.http.get("/foo")`` instead
of ``client.http.request("GET", "/foo")``... method:: get(path, **kwargs)
Makes an HTTP GET request... method:: post(path, **kwargs)
Makes an HTTP POST request... method:: put(path, **kwargs)
Makes an HTTP PUT request... method:: patch(path, **kwargs)
Makes an HTTP PATCH request... method:: options(path, **kwargs)
Makes an HTTP OPTIONS request... method:: delete(path, **kwargs)
Makes an HTTP DELETE request... method:: head(path, **kwargs)
Makes an HTTP HEAD request... class:: TestEventsClient(import_name)
Test client for generating Lambda events. This class should not be
instantiated directly, and instead should be accessed via the
Client.events attribute:
.. code-block:: python
with Client(app) as client:
result = client.lambda_.invoke(
"my_sns_handler",
client.events.generate_sns_event("Hello world")
).. method:: generate_sns_event(message, subject='')
Generates a sample SNS event... method:: generate_s3_event(bucket, key, event_name='ObjectCreated:Put')
Generates a sample S3 event... method:: generate_sqs_event(message_bodies, queue_name='queue-name')
Generates a sample SQS event... method:: generate_cw_event(source, detail_type, detail, resources, region='us-west-2')
Generates a sample CloudWatch event... method:: generate_kinesis_event(message_bodies, stream_name='stream-name')
Generates a Kinesis event... class:: HTTPResponse()
.. attribute:: body
The body of the HTTP response, in ``bytes``... attribute:: headers
A dictionary of HTTP headers in the response... attribute:: status_code
The status code of the HTTP response... class:: InvokeResponse(payload)
.. attribute:: payload
The response payload of Lambda invocation... _cdk-api:
AWS CDK
The Chalice CDK construct is available in the chalice.cdk namespace.
For more details on using the AWS CDK with Chalice, see :doc:tutorials/cdk.
.. code-block:: python
from chalice import cdk
.. class:: cdk.Chalice(scope, id, source_dir, stage_config=None, preserve_logical_ids=True, **kwargs)
A test client used to write tests for Chalice apps. It allows you to
:param scope: The CDK scope that the construct is created within.
:param str id: The identifier for the construct. Must be unique within the
scope in which it's created.
:param str source_dir: Path to Chalice application source code.
:param dict stage_config: Chalice stage configuration.
The configuration object should have the same structure as Chalice
JSON stage configuration.
:param bool preserve_logical_ids: Whether the resources should have
the same logical IDs in the resulting CDK template as they did in
the original CloudFormation template file. If you're vending a
Construct using cdk-chalice, make sure to pass this as False.
Note: regardless of whether this option is true or false, the
:attr:sam_template's get_resource and related methods always
uses the original logical ID of the resource/element, as specified
in the template file.
:raises ChaliceError: Error packaging the Chalice application.
.. code-block:: python
chalice = Chalice(
self, 'ChaliceApp', source_dir='../runtime',
stage_config={
'environment_variables': {
'MY_ENV_VAR': 'FOO'
}
}
).. method:: get_resource(resource_name)
Returns a low-level CfnResource from the resources in a Chalice app with
the given resource name. The resource name corresponds to the logical ID
of the underlying resource in the SAM template. Any modifications
performed on that resource will be reflected in the resulting CDK
template.
:param str resource_name: The logical ID of the resource in the SAM template.
:rtype: aws_cdk.cdk.CfnResource.. method:: get_role(resource_name)
Returns an ``IRole`` for an underlying SAM template resource. This is useful
if you want to grant additional permissions to an IAM role constructed by Chalice.
.. code-block:: python
dynamodb_table = dynamodb.Table(...)
chalice = Chalice(scope, 'ChaliceApp', ....)
dynamodb_table.grant_read_write_data(chalice.get_role('DefaultRole'))
:param str resource_name: The logical ID of the resource in the SAM template.
:rtype: aws_cdk.aws_iam.IRole.. method:: get_function(resource_name)
Returns an ``IFunction`` for an underlying SAM template resource.
:param str resource_name: The logical ID of the resource in the SAM template.
:rtype: aws_cdk.aws_lambda.IFunction.. method:: add_environment_variable(key, value, function_name)
Convenience function to add environment variables to a single Lambda
function constructed by Chalice. You can also add environment variables
to Lambda functions using the ``stage_config`` parameter when creating the
``Chalice()`` construct.
:param str key: The environment variable key name.
:param str value: The value of the environment variable.
:param str function_name: The logical ID of the Lambda function resource.Source Faq (`docs/source/faq.rst`)
Frequently Asked Questions
Q: What is AWS Chalice?
AWS Chalice is a framework for writing serverless apps in python.
It consists of a CLI, a declarative python API for connecting
events to AWS Lambda functions, and a runtime component that
provides APIs accessible to your Lambda functions.
Q: Why should I use AWS Chalice?
Chalice is designed for a seamless getting started experience
that can get you up and running quickly. It handles all the
boilerplate and low level details of creating a serverless
application, allowing you to focus on the business logic
of your application. It also provides deep integration with
various AWS services allowing you to take advantage of the
features available in each service.
Q: How does Chalice compare to AWS SAM and the AWS CDK?
Chalice is designed to work together with AWS SAM.
SAM focus on provisioning the resources needed
for your application, and not necessarily on the application code
itself. Chalice provides a set of APIs to help you write your
application code, including a routing layer for REST and Websocket
APIs, and decorators to connect various AWS event sources to
Lambda functions. It then can integrate with AWS SAM by offloading
the deployment to AWS CloudFormation.
Q: How does Chalice compare to other similar frameworks?
The biggest difference between Chalice and other frameworks is that Chalice
is focused on using a familiar, decorator-based API to write serverless
python applications that run on AWS Lambda. Its goal is to make writing and
deploying these types of applications as simple as possible specifically for
Python developers. It was designed from the ground up to run in a
serverless environment.
To achieve this goal, it has to make certain tradeoffs. Chalice makes
assumptions about how applications will be deployed, and it has restrictions on
how an application can be structured. The feature set is purposefully small.
Source Index (`docs/source/index.rst`)
Documentation
Getting Started
.. toctree::
:maxdepth: 2
quickstart
Tutorials
.. toctree::
:maxdepth: 2
tutorials/index
Topics
.. toctree::
:maxdepth: 2
topics/index
API Reference
.. toctree::
:maxdepth: 2
api
Sample Apps
.. toctree::
:maxdepth: 2
samples/index
Upgrade Notes
.. toctree::
:maxdepth: 2
upgrading
FAQ
.. toctree::
:maxdepth: 1
faq
Indices and tables
- :ref:
genindex - :ref:
search
Source Quickstart (`docs/source/quickstart.rst`)
Quickstart
.. include:: ../../README.rst
:start-after: quick-start-begin
:end-before: quick-start-end
Cleaning Up
If you're done experimenting with Chalice and you'd like to cleanup, you can
use the chalice delete command, and Chalice will delete all the resources
it created when running the chalice deploy command.
::
$ chalice delete
Deleting Rest API: abcd4kwyl4
Deleting function arn:aws:lambda:region:123456789012:function:helloworld-dev
Deleting IAM Role helloworld-devNext Steps
At this point, there are several tutorials you can follow based on what
you're interested in:
- :doc:
Creating REST APIs <tutorials/basicrestapi>- Dive into more detail on
how to create a REST API using Chalice. We'll explore URL parameters, error
messages, content types, CORs, and more. - :doc:
Event Sources <tutorials/events>- In this tutorial, we'll focus on
difference event sources you can connect with a Lambda function other than a
REST API with Amazon API Gateway. - :doc:
Websockets <tutorials/wschat>- In this tutorial, we'll show you
how to create a websocket API and create a sample chat application.
You can also jump into specific :doc:topic guides <topics/index>. These are
more detailed than the tutorials, and provide more reference style
documentation on specific features of Chalice.
And finally, you can look at the :doc:API Reference <api> for detailed API
documentation for Chalice. This is useful if you know exactly what feature
you're using but need to lookup a specific parameter name or return value.
Source Upgrading (`docs/source/upgrading.rst`)
Upgrade Notes
This document provides additional documentation
on upgrading your version of chalice. If you're just
interested in the high level changes, see theCHANGELOG.md <https://github.com/aws/chalice/blob/master/CHANGELOG.md>__)
file.
.. _v1-2-0:
1.2.0
This release features a rewrite of the Chalice deployer
(#604 <https://github.com/aws/chalice/issues/604>__).
This is a backwards compatible change, and should not have any
noticeable changes with deployments with the exception of
fixing deployer bugs (e.g. https://github.com/aws/chalice/issues/604).
This code path affects the chalice deploy, chalice delete, andchalice package commands.
While this release is backwards compatible, you will notice several
changes when you upgrade to version 1.2.0.
The output of chalice deploy has changed in order to give
more details about the resources it creates along with a more detailed
summary at the end::
$ chalice deploy
Creating deployment package.
Creating IAM role: myapp-dev
Creating lambda function: myapp-dev-foo
Creating lambda function: myapp-dev
Creating Rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:12345:function:myapp-dev-foo
- Lambda ARN: arn:aws:lambda:us-west-2:12345:function:myapp-dev
- Rest API URL: https://abcd.execute-api.us-west-2.amazonaws.com/api/Also, the files used to store deployed values has changed. These files are
used internally by the chalice deploy/delete commands and you typically
do not interact with these files directly. It's mentioned here in case
you notice new files in your .chalice directory. Note that these files
are not part of the public interface of Chalice and are documented here
for completeness and to help with debugging issues.
In versions < 1.2.0, the value of deployed resources was stored in.chalice/deployed.json and looked like this::
{
"dev": {
"region": "us-west-2",
"api_handler_name": "demoauth4-dev",
"api_handler_arn": "arn:aws:lambda:us-west-2:123:function:myapp-dev",
"rest_api_id": "abcd",
"lambda_functions": {
"myapp-dev-foo": {
"type": "pure_lambda",
"arn": "arn:aws:lambda:us-west-2:123:function:myapp-dev-foo"
}
},
"chalice_version": "1.1.1",
"api_gateway_stage": "api",
"backend": "api"
},
"prod": {...}
}
In version 1.2.0, the deployed resources are split into multiple files, one
file per chalice stage. These files are in the.chalice/deployed/<stage.json>, so if you had a dev and a prod chalice
stage you'd have .chalice/deployed/dev.json and.chalice/deployed/prod.json. The schema has also changed and looks
like this::
$ cat .chalice/deployed/dev.json
{
"schema_version": "2.0",
"resources": [
{
"role_name": "myapp-dev",
"role_arn": "arn:aws:iam::123:role/myapp-dev",
"name": "default-role",
"resource_type": "iam_role"
},
{
"lambda_arn": "arn:aws:lambda:us-west-2:123:function:myapp-dev-foo",
"name": "foo",
"resource_type": "lambda_function"
},
{
"lambda_arn": "arn:aws:lambda:us-west-2:123:function:myapp-dev",
"name": "api_handler",
"resource_type": "lambda_function"
},
{
"name": "rest_api",
"rest_api_id": "abcd",
"rest_api_url": "https://abcd.execute-api.us-west-2.amazonaws.com/api",
"resource_type": "rest_api"
}
],
"backend": "api"
}
When you run chalice deploy for the first time after upgrading to version
1.2.0, chalice will automatically converted .chalice/deployed.json over to
the format as you deploy a given stage.
.. warning::
Once you upgrade to 1.2.0, chalice will only update the new
.chalice/deployed/<stage>.json. This means you cannot downgrade
to earlier versions of chalice unless you manually update
.chalice/deployed.json as well.
The chalice package command has also been updated to use the
deployer. This results in several changes compared to the previous
version:
- Pure lambdas are supported
- Scheduled events are supported
- Parity between the behavior of
chalice deployandchalice package
As part of this change, the CFN resource names have been updated
to use CamelCase names. Previously, chalice converted your
python function names to CFN resource names by removing all
non alphanumeric characters and appending an md5 checksum,
e.g my_function -> myfunction3bfc. With this new packager
update, the resource name would be converted asmy_function -> MyFunction. Note, the Outputs section
renames unchanged in order to preserve backwards compatibility.
In order to fix parity issues with chalice deploy andchalice package, we now explicitly create an IAM role
resource as part of the default configuration.
.. _v1-0-0b2:
1.0.0b2
The url parameter names and the function argument names must match.
Previously, the routing code would use positional args handler(*args)
to invoke a view function. In this version, kwargs are now used instead:handler(**view_args). For example, this code will no longer work:
.. code-block:: python
@app.route('/{a}/{b}')
def myview(first, second)
return {}The example above must be updated to:
.. code-block:: python
@app.route('/{a}/{b}')
def myview(a, b)
return {}Now that functions are invoked with kwargs, the order doesn't matter. You may
also write the above view function as:
.. code-block:: python
@app.route('/{a}/{b}')
def myview(b, a)
return {}This was done to have consistent behavior with other web frameworks such as
Flask.
.. _v1-0-0b1:
1.0.0b1
The Chalice.define_authorizer method has been removed. This has been
deprecated since v0.8.1. See :doc:topics/authorizers for updated
information on configuring authorizers in Chalice as well as the
original deprecation notice in the :ref:v0-8-1 upgrade notes.
The optional deprecated positional parameter in the chalice deploy command
for specifying the API Gateway stage has been removed. If you want to
specify the API Gateway stage, you can use the --api-gateway-stage
option in the chalice deploy command::
# Deprecated and removed in 1.0.0b1
$ chalice deploy prod
# Equivalent and updated way to specify an API Gateway stage:
$ chalice deploy --api-gateway-stage prod.. _v0-9-0:
0.9.0
The 0.9.0 release changed the type of app.current_request.raw_body to
always be of type bytes(). This only affects users that were using
python3. Previously you would get a type str(), but with the introduction
of binary content type support <https://github.com/aws/chalice/issues/348>__, the raw_body attribute
was made to consistently be of type bytes().
.. _v0-8-1:
0.8.1
The 0.8.1 changed the preferred way of specifying authorizers for view
functions. You now specify either an instance ofchalice.CognitoUserPoolAuthorizer or chalice.CustomAuthorizer
to an @app.route() function using the authorizer argument.
Deprecated:
.. code-block:: python
@app.route('/user-pools', methods=['GET'], authorizer_name='MyPool')
def authenticated():
return {"secure": True}
app.define_authorizer(
name='MyPool',
header='Authorization',
auth_type='cognito_user_pools',
provider_arns=['arn:aws:cognito:...:userpool/name']
)Equivalent, and preferred way
.. code-block:: python
from chalice import CognitoUserPoolAuthorizer
authorizer = CognitoUserPoolAuthorizer(
'MyPool', header='Authorization',
provider_arns=['arn:aws:cognito:...:userpool/name'])
@app.route('/user-pools', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"secure": True}The define_authorizer is still available, but is now deprecated and will
be removed in future versions of chalice. You can also use the newauthorizer argument to provider a CustomAuthorizer:
.. code-block:: python
from chalice import CustomAuthorizer
authorizer = CustomAuthorizer(
'MyCustomAuth', header='Authorization',
authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-01'
'/functions/arn:aws:lambda:region:account-id:'
'function:FunctionName/invocations'))
@app.route('/custom-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"secure": True}.. _v0-7-0:
0.7.0
The 0.7.0 release adds several major features to chalice. While the majority
of these features are introduced in a backwards compatible way, there are a few
backwards incompatible changes that were made in order to support these new
major features.
Separate Stages
Prior to this version, chalice had a notion of a "stage" that corresponded to
an API gateway stage. You can create and deploy a new API gateway stage by
running ``chalice deploy <stage-name>``. In 0.7.0, stage support was been
reworked such that a chalice stage is a completely separate set of AWS
resources. This means that if you have two chalice stages, say ``dev`` and
``prod``, then you will have two separate sets of AWS resources, one set per
stage:
* Two API Gateway Rest APIs
* Two separate Lambda functions
* Two separate IAM roles
The :doc:`topics/stages` doc has more details on the new chalice stages
feature. This section highlights the key differences between the old stage
behavior and the new chalice stage functionality in 0.7.0. In order to ease
transition to this new model, the following changes were made:
* A new ``--stage`` argument was added to the ``deploy``, ``logs``, ``url``,
``generate-sdk``, and ``package`` commands. If this value is specified
and the stage does not exist, a new chalice stage with that name will
be created for you.
* The existing form ``chalice deploy <stage-name>`` has been deprecated.
The command will still work in version 0.7.0, but a deprecation warning
will be printed to stderr.
* If you want the pre-existing behavior of creating a new API gateway stage
(while using the same Lambda function), you can use the
``--api-gateway-stage`` argument. This is the replacement for the
deprecated form ``chalice deploy <stage-name>``.
* The default stage if no ``--stage`` option is provided is ``dev``. By
defaulting to a ``dev`` stage, the pre-existing behavior of not
specifying a stage name, e.g ``chalice deploy``, ``chalice url``, etc.
will still work exactly the same.
* A new ``stages`` key is supported in the ``.chalice/config.json``. This
allows you to specify configuration specific to a chalice stage.
See the :doc:`topics/configfile` doc for more information about stage
specific configuration.
* Setting ``autogen_policy`` to false will result in chalice looking
for a IAM policy file named ``.chalice/policy-<stage-name>.json``.
Previously it would look for a file named ``.chalice/policy.json``.
You can also explicitly set this value to
In order to ease transition, chalice will check for a
``.chalice/policy.json`` file when deploying to the ``dev`` stage.
Support for ``.chalice/policy.json`` will be removed in future
versions of chalice and users are encouraged to switch to the
stage specific ``.chalice/policy-<stage-name>.json`` files.
See the :doc:`topics/stages` doc for more details on the new chalice stages
feature.
**Note, the AWS resource names it creates now have the form
``<app-name>-<stage-name>``, e.g. ``myapp-dev``, ``myapp-prod``.**
We recommend using the new stage specific resource names. However, If you
would like to use the existing resource names for a specific stage, you can
create a ``.chalice/deployed.json`` file that specifies the existing values::
{
"dev": {
"backend": "api",
"api_handler_arn": "lambda-function-arn",
"api_handler_name": "lambda-function-name",
"rest_api_id": "your-rest-api-id",
"api_gateway_stage": "dev",
"region": "your region (e.g us-west-2)",
"chalice_version": "0.7.0",
}
}
This file is discussed in the next section.
Deployed ValuesIn version 0.7.0, the way deployed values are stored and retrieved
has changed. In prior versions, only the lambda_arn was saved,
and its value was written to the .chalice/config.json file.
Any of other deployed values that were needed (for example the
API Gateway rest API id) was dynamically queried by assuming the
resource names matches the app name. In this version of chalice,
a separate .chalice/deployed.json file is written on every
deployment which contains all the resources that have been created.
While this should be a transparent change, you may noticed
issues if you run commands such as chalice url and chalice logs
without first deploying. To fix this issue, run chalice deploy
and version 0.7.0 of chalice so a .chalice/deployed.json will
be created for you.
Authorizer Changes
**The ``authorizer_id`` and ``authorization_type`` args are
no longer supported in ``@app.route(...)`` calls.**
They have been replaced with an ``authorizer_name`` parameter and an
``app.define_authorizer`` method.
This version changed the internals of how an API gateway REST API is created.
Prior to 0.7.0, the AWS SDK for Python was used to make the appropriate service
API calls to API gateway include ``create_rest_api`` and ``put_method /
put_method_response`` for each route. In version 0.7.0, this internal
mechanism was changed to instead generate a swagger document. The rest api is
then created or updated by calling ``import_rest_api`` or ``put_rest_api`` and
providing the swagger document. This simplifies the internals and also unifies
the code base for the newly added ``chalice package`` command (which uses a
swagger document internally). One consequence of this change is that the
entire REST API must be defined in the swagger document. With the previous
``authorizer_id`` parameter, you would create/deploy a rest api, create your
authorizer, and then provide that ``authorizer_id`` in your ``@app.route``
calls. Now they must be defined all at once in the ``app.py`` file:
.. code-block:: python
app = chalice.Chalice(app_name='demo')
@app.route('/auth-required', authorizer_name='MyUserPool')
def foo():
return {}
app.define_authorizer(
name='MyUserPool',
header='Authorization',
auth_type='cognito_user_pools',
provider_arns=['arn:aws:cognito:...:userpool/name']
)
.. _v0-6-0:
0.6.0
-----
This version changed how the internals of how API gateway resources are created
by chalice. The integration type changed from ``AWS`` to ``AWS_PROXY``. This
was to enable additional functionality, notable to allows users to provide
non-JSON HTTP responses and inject arbitrary headers to the HTTP responses.
While this change to the internals is primarily internal, there are several
user-visible changes.
* Uncaught exceptions with ``app.debug = False`` (the default value)
will result in a more generic ``InternalServerError`` error. The
previous behavior was to return a ``ChaliceViewError``.
* When you enabled debug mode via ``app.debug = True``, the HTTP
response will contain the python stack trace as the entire request
body. This is to improve the readability of stack traces.
For example::
$ http https://endpoint/dev/
HTTP/1.1 500 Internal Server Error
Content-Length: 358
Content-Type: text/plain
Traceback (most recent call last):
File "/var/task/chalice/app.py", line 286, in __call__
response = view_function(*function_args)
File "/var/task/app.py", line 12, in index
return a()
File "/var/task/app.py", line 16, in a
return b()
File "/var/task/app.py", line 19, in b
raise ValueError("Hello, error!")
ValueError: Hello, error!
* Content type validation now has error responses that match the same error
response format used for other chalice built in responses. Chalice was
previously relying on API gateway to perform the content type validation.
As a result of the ``AWS_PROXY`` work, this logic has moved into the chalice
handler and now has a consistent error response::
$ http https://endpoint/dev/ 'Content-Type: text/plain'
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json
{
"Code": "UnsupportedMediaType",
"Message": "Unsupported media type: text/plain"
}
* The keys in the ``app.current_request.to_dict()`` now match the casing used
by the ``AWS_PPROXY`` lambda integration, which are ``lowerCamelCased``.
This method is primarily intended for introspection purposes.
---
### Source Samples Index (`docs/source/samples/index.rst`)
Sample Applications
===================
Below are a collection of Chalice sample applications. They
show you how you can write more real-world serverless applications.
The code for all of these sample applications is available in
the `Chalice repository on GitHub
<https://github.com/aws/chalice/tree/master/docs/source/samples>`__.
For each of these sample apps, we'll cover what is does,
the architecture of the app, how to deploy and test the app,
and we'll walk through the key parts of the application code.
:doc:`todo-app/index`
This app is a REST API that manages Todo
items. These items are stored in an Amazon DynamoDB database.
The REST API is protected with JWT auth. We show you how to
implement auth and login with Chalice's :ref:`builtin-authorizers`.
:doc:`media-query/index`
This app shows how to create an image
processing pipeline that can analyze images and videos to detect
real world objects. The results of this analysis are then
stored in a database and exposed through a queryable REST API.
.. toctree::
:hidden:
:glob:
./*/index
---
### Source Topics Authorizers (`docs/source/topics/authorizers.rst`)
Authorization
=============
Chalice supports multiple mechanisms for authorization. This topic
covers how you can integrate authorization into your Chalice applications.
In Chalice, all the authorizers are configured per-route and specified
using the ``authorizer`` kwarg to an ``@app.route()`` call. You
control which type of authorizer to use based on what's passed as the
``authorizer`` kwarg. You can use the same authorizer instance for
multiple routes.
The first set of authorizers chalice supports cover the scenario where
you have some existing authorization mechanism that you just want your
Chalice app to use.
Chalice also supports built-in authorizers, which allows Chalice to
manage your custom authorizers as part of ``chalice deploy``. This is
covered in the Built-in Authorizers section.
AWS IAM Authorizer
------------------
The IAM Authorizer allows you to control access to API Gateway with
`IAM permissions`_
To associate an IAM authorizer with a route in chalice, you use the
:class:`IAMAUthorizer` class:
.. code-block:: python
from chalice import IAMAuthorizer
authorizer = IAMAuthorizer()
@app.route('/iam-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"success": True}
See the `API Gateway documentation
<https://docs.aws.amazon.com/apigateway/latest/developerguide/permissions.html>`__
for more information on controlling access to API Gateway with IAM permissions.
Amazon Cognito User Pools
-------------------------
In addition to using IAM roles and policies with the :class:`IAMAuthorizer` you
can also use a `Cognito user pools`_ to control who can access your Chalice
app. A cognito user pool serves as your own identity provider to maintain a
user directory.
To integrate Cognito user pools with Chalice, you'll need to have an existing
cognito user pool configured.
.. code-block:: python
from chalice import CognitoUserPoolAuthorizer
authorizer = CognitoUserPoolAuthorizer(
'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name'])
@app.route('/user-pools', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"success": True}
For more information about using Cognito user pools with API Gateway,
see the `Use Amazon Cognito User Pools documentation
<https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html>`__.
Custom Authorizers
------------------
API Gateway also lets you write custom authorizers using a Lambda function.
You can configure a Chalice route to use a pre-existing Lambda function as
a custom authorizer. If you also want to write and manage your Lambda
authorizer using Chalice, see the next section, Built-in Authorizers.
To connect an existing Lambda function as a custom authorizer in chalice,
you use the ``CustomAuthorizer`` class:
.. code-block:: python
from chalice import CustomAuthorizer
authorizer = CustomAuthorizer(
'MyCustomAuth', header='Authorization',
authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-31'
'/functions/arn:aws:lambda:region:account-id:'
'function:FunctionName/invocations'))
@app.route('/custom-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"success": True}
.. _builtin-authorizers:
Built-in Authorizers
--------------------
The ``IAMAuthorizer``, ``CognitoUserPoolAuthorizer``, and the
``CustomAuthorizer`` classes are all for cases where you have existing
resources for managing authorization and you want to wire them together with
your Chalice app. A Built-in authorizer is used when you'd like to write your
custom authorizer in Chalice, and have the additional Lambda functions managed
when you run ``chalice deploy/delete``. This section will cover how to use the
built-in authorizers in chalice.
Creating an authorizer in chalice requires you use the ``@app.authorizer``
decorator to a function. The function must accept a single arg, which will be
an instance of :class:`AuthRequest`. The function must return a
:class:`AuthResponse`. As an example, we'll port the example from the `API
Gateway documentation`_. First, we'll show the code and then walk through it:
.. code-block:: python
from chalice import Chalice, AuthResponse
app = Chalice(app_name='demoauth1')
@app.authorizer()
def demo_auth(auth_request):
token = auth_request.token
# This is just for demo purposes as shown in the API Gateway docs.
# Normally you'd call an oauth provider, validate the
# jwt token, etc.
# In this example, the token is treated as the status for demo
# purposes.
if token == 'allow':
return AuthResponse(routes=['/'], principal_id='user')
else:
# By specifying an empty list of routes,
# we're saying this user is not authorized
# for any URLs, which will result in an
# Unauthorized response.
return AuthResponse(routes=[], principal_id='user')
@app.route('/', authorizer=demo_auth)
def index():
return {'context': app.current_request.context}
In the example above we define a built-in authorizer by decorating
the ``demo_auth`` function with the ``@app.authorizer()`` decorator.
Note you must use ``@app.authorizer()`` and not ``@app.authorizer``.
A built-in authorizer function has this type signature::
def auth_handler(auth_request: AuthRequest) -> AuthResponse: ...
Within the auth handler you must determine if the request is
authorized or not. The ``AuthResponse`` contains the allowed
URLs as well as the principal id of the user. You can optionally
return a dictionary of key value pairs (as the ``context`` kwarg).
This dictionary will be passed through on subsequent requests.
In our example above we're not using the context dictionary.
API Gateway will convert all the values in the ``context``
dictionary to string values.
Now let's deploy our app. As usual, we just need to run
``chalice deploy`` and chalice will automatically deploy all the
necessary Lambda functions for us.
Now when we try to make a request, we'll get an Unauthorized error::
$ http https://api.us-west-2.amazonaws.com/api/
HTTP/1.1 401 Unauthorized
{
"message": "Unauthorized"
}
If we add the appropriate authorization header, we'll see the call succeed::
$ http https://api.us-west-2.amazonaws.com/api/ 'Authorization: allow'
HTTP/1.1 200 OK
{
"context": {
"accountId": "12345",
"apiId": "api",
"authorizer": {
"principalId": "user"
},
"httpMethod": "GET",
"identity": {
"accessKey": null,
"accountId": null,
"apiKey": "",
"caller": null,
"cognitoAuthenticationProvider": null,
"cognitoAuthenticationType": null,
"cognitoIdentityId": null,
"cognitoIdentityPoolId": null,
"sourceIp": "1.1.1.1",
"user": null,
"userAgent": "HTTPie/0.9.9",
"userArn": null
},
"path": "/api/",
"requestId": "d35d2063-56be-11e7-9ce1-dd61c24a3668",
"resourceId": "id",
"resourcePath": "/",
"stage": "dev"
}
}
The low level API for API Gateway's custom authorizer feature requires
that an IAM policy must be returned. The :class:`AuthResponse` class we're
using is a wrapper over building the IAM policy ourselves. If you want
low level control and would prefer to construct the IAM policy yourself
you can return a dictionary of the IAM policy instead of an instance of
:class:`AuthResponse`. If you do that, the dictionary is returned
without modification back to API Gateway.
For more information on custom authorizers, see the
`Use API Gateway Custom Authorizers
<https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html>`__
page in the API Gateway user guide.
Scopes
-------------------------
OAuth 2.0 and OpenID Connect (OIDC) scopes can be used to implement access
controls in your Chalice app. Scopes are supported when using the Cognito
Authorizer, Custom Authorizers, and Built-In Authorizers.
To integrate Scopes with a Cognito Authorizer in Chalice, you'll need to have
an existing `Cognito user pools`_ and `Cognito resource server`_ configured.
Scopes for Cognito Authorizers need to include the full identifier
which is ``resourceServerIdentifier/scopeName``.
Scopes can be configured per-authorizer using the ``scopes`` attribute.
.. code-block:: python
from chalice import CognitoUserPoolAuthorizer
authorizer = CognitoUserPoolAuthorizer(
'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name'],
scopes=["https://mychaliceapp.example.com/todos.read"])
@app.route('/user-pools', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"success": True}
Scopes can be configured per-route for an Authorizer using ``with_scopes``.
.. code-block:: python
from chalice import CognitoUserPoolAuthorizer
authorizer = CognitoUserPoolAuthorizer(
'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name'])
@app.route(
'/user-pools',
methods=['GET'],
authorizer=authorizer.with_scopes(["https://mychaliceapp.example.com/todos.read"]))
def authenticated():
return {"success": True}
Scopes can also be used with custom authorizers and built-in authorizers.
These authorizers will need to inspect the access token to determine if access
should be granted based on the scopes configured for the authorizer and route.
.. _IAM permissions: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_controlling.html
.. _Cognito User Pools: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html
.. _Cognito Resource Server: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-define-resource-servers.html
.. _API Gateway documentation: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
---
### Source Topics Blueprints (`docs/source/topics/blueprints.rst`)
Blueprints
==========
Chalice blueprints are used to organize your application into logical
components. Using a blueprint, you define your resources and decorators in
modules outside of your ``app.py``. You then register a blueprint in your main
``app.py`` file. Blueprints support any decorator available on an application
object.
.. note::
The Chalice blueprints are conceptually similar to `Blueprints
<https://flask.palletsprojects.com/blueprints/>`__ in Flask. Flask
blueprints allow you to define a set of URL routes separately from the main
``Flask`` object. This concept is extended to all resources in Chalice. A
Chalice blueprint can have Lambda functions, event handlers, built-in
authorizers, etc. in addition to a collection of routes.
Example
-------
In this example, we'll create a blueprint with part of our routes defined in a
separate file. First, let's create an application::
$ chalice new-project blueprint-demo
$ cd blueprint-demo
$ mkdir chalicelib
$ touch chalicelib/__init__.py
$ touch chalicelib/blueprints.py
Next, we'll open the ``chalicelib/blueprints.py`` file:
.. code-block:: python
from chalice import Blueprint
extra_routes = Blueprint(__name__)
@extra_routes.route('/foo')
def foo():
return {'foo': 'bar'}
The ``__name__`` is used to denote the import path of the blueprint. This name
must match the import name of the module so the function can be properly
imported when running in Lambda. We'll now import this module in our
``app.py`` and register this blueprint. We'll also add a route in our
``app.py`` directly:
.. code-block:: python
from chalice import Chalice
from chalicelib.blueprints import extra_routes
app = Chalice(app_name='blueprint-demo')
app.register_blueprint(extra_routes)
@app.route('/')
def index():
return {'hello': 'world'}
At this point, we've defined two routes. One route, ``/``, is directly defined
in our ``app.py`` file. The other route, ``/foo`` is defined in
``chalicelib/blueprints.py``. It was added to our Chalice app when we
registered it via ``app.register_blueprint(extra_routes)``.
We can deploy our application to verify this works as expected::
$ chalice deploy
Creating deployment package.
Creating IAM role: blueprint-demo-dev
Creating lambda function: blueprint-demo-dev
Creating Rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:1234:function:blueprint-demo-dev
- Rest API URL: https://rest-api.execute-api.us-west-2.amazonaws.com/api/
We should now be able to request the ``/`` and ``/foo`` routes::
$ http https://rest-api.execute-api.us-west-2.amazonaws.com/api/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 17
Content-Type: application/json
Date: Sat, 22 Dec 2018 01:05:48 GMT
Via: 1.1 5ab5dc09da67e3ea794ec8a82992cc89.cloudfront.net (CloudFront)
X-Amz-Cf-Id: Cdsow9--fnTH5EdjkjWBMWINCCMD4nGmi4S_3iMYMK0rpc8Mpiymgw==
X-Amzn-Trace-Id: Root=1-5c1d8dec-f1ef3ee83c7c654ca7fb3a70;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: SSMc6H_yvHcFcEw=
x-amzn-RequestId: b7bd0c87-0585-11e9-90cf-59b71c1a1de1
{
"hello": "world"
}
$ http https://rest-api.execute-api.us-west-2.amazonaws.com/api/foo
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 13
Content-Type: application/json
Date: Sat, 22 Dec 2018 01:05:51 GMT
Via: 1.1 95b0ac620fa3a80ee590ecf1cda1c698.cloudfront.net (CloudFront)
X-Amz-Cf-Id: HX4l1BNdWvYDRXan17PFZya1vaomoJel4rP7d8_stdw2qT50v7Iybg==
X-Amzn-Trace-Id: Root=1-5c1d8def-214e7f681ff82c00fd81f37a;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: SSMdXF40vHcF-mg=
x-amzn-RequestId: b96f77bf-0585-11e9-b229-01305cd40040
{
"foo": "bar"
}
Blueprint Registration
----------------------
The ``app.register_blueprint`` function accepts two optional arguments,
``name_prefix`` and ``url_prefix``. This allows you to register the resources
in your blueprint at a certain url and name prefix. If you specify
``url_prefix``, any routes defined in your blueprint will have the
``url_prefix`` prepended to it. If you specify the ``name_prefix``, any Lambda
functions created will have the ``name_prefix`` prepended to the resource name.
.. note::
The ``name_prefix`` parameter does not apply to the Lambda function
associated with API Gateway, which is anything decorated with
``@app.route()``.
Advanced Example
----------------
Let's create a more advanced example. If this application, let's say we want
to organize our application into separate modules for our API and our event
sources. We can create an app with these files::
$ ls -la chalicelib/
__init__.py
api.py
events.py
The contents of ``api.py`` are:
.. code-block:: python
from chalice import Blueprint
myapi = Blueprint(__name__)
@myapi.route('/')
def index():
return {'hello': 'world'}
@myapi.route('/foo')
def index():
return {'foo': 'bar'}
The contents of ``events.py`` are:
.. code-block:: python
from chalice import Blueprint
myevents = Blueprint(__name__)
@myevents.schedule('rate(5 minutes)')
def cron(event):
pass
@myevents.on_sns_message('MyTopic')
def handle_sns_message(event):
pass
In our ``app.py`` we'll register these blueprints:
.. code-block:: python
from chalice import Chalice
from chalicelib.events import myevents
from chalicelib.api import myapi
app = Chalice(app_name='blueprint-demo')
app.register_blueprint(myevents)
app.register_blueprint(myapi)
Now our ``app.py`` only registers the necessary blueprints, and all our
resources are defined in blueprints.
---
### Source Topics Cd (`docs/source/topics/cd.rst`)
===========================
Continuous Deployment (CD)
===========================
Chalice can be used to set up a basic Continuous Deployment pipeline. The
``chalice deploy`` command is good for getting up and running quickly with
Chalice, but in a team environment properly managing permissions and sharing
and updating the ``deployed.json`` file will get messy.
One way to scale up your chalice app is to create a continuous deployment
pipeline. The pipeline can run tests on code changes and, if they pass, promote
the new build to a testing stage. More checks can be put in place to manually
promote a build to production, or you can do so automatically. This model
greatly simplifies managing what resources belong to your Chalice app as they
are all stored in the Continuous Deployment pipeline.
Chalice can generate a CloudFormation template that will create a starter CD
pipeline. By default it contains an AWS CodeCommit repo, an AWS CodeBuild stage
for packaging your chalice app, and an AWS CodePipeline stage to deploy your
application using CloudFormation.
You can also configure a source repository hosted on GitHub instead of
a CodeCommit repository.
Pipeline Template Versions
==========================
This starter pipeline template can be generated using the ``generate-pipeline``
command. There are two versions of this pipeline. The older ``v1`` template
is the default (for backwards compatibility reasons), but the newer template
version, ``v2``, is recommended. The version can be specified using the
``--pipeline-version`` option. These are the differences between ``v1`` and
``v2`` templates:
* The ``v1`` templates use version ``0.1`` of the CodeBuild buildspec, whereas
``v2`` uses ``0.2`` of the CodeBuild buildspec. Buildspec ``0.2`` is the
recommended version to use with CodeBuild. See their
`documentation <https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html>`__
for more information.
* The ``v2`` template uses `AWS Secrets Manager <https://aws.amazon.com/secrets-manager/>`__
to configure access to a GitHub repository.
* The ``v2`` buildspec uses `runtime-versions <https://docs.aws.amazon.com/codebuild/latest/userguide/build-spec-ref.html#build-spec.phases.install.runtime-versions>`__
to configure which version of Python to use instead of a Python
version specific CodeBuild image. For ``v2`` templates the
``aws/codebuild/amazonlinux2023-x86_64-standard`` image.
**The v2 pipeline template requires Python 3.10 or higher.** If you're using
Python versions less than 3.10 you must use the ``v1`` pipeline template.
Usage example
=============
Setting up the deployment pipeline is a two step process. First use the
``chalice generate-pipeline`` command to generate a base CloudFormation
template. Second use the AWS CLI to deploy the CloudFormation template using
the ``aws cloudformation deploy`` command. Below is an example.
::
$ chalice generate-pipeline --pipeline-version v2 pipeline.json
$ aws cloudformation deploy --stack-name mystack
--template-file pipeline.json --capabilities CAPABILITY_IAM
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - mystack
.. note::
To configure your Chalice app to use a GitHub repository instead of
CodeCommit see the :ref:`cicd-github-repo` section below.
Once the CloudFormation template has finished creating the stack, you will have
several new AWS resources that make up a bare bones CD pipeline.
* **CodeCommit Repository** - The `CodeCommit <https://aws.amazon.com/codecommit/>`_
repository is the entrypoint into the pipeline. Any code you want to deploy
should be pushed to this remote.
* **CodePipeline Pipeline** - The
`CodePipeline <https://aws.amazon.com/codepipeline/>`_ is what coordinates
the build process, and pushes the released code out.
* **CodeBuild Project** - The `CodeBuild <https://aws.amazon.com/codebuild/>`_
project is where the code bundle is built that will be pushed to Lambda. The
default CloudFormation template will create a CodeBuild stage that builds
a package using ``chalice package`` and then uploads those artifacts for
CodePipeline to deploy.
* **S3 Buckets** - Two S3 buckets are created on your behalf.
* **artifactbucketstore** - This bucket stores artifacts that are built by
the CodeBuild project. The only artifact by default is the
``transformed.yaml`` created by the ``aws cloudformation package`` command.
* **applicationbucket** - Stores the application bundle after the Chalice
application has been packaged in the CodeBuild stage.
* Each resource is created with all the required IAM roles and policies.
CodeCommit repository
---------------------
The CodeCommit repository can be added as a git remote for deployment. This
makes it easy to kick off deployments. The developer doing the deployment only
needs to push the release code up to the CodeCommit repository master branch.
All the developer needs is keys that allow for push access to the CodeCommit
repository. This is a lot easier than managing a set of ``deployed.json``
resources across a repsoitory and manually doing ``chalice deploy`` whenever
a change needs to be deployed.
The default CodeCommit repository that is created is empty, you will have to
populate it with the Chalice application code. Permissions will also need to be
set up, you can find the documentation on how to do that
`here <https://docs.aws.amazon.com/codebuild/latest/userguide/setting-up.html>`_
.
You can retrieve the CodeCommit clone URL by searching for the
``SourceRepoURL`` in the CloudFormation stack output::
$ aws cloudformation describe-stacks --stack-name mystack \
--query "Stacks[0].Outputs[?OutputKey=='SourceRepoURL'] | [0].OutputValue"
CodePipeline
------------
CodePipeline is the main coordinator between all the other resources. It
watches for changes on the CodeCommit repository, and triggers builds in the
CodeBuild project. If the build succeeds then it will start a CloudFormation
deployment of the built artifacts to a beta stage. This should be treated as
a starting point, not a fully featured CD system.
CodeBuild build script
----------------------
By default Chalice will create the CodeBuild project with a default buildspec
that does the following.
.. code-block:: yaml
version: 0.1
phases:
install:
commands:
- sudo pip install --upgrade awscli
- aws --version
- sudo pip install chalice
- sudo pip install -r requirements.txt
- chalice package /tmp/packaged
- aws cloudformation package --template-file
/tmp/packaged/sam.json --s3-bucket ${APP_S3_BUCKET}
--output-template-file transformed.yaml
artifacts:
type: zip
files:
- transformed.yaml
The CodeBuild stage installs both the AWS CLI and Chalice, then creates a
package out of your chalice project, pushing the package to the application
S3 bucket that was created for you. The transformed CloudFormation template
is the only artifact, and can be run by CodePipeline after the build has
succeeded.
Deploying to beta stage
-----------------------
Once the CodeBuild stage has finished building the Chalice package and
creating the ``transformed.yaml``, CodePipeline will take these artifacts and
use them to create or update the beta stage. The ``transformed.yaml``
is a CloudFormation template that CodePipeline will execute, all the code it
references has been uploaded to the application bucket by the AWS CLI in the
CodeBuild stage, so this is the only artifact we need.
Once the CodePipeline beta build stage is finished, the beta version of the app
is deployed and ready for testing.
Extending
---------
It is recommended to use this pipeline as a starting point. The default
template does not run any tests on the Chalice app before deploying to beta.
There is also no mechanism provided by Chalice for a production stage.
Ideally the CodeBuild stage would be used to run unit and functional tests
before deploying to beta. After the beta stage is up, integration tests can be
run against that endpoint, and if they all pass the beta stage could be
promoted to a production stage using the CodePipeline manual approval feature.
.. _cicd-github-repo:
Configuring a GitHub Repository
===============================
You can configure a GitHub repository instead of a CodeCommit repo when
setting up your deployment pipeline by specifying the ``--source github``
option. When generating a CloudFormation template for a GitHub repository,
there are several parameters that are added to your template that allow
you to configure how to connect your GitHub repository with your CodePipeline.
You must store your OAuth token that enables access to a GitHub repository
in AWS Secrets Manager. You then specify the secret name/id and the JSON
key name as CloudFormation parameters. These values default to a secret
name of ``GithubRepoAccess`` and a JSON key name of ``OAuthToken``.
Below is an example of how to configure a GitHub repository as the
source for your deployment pipeline.
First create a `GitHub token <https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token>`__
that can be used in this template. Next create a secret in AWS Secrets
Manager. You can either follow the documentation
`here <https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html>`__
or use the AWS CLI or any AWS SDK. For this example, we'll use the AWS CLI
to create our secret. Create a file named ``/tmp/secrets.json`` with these
contents::
{"OAuthToken": "abcdefghhijklmnop"}
Be sure to replace the value of ``OAuthToken`` with the value of your GitHub
token you created. Next we can create the secret using this command::
$ aws secretsmanager create-secret --name GithubRepoAccess \
--description "Token for Github Repo Access" \
--secret-string file:///tmp/secrets.json
Now we can generate our deployment pipeline::
$ chalice generate-pipeline --pipeline-version v2 \
--source github --buildspec-file buildspec.yml pipeline.json
This will create two files, a ``pipeline.json`` file containing our
deployment pipeline and a ``buildspec.yml`` file. This buildspec file
lets us update what commands should be run as part of our build process
without having to redeploy our CloudFormation template.
We now add and commit our changes to our repository.
::
$ git add buildspec.yml pipeline.json
$ git commit -m "Add deployment pipeline template"
$ git push
Now we're ready to deploy our CloudFormation template using the AWS CLI. Be
sure to replace the ``GithubOwner`` and ``GithubRepoName`` with your own
values for your GitHub repository. You'll also need to specify the
``GithubRepoSecretId`` and ``GithubRepoSecretJSONKey`` if you used values
other than the default values of ``GithubRepoAccess`` and ``OAuthToken`` when
creating your secret in Secrets Manager.
::
$ aws cloudformation deploy --template-file pipeline.json \
--stack-name MyChaliceApp --parameter-overrides \
GithubOwner=repo-owner-name \
GithubRepoName=repo-name \
--capabilities CAPABILITY_IAM
We've now created a deployment pipeline that will automatically deploy our
Chalice app whenever we push to our GitHub repository.
---
### Source Topics Cfn (`docs/source/topics/cfn.rst`)
AWS CloudFormation Support
==========================
When you run ``chalice deploy``, chalice will deploy your application using the
`AWS SDK for Python <http://boto3.readthedocs.io/en/docs/>`__). Chalice also
provides functionality that allows you to manage deployments yourself using
cloudformation. This is provided via the ``chalice package`` command.
When you run this command, chalice will generate the AWS Lambda deployment
package that contains your application as well as a `Serverless Application
Model (SAM) <https://github.com/awslabs/serverless-application-model>`__
template. You can then use a tool like the AWS CLI, or any cloudformation
deployment tools you use, to deploy your chalice application.
Considerations
--------------
Using the ``chalice package`` command is useful when you don't want to
use ``chalice deploy`` to manage your deployments. There's several reasons
why you might want to do this:
* You have pre-existing infrastructure and tooling set up to manage
cloudformation stacks.
* You want to integrate with other cloudformation stacks to manage
all your AWS resources, including resources outside of your chalice
app.
* You'd like to integrate with `AWS CodePipeline
<https://aws.amazon.com/codepipeline/>`__ to automatically deploy
changes when you push to a git repo.
Keep in mind that you can't switch between ``chalice deploy`` and
``chalice package`` + CloudFormation for deploying your app.
If you choose to use ``chalice package`` and CloudFormation to deploy
your app, you won't be able to switch back to ``chalice deploy``.
Running ``chalice deploy`` would create an entirely new set of AWS
resources (API Gateway Rest API, AWS Lambda function, etc).
Template Merge
--------------
It's a common use case to need to modify a Chalice generated template
before deployment. Often to inject extra resources, values, or
configurations that are not supported directly by Chalice. It will
always be the case that something on AWS is not supported by Chalice
directly that a consumer may want to interact with.
The package command can now be invoked with the ``--merge-template`` argument::
$ chalice package --merge-template extras.json out
This extras.json file should be a JSON formatted file which will be
deep-merged on top of the sam.json that is generated by Chalice.
For a simple example lets assume that we have the default new Chalice
project and that extras.json has the following content::
{
"Resources" : {
"MusicTable" : {
"Type" : "AWS::DynamoDB::Table",
"Properties" : {
"TableName" : "MusicData",
"AttributeDefinitions" : [
{
"AttributeName" : "Album",
"AttributeType" : "S"
},
{
"AttributeName" : "Artist",
"AttributeType" : "S"
}
],
"KeySchema" : [
{
"AttributeName" : "Album",
"KeyType" : "HASH"
},
{
"AttributeName" : "Artist",
"KeyType" : "RANGE"
}
],
"ProvisionedThroughput" : {
"ReadCapacityUnits" : "5",
"WriteCapacityUnits" : "5"
}
}
},
"APIHandler": {
"Properties": {
"Environment": {
"Variables": {
"MUSIC_TABLE": {"Ref": "MusicTable"}
}
}
}
}
}
}
The generated template located at out/sam.json will have the DynamoDB table
injected into the resource section, as well as the ``MUSIC_TABLE`` environment
variable added to the ``APIHandler`` Lambda function::
...
"APIHandler": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Runtime": "python3.6",
"Handler": "app.app",
"CodeUri": "./deployment.zip",
"Tags": {
"aws-chalice": "version=1.10-:stage=dev:app=test"
},
"Timeout": 60,
"MemorySize": 128,
"Role": {
"Fn::GetAtt": [
"DefaultRole",
"Arn"
]
},
"Environment": {
"Variables": {
"MUSIC_TABLE": "MusicData"
}
}
}
},
...
This gives us the ability to inject arbitrary resources into our Chalice
applications, and reference them from our Chalice-deployed functions. We can
now rely on Chalice's policy auto-generation to generate a policy that allows
DynamoDB access, inject our own policy modifications through the same
extras.json file, or specify a custom policy using the config file.
Example
-------
In this example, we'll create a chalice app and deploy it using
the AWS CLI.
First install the necessary packages::
$ virtualenv /tmp/venv
$ . /tmp/venv/bin/activate
$ pip install chalice awscli
$ chalice new-project test-cfn-deploy
$ cd test-cfn-deploy
At this point we've installed chalice and the AWS CLI and we have
a basic app created locally. Next we'll run the ``package`` command
and look at its contents::
$ $ chalice package /tmp/packaged-app/
Creating deployment package.
$ ls -la /tmp/packaged-app/
-rw-r--r-- 1 j wheel 3355270 May 25 14:20 deployment.zip
-rw-r--r-- 1 j wheel 3068 May 25 14:20 sam.json
$ unzip -l /tmp/packaged-app/deployment.zip | tail -n 5
17292 05-25-17 14:19 chalice/app.py
283 05-25-17 14:19 chalice/__init__.py
796 05-25-17 14:20 app.py
-------- -------
9826899 723 files
$ head < /tmp/packaged-app/sam.json
{
"AWSTemplateFormatVersion": "2010-09-09",
"Outputs": {
"RestAPIId": {
"Value": {
"Ref": "RestAPI"
}
},
"APIHandlerName": {
"Value": {
As you can see in the above example, the ``package`` command created a
directory that contained two files, a ``deployment.zip`` file, which is the
Lambda deployment package, and a ``sam.json`` file, which is the SAM template
that can be deployed using CloudFormation. Next we're going to use the AWS CLI
to deploy our app. To this, we'll first run the ``aws cloudformation package``
command, which will take our deployment.zip file and upload to an S3 bucket
we specify::
$ aws cloudformation package \
--template-file /tmp/packaged-app/sam.json \
--s3-bucket myapp-bucket \
--output-template-file /tmp/packaged-app/packaged.yaml
Now we can deploy our app using the ``aws cloudformation deploy`` command::
$ aws cloudformation deploy \
--template-file /tmp/packaged-app/packaged.yaml \
--stack-name test-cfn-stack \
--capabilities CAPABILITY_IAM
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - test-cfn-stack
This will take a few minutes to complete, but once it's done, the endpoint url
will be available as an output::
$ aws cloudformation describe-stacks --stack-name test-cfn-stack \
--query "Stacks[].Outputs[?OutputKey=='EndpointURL'][] | [0].OutputValue"
"https://abc29hkq0i.execute-api.us-west-2.amazonaws.com/api/"
$ http "https://abc29hkq0i.execute-api.us-west-2.amazonaws.com/api/"
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 18
Content-Type: application/json
...
{
"hello": "world"
}
---
### Source Topics Configfile (`docs/source/topics/configfile.rst`)
Configuration File
==================
Whenever you create a new project using
``chalice new-project``, a ``.chalice`` directory is created
for you. In this directory is a ``config.json`` file that
you can use to control what happens when you ``chalice deploy``::
$ tree -a
.
โโโ .chalice
โ โโโ config.json
โโโ app.py
โโโ requirements.txt
1 directory, 3 files
.. _stage-config:
Stage Specific Configuration
----------------------------
As of version 0.7.0 of chalice, you can specify configuration
that is specific to a chalice stage as well as configuration that should
be shared across all stages. See the :doc:`stages` doc for more
information about chalice stages.
* ``stages`` - This value of this key is a mapping of chalice stage
name to stage configuration. Chalice assumes a default stage name
of ``dev``. If you run the ``chalice new-project`` command on
chalice 0.7.0 or higher, this key along with the default ``dev``
key will automatically be created for you. See the examples
section below for some stage specific configurations.
The following config values can either be specified per stage config
or as a top level key which is not tied to a specific stage. Whenever
a stage specific configuration value is needed, the ``stages`` mapping
is checked first. If no value is found then the top level keys will
be checked.
``api_gateway_endpoint_type``The endpoint configuration of the deployed API Gateway which determines how the
API will be accessed, can be EDGE, REGIONAL, PRIVATE. Note this value can only
be set as a top level key and defaults to EDGE. For more information see
https://amzn.to/2LofApt
api_gateway_endpoint_vpce
When configuring a Private API a VPC Endpoint id must be specified to configure
a default resource policy on the API if an explicit policy is not specified.
This value can be a list or a string of endpoint ids.
``api_gateway_policy_file``
~~~~~~~~~~~~~~~~~~~~~~~~~~~
A file pointing to an IAM resource policy for the REST API. If not specified
chalice will autogenerate this policy when endpoint_type is PRIVATE. This
filename is relative to the ``.chalice`` directory.
``api_gateway_stage``
~~~~~~~~~~~~~~~~~~~~~
The name of the API gateway stage. This will also be the URL prefix for your
API (``https://endpoint/prefix/your-api``).
``autogen_policy``
~~~~~~~~~~~~~~~~~~
A boolean value that indicates if chalice should try to automatically generate
an IAM policy based on analyzing your application source code. The default
value is ``true``. If this value is ``false`` then chalice will try to load an
IAM policy from disk at ``.chalice/policy-<stage-name>.json`` instead of
auto-generating a policy from source code analysis. You can change the filename
by providing the ``iam_policy_file`` config option.
See :ref:`iam-role-pol-examples` for examples of how to configure IAM roles
and policies.
``environment_variables``
~~~~~~~~~~~~~~~~~~~~~~~~~
A mapping of key value pairs. These key value pairs will be set as environment
variables in your application. All environment variables must be strings. If
this key is specified in both a stage specific config option as well as a top
level key, the stage specific environment variables will be merged into the top
level keys. See the :ref:`env-var-examples` section below for a concrete
example.
``iam_policy_file``
~~~~~~~~~~~~~~~~~~~
When ``autogen_policy`` is ``false``, Chalice will try to load an IAM policy
from disk instead of auto-generating one based on source code analysis. The
default location of this file is ``.chalice/policy-<stage-name>.json``, e.g
``.chalice/policy-dev.json``, ``.chalice/policy-prod.json``, etc. You can
change the filename by providing this ``iam_policy_file`` config option. This
filename is relative to the ``.chalice`` directory. For example, this config
will create an IAM role using the file in ``.chalice/my-policy.json``::
{
"version": "2.0",
"app_name": "app",
"stages": {
"dev": {
"autogen_policy": false,
"iam_policy_file": "my-policy.json"
}
}
}
See :ref:`iam-role-pol-examples` for more examples of how to configure IAM
roles and policies.
``iam_role_arn``
~~~~~~~~~~~~~~~~
If ``manage_iam_role`` is ``false``, you must specify this value that indicates
which IAM role arn to use when configuration your application. This value is
only used if ``manage_iam_role`` is ``false``.
See :ref:`iam-role-pol-examples` for examples of how to configure IAM roles
and policies.
``lambda_architecture``
~~~~~~~~~~~~~~~~~~~~~~~
The instruction set architecture of your Lambda functions and automatic
layer. Valid values are ``x86_64`` and ``arm64``. The default
``lambda_architecture`` is ``x86_64``. This value can be set globally or per
stage, but not per function. When set to ``arm64``, Chalice will package
``manylinux`` wheel files built for ``aarch64``. See
:ref:`package-3rd-party` for more information on how third party packages are
handled.
``lambda_memory_size``
~~~~~~~~~~~~~~~~~~~~~~
An integer representing the amount of memory, in MB, your Lambda function is
given. AWS Lambda uses this memory size to infer the amount of CPU allocated to
your function. The default ``lambda_memory_size`` value is ``128``. The value
must be a multiple of 64 MB.
``lambda_timeout``
~~~~~~~~~~~~~~~~~~
An integer representing the function execution time, in seconds, at which AWS
Lambda should terminate the function. The default ``lambda_timeout`` is ``60``
seconds.
``layers``
~~~~~~~~~~
A list of Lambda Layers arns. This value can be provided per stage as well as
per Lambda function. See `AWS Lambda Layers Configuration`_.
.. _automatic-layer-option:
``automatic_layer``
~~~~~~~~~~~~~~~~~~~~
A boolean value that indicates whether chalice will automatically construct a
single stage layer for all Lambda functions with requirements.txt libraries and
vendored libraries. Boolean value defaults to ``false`` if not specified. See
:ref:`package-3rd-party` for more information.
.. _custom-domain-config-options:
``api_gateway_custom_domain``A mapping of key value pairs. The following are required keys when
specifying an api_gateway_custom_domain:
domain_name: The custom domain name to associated with the REST API
(api.example.com)certificate_arn: the ARN of ACM certificate for the current domain name.
If you're using aREGIONALendpoint type for your API, the ACM
certificate must be in the same region as your API. If you're using anEDGEendpoint type, the certificate must be inus-east-1.
You can also provide the following optional configuration:
tls_version- The Transport Layer Security (TLS) version of the security
policy for this domain name. Defaults toTLS_1_2, you can also provideTLS_1_0for REST APIs.url_prefix- A custom domain name plus a url_prefix (BasePathMapping)
specification identifies a deployed REST API in a given stage. With custom
domain names, you can set up your API's hostname, and choose a base path (for
example,myservice) to map the alternative URL to your API (for examplehttps://api.example.com/myservice). If you don't set anyurl_prefix,
the resulting API's base URL is the same as the custom domain (for examplehttps://api.example.com/).- tags - A dictionary of tags with the keys being the tag key, and the values
being the value for the tag.
See the :doc:domainname documentation for more information on configuring
your Chalice application with a custom domain name.
See AWS Custom Domain names setup_ for the API Gateway documentation on
configuring a custom domain name.
.. _custom-domain-ws-config-options:
websocket_api_custom_domain
A mapping of key value pairs. The following are required keys when
specifying a ``websocket_api_custom_domain``:
- ``domain_name``: The custom domain name to associated with the REST API
(api.example.com)
- ``certificate_arn``: the ARN of ACM certificate for the current domain name.
If you're using a ``REGIONAL`` endpoint type for your API, the ACM
certificate **must** be in the same region as your API. If you're using an
``EDGE`` endpoint type, the certificate must be in ``us-east-1``.
You can also provide the following optional configuration:
- ``tls_version`` - The Transport Layer Security (TLS) version of the security
policy for this domain name. Defaults to ``TLS_1_2``, you can also provide
``TLS_1_0`` for REST APIs.
- ``url_prefix`` - A custom domain name plus a url_prefix (BasePathMapping)
specification identifies a deployed REST API in a given stage. With custom
domain names, you can set up your API's hostname, and choose a base path (for
example, `myservice`) to map the alternative URL to your API (for example
``https://api.example.com/myservice``). If you don't set any ``url_prefix``,
the resulting API's base URL is the same as the custom domain (for example
``https://api.example.com/``).
- tags - A dictionary of tags with the keys being the tag key, and the values
being the value for the tag.
See the :doc:`domainname` documentation for more information on configuring
your Chalice application with a custom domain name.
See `AWS Custom Domain names setup`_ for the API Gateway documentation on
configuring a custom domain name.
``manage_iam_role``
~~~~~~~~~~~~~~~~~~~
``true``/``false``. Indicates if you want chalice to create and update the IAM
role used for your application. By default, this value is ``true``. However,
if you have a pre-existing role you've created, you can set this value to
``false`` and a role will not be created or updated. ``"manage_iam_role":
false`` means that you are responsible for managing the role and any associated
policies associated with that role. If this value is ``false`` you must
specify an ``iam_role_arn``, otherwise an error is raised when you try to run
``chalice deploy``.
``minimum_compression_size``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An integer value that indicates the minimum compression size to apply to the
API gateway. If this key is specified in both a stage specific config option as
well as a top level key, the stage specific key will override the top level key
for the given stage. For more information check out the `Service Docs
<https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-gzip-compression-decompression.html>`__
``reserved_concurrency``
~~~~~~~~~~~~~~~~~~~~~~~~
An integer representing each function's reserved concurrency. This value can
be provided per stage as well as per Lambda function. AWS Lambda reserves this
value of concurrency to each lambda deployed in this stage. If the value is set
to 0, invocations to this function are blocked. If the value is unset, there
will be no reserved concurrency allocations. For more information, see `AWS
Documentation on managing concurrency`_.
``subnet_ids``
~~~~~~~~~~~~~~
A list of subnet ids for VPC configuration. This value can be provided per
stage as well as per Lambda function. In order for this value to take effect,
you must also provide the ``security_group_ids`` value. When both values are
provided and ``autogen_policy`` is True, chalice will automatically update your
IAM role with the necessary permissions to create, describe, and delete ENIs.
If you are managing the IAM role policy yourself, make sure to update your
permissions accordingly, as described in the `AWS Lambda VPC documentation`_.
``security_group_ids``
~~~~~~~~~~~~~~~~~~~~~~
A list of security groups for VPC configuration. This value can be provided
per stage as well as per Lambda function. In order for this value to take
effect, you must also provide the ``subnet_ids`` value.
``tags``
~~~~~~~~
A mapping of key value pairs. These key value pairs will be set as the tags on
the resources running your deployed application. All tag keys and values must
be strings. Similar to ``environment_variables``, if a key is specified in both
a stage specific config option as well as a top level key, the stage specific
tags will be merged into the top level keys. By default, all chalice deployed
resources are tagged with the key ``'aws-chalice'`` whose value is
``'version={chalice-version}:stage={stage-name}:app={app-name}'``. Currently
only the following chalice deployed resources are tagged: Lambda functions.
``xray``
~~~~~~~~
A boolean that turns on AWS XRay's Active tracing configuration.
This will turn on XRay for both Lambda functions and API Gateway stages.
``log_retention_in_days``
~~~~~~~~~~~~~~~~~~~~~~~~~
An integer value that indicates the retention time to be applied to lambda
function log groups. Only certain values are valid, see the `AWS CloudWatch
Logs docs
<https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutRetentionPolicy.html#API_PutRetentionPolicy_RequestParameters>`__
.. warning::
If you using the `chalice package` command to generate a CloudFormation template,
a Log Group resource will be added to your template with the configured
``log_retention_in_days``. This will cause your deployment to fail
if this Log Group resource already exists (i.e. if the associated
Lambda function has previously been invoked which results in Lambda
automatically created a Log Group for the function). In order to use
this configuration option, it should be part of the initial deployment
of the Lambda function.
.. _lambda-config:
Lambda Specific Configuration
-----------------------------
In addition to a chalice stage, there are also some configuration values
that can be specified per Lambda function. A chalice app can have many
stages, and a stage can have many Lambda functions.
You have the option to specify configuration for a lambda function across
all your stages, or for a lambda function in a specific stage.
To configure per lambda configuration for a specific stage, you add a
``lambda_functions`` key in your stage configuration::
{
"version": "2.0",
"app_name": "app",
"stages": {
"dev": {
"lambda_functions": {
"foo": {
"lambda_timeout": 120
}
}
}
}
}
To specify per lambda configuration across all stages, you add
a top level ``lambda_functions`` key::
{
"version": "2.0",
"app_name": "app",
"lambda_functions": {
"foo": {
"lambda_timeout": 120
}
}
}
Each key in the ``lambda_functions`` dictionary is the name of a Lambda
function in your app. The value is a dictionary of configuration that
will be applied to that function. These are the configuration options
that can be applied per function:
* ``autogen_policy``
* ``environment_variables``
* ``iam_policy_file``
* ``iam_role_arn``
* ``lambda_memory_size``
* ``lambda_timeout``
* ``layers``
* ``manage_iam_role``
* ``reserved_concurrency``
* ``security_group_ids``
* ``subnet_ids``
* ``tags``
* ``log_retention_in_days``
See the :ref:`stage-config` section above for a description
of these config options.
In general, the name of your lambda function will correspond to
the name of the function in your app. For example:
.. code-block:: python
@app.lambda_function()
def foo(event, context):
pass
To specify configuration for this function, you would use the
key of ``foo`` in the ``lambda_functions`` configuration.
There is one exception to this, which is any python function
decorated with the ``@app.route()`` decorator. Chalice uses
a single Lambda function for all requests from API gateway,
and this name is ``api_handler``. So if you have an app
like this:
.. code-block:: python
@app.route('/')
def index(): pass
@app.route('/foo/bar')
def other_handler(): pass
Then to specify configuration values for the underlying
lambda function, which ``index()`` and ``other_handler()`` share,
you would specify:
.. code-block:: json
{
"lambda_functions": {
"api_handler": {
"subnet_ids": ["sn-1", "sn-2"],
"security_group_ids": ["sg-10", "sg-11"],
"layers": ["layer-arn-1", "layer-arn-2"],
}
}
}
Examples
--------
Below are examples that show how you can configure your chalice app.
Custom Domain Name
~~~~~~~~~~~~~~~~~~
Here's an example for configuring Custom domain name for
dev stage for REST API::
{
"version": "2.0",
"app_name": "app",
"stages": {
"dev": {
"autogen_policy": true,
"api_gateway_stage": "dev"
"api_gateway_custom_domain": {
"domain_name": "api.example.com",
"security_policy": "TLS 1.2|TLS 1.0",
"certificate_arn": "arn:aws:acm:example.com",
"url_prefixes": ["foo", "bar"],
"tags": {
"key": "tag1",
"key1": "tag2"
}
},
},
}
}
In this config file we're specifying ``dev`` stage for ApiGateway.
In the ``dev`` stage, chalice will automatically create ``custom domain name``
with specified ``url_prefixes`` that should contain information about
`AWS Api Mapping key`_.
If there is Websocket API ``websocket_api_custom_domain`` should be used
instead of ``api_gateway_custom_domain``.
.. _iam-role-pol-examples:
IAM Roles and Policies
~~~~~~~~~~~~~~~~~~~~~~
Here's an example for configuring IAM policies across stages::
{
"version": "2.0",
"app_name": "app",
"stages": {
"dev": {
"autogen_policy": true,
"api_gateway_stage": "dev"
},
"beta": {
"autogen_policy": false,
"iam_policy_file": "beta-app-policy.json"
},
"prod": {
"manage_iam_role": false,
"iam_role_arn": "arn:aws:iam::...:role/prod-role"
}
}
}
In this config file we're specifying three stages, ``dev``, ``beta``,
and ``prod``. In the ``dev`` stage, chalice will automatically
generate an IAM policy based on analyzing the application source code.
For the ``beta`` stage, chalice will load the
``.chalice/beta-app-policy.json`` file and use it as the policy to
associate with the IAM role for that stage. In the ``prod`` stage,
chalice won't modify any IAM roles. It will just set the IAM role
for the Lambda function to be ``arn:aws:iam::...:role/prod-role``.
Here's an example that shows config precedence::
{
"version": "2.0",
"app_name": "app",
"api_gateway_stage": "api",
"stages": {
"dev": {
},
"beta": {
},
"prod": {
"api_gateway_stage": "prod",
"manage_iam_role": false,
"iam_role_arn": "arn:aws:iam::...:role/prod-role"
}
}
}
In this config file, both the ``dev`` and ``beta`` stage will
have an API gateway stage name of ``api`` because they will
default to the top level ``api_gateway_stage`` key.
However, the ``prod`` stage will have an API gateway stage
name of ``prod`` because the ``api_gateway_stage`` is specified
in ``{"stages": {"prod": ...}}`` mapping.
.. _env-var-examples:
Environment Variables
~~~~~~~~~~~~~~~~~~~~~
In the following example, environment variables are specified
both as top level keys as well as per stage. This allows us to
provide environment variables that all stages should have as well
as stage specific environment variables::
{
"version": "2.0",
"app_name": "app",
"environment_variables": {
"SHARED_CONFIG": "foo",
"OTHER_CONFIG": "from-top"
},
"stages": {
"dev": {
"environment_variables": {
"TABLE_NAME": "dev-table",
"OTHER_CONFIG": "dev-value"
}
},
"prod": {
"environment_variables": {
"TABLE_NAME": "prod-table",
"OTHER_CONFIG": "prod-value"
}
}
}
}
For the above config, the ``dev`` stage will have the
following environment variables set::
{
"SHARED_CONFIG": "foo",
"TABLE_NAME": "dev-table",
"OTHER_CONFIG": "dev-value",
}
The ``prod`` stage will have these environment variables set::
{
"SHARED_CONFIG": "foo",
"TABLE_NAME": "prod-table",
"OTHER_CONFIG": "prod-value",
}
Per Lambda Examples
~~~~~~~~~~~~~~~~~~~
Suppose we had the following chalice app:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='demo')
@app.lambda_function()
def foo(event, context):
pass
@app.lambda_function()
def bar(event, context):
pass
Given these two functions, we'd like to configure the functions
as follows:
* Both functions should have an environment variable ``OWNER`` with value
``dev-team``.
* The ``foo`` function should have an autogenerated IAM policy managed by
chalice.
* The ``foo`` function should be run in a VPC with subnet ids ``sn-1`` and
``sn-2``, with security groups ``sg-10`` and ``sg-11``. Chalice should
also automatically configure the IAM policy with permissions to modify
EC2 network interfaces.
* The ``foo`` function should have two connected layers as ``layer-arn-1`` and
``layer-arn-2``. Chalice should automatically configure the IAM policy.
* The ``bar`` function should use a pre-existing IAM role that was created
outside of chalice. Chalice should not perform an IAM role management for
the ``bar`` function.
* The ``bar`` function should have an environment variable ``TABLE_NAME`` with
value ``mytable``.
We can accomplish all this with this config file::
{
"stages": {
"dev": {
"environment_variables": {
"OWNER": "dev-team"
}
"api_gateway_stage": "api",
"lambda_functions": {
"foo": {
"subnet_ids": ["sn-1", "sn-2"],
"security_group_ids": ["sg-10", "sg-11"],
"layers": ["layer-arn-1", "layer-arn-2"],
},
"bar": {
"manage_iam_role": false,
"iam_role_arn": "arn:aws:iam::my-role-name",
"environment_variables": {"TABLE_NAME": "mytable"}
}
}
}
},
"version": "2.0",
"app_name": "demo"
}
.. _AWS Lambda VPC documentation: https://docs.aws.amazon.com/lambda/latest/dg/vpc.html#vpc-configuring
.. _AWS Documentation on managing concurrency: https://docs.aws.amazon.com/lambda/latest/dg/concurrent-executions.html
.. _AWS Lambda Layers Configuration: https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html
.. _AWS Custom Domain names setup: https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains.html
.. _AWS Api Mapping key: https://docs.aws.amazon.com/apigatewayv2/latest/api-reference/domainnames-domainname-apimappings.html
---
### Source Topics Domainname (`docs/source/topics/domainname.rst`)
Custom Domain Names
===================
Custom domain names are simpler and more intuitive URLs
that you can provide to your API users.
With custom domain names, you can set up your API's hostname,
and choose a base path to map the alternative URL to your API.
You must have an AWS managed certificate created or imported through
AWS Certificate Manager (ACM) in order to configure a custom domain name
for REST and WebSocket APIs.
See `Get certificate in AWS Certificate Manager <https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-custom-domains-prerequisites.html>`__
for more information.
Custom domain name can be configured per Chalice stage.
.. note::
This document describes the configuration option and process
needed to configure a custom domain with your Chalice application.
If you'd like a step-by-step example that walks you through configuring
a custom domain for a Chalice app using Amazon Route53 and ACM, see
the :doc:`../tutorials/customdomain` tutorial.
There are two steps to configuring a custom domain name. First you must
configure your Chalice app such that it creates the necessary resources
and configuration when provisioning your REST or WebSocket APIs. This
is explained in the next two sections below.
Then you must configure your DNS configuration to point your custom domain
name to the domain name created by API Gateway. This is explained in the
:ref:`dns-last-config` section below.
Configure custom domain name for REST API
-----------------------------------------
To create custom domain name for REST API, add the
``api_gateway_custom_domain`` configuration option to your
``.chalice/config.json`` file. You must specify the ``certificate_arn``,
which is the ARN of your ACM certificate associated with your domain as
well as your ``domain_name``. The remaining fields are optional and
may be omitted. By default TLS 1.2 is used for your endpoint unless
otherwise specified.
Below is an example of all the configuration options you can specify
when configuring a custom domain. They are explained in the
:ref:`custom-domain-config-options` section of the :doc:`configfile`
documentation.
.. code-block:: json
{
"stages": {
"dev": {
"api_gateway_stage": "api",
"api_gateway_custom_domain": {
"domain_name": "api.example.com",
"tls_version": "TLS_1_2|TLS_1_0",
"certificate_arn": "arn:aws:acm:example",
"url_prefix": "foo",
"tags": {
"key": "tag1",
"key1": "tag2"
}
}
}
}
}
Configure custom domain name for WebSocket
------------------------------------------
To create custom domain name for WebSocket API, add the
``websocket_api_custom_domain`` configuration option to your
``.chalice/config.json`` file.
Below is an example of all the configuration options you can specify when
configuring a custom domain for a WebSocket API. They are explained in the
:ref:`custom-domain-ws-config-options` section of the :doc:`configfile`
documentation.
.. code-block:: json
{
"stages": {
"dev": {
"api_gateway_stage": "api",
"websocket_api_custom_domain": {
"domain_name": "api.example.com",
"tls_version": "TLS_1_2|TLS_1_0",
"certificate_arn": "arn:aws:acm:example",
"url_prefix": "foo",
"tags": {
"key": "tag1",
"key1": "tag2"
}
}
}
}
}
.. _dns-last-config:
DNS Configuration
-----------------
Chalice only configures your API Gateway API with the necessary resources
and configuration so a custom domain can be used. It does not alter any
existing DNS records you have associated with your domain name. After you've
deployed your Chalice app with the configuration options described above,
you'll need to modify your DNS records to point to your API Gateway API
using the web interface or API of your domain registrar associated with
your domain name. When you run ``chalice deploy`` with a custom domain
configured, there will be two new fields in the output::
$ chalice deploy
Creating deployment package.
Updating policy for IAM role: customdomain-dev
Updating lambda function: customdomain-dev
Updating rest API
Creating custom domain name: api.chalice-demo-app.com
Creating api mapping: /
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:0123456789:function:customdomain-dev
- Rest API URL: https://qxea58abcd.execute-api.us-west-2.amazonaws.com/api/
- Custom domain name:
HostedZoneId: Z1UJRXOUMOOFQ8
AliasDomainName: d-6vj4cynstd.execute-api.us-west-2.amazonaws.com
If you're using Route53 to manage your hosted zone, you'll need to create
an Alias record using the ``HostedZoneId`` and ``AliasDomainName`` specified
in the output of ``chalice deploy``. If you're using a third party domain
registrar, you'll need to create a CNAME record to the ``AliasDomainName``.
If you'd like a step-by-step example of how to do this with Route53, see
the :doc:`../tutorials/customdomain` tutorial.
---
### Source Topics Events (`docs/source/topics/events.rst`)
====================
Lambda Event Sources
====================
.. _scheduled-events:
Scheduled Events
================
Chalice has support for `scheduled events`_. This feature allows you to
periodically invoke a lambda function based on some regular schedule. You can
specify a fixed rate or a cron expression.
To create a scheduled event in chalice, you use the ``@app.schedule()``
decorator. Let's look at an example.
.. code-block:: python
app = chalice.Chalice(app_name='foo')
@app.schedule('rate(1 hour)')
def every_hour(event):
print(event.to_dict())
In this example, we have a single lambda function that we want automatically
invoked every hour. When you run ``chalice deploy`` Chalice will create a
lambda function as well as the necessary CloudWatch events/rules such that the
``every_hour`` function is invoked every hour.
The :meth:`Chalice.schedule` method accepts either a string or an
instance of :class:`Rate` or :class:`Cron`. For example:
.. code-block:: python
app = chalice.Chalice(app_name='foo')
@app.schedule(Rate(1, unit=Rate.HOURS))
def every_hour(event):
print(event.to_dict())
The function you decorate must accept a single argument,
which will be of type :class:`CloudWatchEvent`.
You can use the ``schedule()`` decorator multiple times
in your chalice app. Each ``schedule()`` decorator will
result in a new lambda function and associated CloudWatch
event rule. For example:
.. code-block:: python
app = chalice.Chalice(app_name='foo')
@app.schedule(Rate(1, unit=Rate.HOURS))
def every_hour(event):
print(event.to_dict())
@app.schedule(Rate(2, unit=Rate.HOURS))
def every_two_hours(event):
print(event.to_dict())
In the app above, chalice will create two lambda functions,
and configure ``every_hour`` to be invoked once an hour,
and ``every_two_hours`` to be invoked once every two hours.
.. _cwe-events:
CloudWatch Events
==================
You can configure a lambda function to subscribe to
any `CloudWatch Event <https://amzn.to/2SCgWA6>`__.
To subscribe to a CloudWatch Event in chalice, you use the
``@app.on_cw_event()`` decorator. Let's look at an example.
.. code-block:: python
app = chalice.Chalice(app_name='foo')
@app.on_cw_event({"source": ["aws.codecommit"]})
def on_code_commit_changes(event):
print(event.to_dict())
In this example, we have a single lambda function that we subscribe to all
events from the AWS Code Commit service. The first parameter to the decorator
is the event pattern that will be used to filter the events sent to the function.
See the `CloudWatch Event pattern docs <https://amzn.to/2OlqZso>`__
for additional syntax and examples.
The function you decorate must accept a single argument,
which will be of type :class:`CloudWatchEvent`.
.. _s3-events:
S3 Events
=========
You can configure a lambda function to be invoked whenever
certain events happen in an S3 bucket. This uses the
`event notifications`_ feature provided by Amazon S3.
To configure this, you just tell Chalice the name of an existing
S3 bucket, along with what events should trigger the lambda function.
This is done with the :meth:`Chalice.on_s3_event` decorator.
Here's an example:
.. code-block:: python
from chalice import Chalice
app = chalice.Chalice(app_name='s3eventdemo')
app.debug = True
@app.on_s3_event(bucket='mybucket-name',
events=['s3:ObjectCreated:*'])
def handle_s3_event(event):
app.log.debug("Received event for bucket: %s, key: %s",
event.bucket, event.key)
In this example above, Chalice connects the S3 bucket to the
``handle_s3_event`` Lambda function such that whenever an object is uploaded
to the ``mybucket-name`` bucket, the Lambda function will be invoked.
This example also uses the ``.bucket`` and ``.key`` attributes from the
``event`` parameter, which is of type :class:`S3Event`.
It will automatically create the appropriate S3 notification configuration
as needed. Chalice will also leave any existing notification configuration
on the ``mybucket-name`` untouched. It will only merge in the additional
configuration needed for the ``handle_s3_event`` Lambda function.
.. warning::
This feature only works when using `chalice deploy`. Because you
configure the lambda function with the name of an existing S3 bucket,
it is not possible to describe this using a CloudFormation/SAM template.
The ``chalice package`` command will fail. You will eventually be able
to request that chalice create a bucket for you, which will support
the ``chalice package`` command.
The function you decorate must accept a single argument,
which will be of type :class:`S3Event`.
.. _sns-events:
SNS Events
==========
You can configure a lambda function to be automatically invoked whenever
something publishes to an SNS topic. Chalice will automatically handle
creating the lambda function, subscribing the lambda function to the
SNS topic, and modifying the lambda function policy to allow SNS to invoke
the function.
To configure this, you just need the name of an existing SNS topic you'd
like to subscribe to. The SNS topic must already exist.
Below is an example of how to set this up. The example uses boto3 to
create the SNS topic. If you don't have boto3 installed in your virtual
environment, be sure to install it with::
$ pip install boto3
First, we'll create an SNS topic using boto3.
::
$ python
>>> import boto3
>>> sns = boto3.client('sns')
>>> sns.create_topic(Name='my-demo-topic')
{'TopicArn': 'arn:aws:sns:us-west-2:12345:my-demo-topic',
'ResponseMetadata': {}}
Next, we'll create our chalice app::
$ chalice new-project chalice-demo-sns
$ cd chalice-demo-sns/
We'll update the ``app.py`` file to use the ``on_sns_message`` decorator:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='chalice-sns-demo')
app.debug = True
@app.on_sns_message(topic='my-demo-topic')
def handle_sns_message(event):
app.log.debug("Received message with subject: %s, message: %s",
event.subject, event.message)
We can now deploy our chalice app::
$ chalice deploy
Creating deployment package.
Creating IAM role: chalice-demo-sns-dev
Creating lambda function: chalice-demo-sns-dev-handle_sns_message
Subscribing chalice-demo-sns-dev-handle_sns_message to SNS topic my-demo-topic
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:123:function:...
And now we can test our app by publishing a few SNS messages to our topic.
We'll do this using boto3. In the example below, we're using ``list_topics()``
to find the ARN associated with our topic name before calling the ``publish()``
method.
::
$ python
>>> import boto3
>>> sns = boto3.client('sns')
>>> topic_arn = [t['TopicArn'] for t in sns.list_topics()['Topics']
... if t['TopicArn'].endswith(':my-demo-topic')][0]
>>> sns.publish(Message='TestMessage1', Subject='TestSubject1',
... TopicArn=topic_arn)
{'MessageId': '12345', 'ResponseMetadata': {}}
>>> sns.publish(Message='TestMessage2', Subject='TestSubject2',
... TopicArn=topic_arn)
{'MessageId': '54321', 'ResponseMetadata': {}}
To verify our function was called correctly, we can use the ``chalice logs``
command::
$ chalice logs -n handle_sns_message
2018-06-28 17:49:30.513000 547e0f chalice-demo-sns - DEBUG - Received message with subject: TestSubject1, message: TestMessage1
2018-06-28 17:49:40.391000 547e0f chalice-demo-sns - DEBUG - Received message with subject: TestSubject2, message: TestMessage2
In this example we used the SNS topic name to register our handler, but you can
also use the topic arn. This can be useful if your topic is in another region
or account.
.. _sqs-events:
SQS Events
==========
You can configure a lambda function to be invoked whenever messages are
available on an SQS queue. To configure this, use the
:meth:`Chalice.on_sqs_message` decorator and provide the name of the SQS queue
and an optional batch size.
The message visibility timeout of your SQS queue must be greater than or
equal to the lambda timeout. The default message visibility timeout
when you create an SQS queue is 30 seconds, and the default timeout
for a Lambda function is 60 seconds, so you'll need to modify one of these
values in order to successfully connect an SQS queue to a Lambda function.
You can check the visibility timeout of your queue using the
``GetQueueAttributes`` API call. Using the
`AWS CLI <https://docs.aws.amazon.com/cli/latest/reference/sqs/get-queue-attributes.html>`__,
you can run this command to check the value::
$ aws sqs get-queue-attributes \
--queue-url https://us-west-2.queue.amazonaws.com/1/testq \
--attribute-names VisibilityTimeout
{
"Attributes": {
"VisibilityTimeout": "30"
}
}
You can set the visibility timeout of your SQS queue using the
``SetQueueAttributes`` API call. Again using the AWS CLI you can
run this command::
$ aws sqs set-queue-attributes \
--queue-url https://us-west-2.queue.amazonaws.com/1/testq \
--attributes VisibilityTimeout=60
If you would prefer to change the timeout of your lambda function instead,
you can specify this timeout value using the ``lambda_timeout`` config key
if your ``.chalice/config.json`` file.
See :ref:`lambda-config` for a list of all supported lambda configuration
values in chalice. In this example below, we're setting the timeout
of our ``handle_sqs_message`` lambda function to 30 seconds::
$ cat .chalice/config.json
{
"stages": {
"dev": {
"lambda_functions": {
"handle_sqs_message": {
"lambda_timeout": 30
}
}
}
},
"version": "2.0",
"app_name": "chalice-sqs-demo"
}
In this example below, we're connecting the ``handle_sqs_message`` lambda
function to the ``my-queue`` SQS queue. Note that we are specifying the
queue name, not the queue URL or queue ARN. If you are connecting your
lambda function to a FIFO queue, make sure you specify the ``.fifo``
suffix, e.g. ``my-queue.fifo``.
.. code-block:: python
from chalice import Chalice
app = chalice.Chalice(app_name='chalice-sqs-demo')
app.debug = True
@app.on_sqs_message(queue='my-queue', batch_size=1)
def handle_sqs_message(event):
for record in event:
app.log.debug("Received message with contents: %s", record.body)
Whenever a message is sent to the SQS queue our function will be automatically
invoked. The function argument is an :class:`SQSEvent` object, and each
``record`` in the example above is of type :class:`SQSRecord`. Lambda takes
care of automatically scaling your function as needed. See `Understanding
Scaling Behavior`_ for more information on how Lambda scaling works.
If your lambda function completes without raising an exception, then
Lambda will automatically delete all the messages associated with the
:class:`SQSEvent`. You don't need to manually call ``sqs.delete_message()``
in your lambda function. If your lambda function raises an exception, then
Lambda won't delete any messages, and once the visibility timeout has been
reached, the messages will be available again in the SQS queue. Note that
if you are using a batch size of more than one, the entire batch succeeds or
fails. This means that it is possible for your lambda function to see
a message multiple times, even if it's successfully processed the message
previously. There are a few options available to mitigate this:
* Use a batch size of 1 (the default value).
* Use a separate data store to check if you've already processed an SQS
message. You can use services such as Amazon DynamoDB or Amazon ElastiCache.
* Manually call ``sqs.delete_message()`` in your Lambda function once you've
successfully processed a message.
For more information on Lambda and SQS,
see the `AWS documentation`_.
.. _kinesis-events:
Kinesis Events
==============
You can configure a Lambda function to be invoked whenever messages are
published to an Amazon Kinesis data stream. To configure this, use the
:meth:`Chalice.on_kinesis_record` decorator and provide the name of the
Kinesis stream.
The :class:`KinesisEvent` that is passed in as the ``event`` argument
to the event handler is also iterable. This allows you to iterate over
all the records in the event. Additionally, each record has a ``.data``
attribute that is automatically base64 decoded for you.
Here's an example:
.. code-block:: python
from chalice import Chalice
app = chalice.Chalice(app_name='kinesiseventdemo')
app.debug = True
@app.on_kinesis_record(stream='mystream')
def handle_kinesis_message(event):
for record in event:
# The .data attribute is automatically base64 decoded for you.
app.log.debug("Received message with contents: %s", record.data)
For more information on using Kinesis and Lambda, see
`Using AWS Lambda with Amazon Kinesis <https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html>`__.
.. _dynamodb-events:
DynamoDB Events
===============
You can configure a Lambda function to be invoked whenever messages are
published to an Amazon DynamoDB stream. To configure this, use the
:meth:`Chalice.on_dynamodb_record` decorator and provide the name of the
DynamoDB stream ARN.
.. note::
Other event handlers such as :meth:`Chalice.on_kinesis_record`,
:meth:`Chalice.on_sqs_message`, and :meth:`Chalice.on_sns_message`
only require the resource name and not the full ARN. In the case
of DynamoDB streams, there are auto-generated portions of the
stream ARN that cannot be computed based on the resource name. This
is why Chalice requires that full stream ARN when configuring
a DynamoDB stream handler.
The :class:`DynamoDBEvent` that is passed in as the ``event`` argument
to the event handler is also iterable. This allows you to iterate over
all the records in the event.
Here's an example:
.. code-block:: python
from chalice import Chalice
app = chalice.Chalice(app_name='ddb-event-demo')
app.debug = True
@app.on_dynamodb_record(stream_arn='arn:aws:dynamodb:.../stream/2020')
def handle_ddb_message(event):
for record in event:
app.log.debug("New: %s", record.new_image)
For more information on using Lambda and DynamoDB, see
`Using AWS Lambda with Amazon DynamoDB <https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html>`__.
.. _event notifications: https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
.. _AWS documentation: https://docs.aws.amazon.com/lambda/latest/dg/with-sqs.html
.. _Understanding Scaling Behavior: https://docs.aws.amazon.com/lambda/latest/dg/scaling.html
---
### Source Topics Experimental (`docs/source/topics/experimental.rst`)
Experimental APIs
=================
Chalice maintains backwards compatibility for all features that appear in this
documentation. Any Chalice application using version 1.x will continue to work
for all future versions of 1.x.
We also believe that Chalice has a lot of potential for new ideas and APIs,
many of which will take several iterations to get right. We may implement a
new idea and need to make changes based on customer usage and feedback. This
may include backwards incompatible changes all the way up to the removal of
a feature.
To accommodate these new features, Chalice has support for experimental APIs,
which are features that are added to Chalice on a provisional basis. Because
these features may include backwards incompatible changes, you must explicitly
opt-in to using these features. This makes it clear that you are using an
experimental feature that may change.
Opting-in to Experimental APIs
------------------------------
Each experimental feature in chalice has a name associated with it. To opt-in
to an experimental API, you must have the feature name to the
``experimental_feature_flags`` attribute on your ``app`` object.
This attribute's type is a set of strings.
.. code-block:: python
from chalice import Chalice
app = Chalice('myapp')
app.experimental_feature_flags.update([
'MYFEATURE1',
'MYFEATURE2',
])
If you use an experimental API without opting-in, you will receive
a message whenever you run a Chalice CLI command. The error message
tells you which feature flags you need to add::
$ chalice deploy
You are using experimental features without explicitly opting in.
Experimental features do not guarantee backwards compatibility and may be removed in the future.
If you still like to use these experimental features, you can opt-in by adding this to your app.py file:
app.experimental_feature_flags.update([
'FEATURE_FLAG_NAME'
])
See https://aws.github.io/chalice/topics/experimental.rst for more details.
The feature flag only happens when running CLI commands. There are no runtime
checks for experimental features once your application is deployed.
List of Experimental APIs
-------------------------
In the table below, the "Feature Flag Name" column is the value you
must add to the ``app.experimental_feature_flags`` attribute.
The status of an experimental API can be:
* ``Trial`` - You must explicitly opt-in to use this feature.
* ``Accepted`` - This feature has graduated from an experimental
feature to a fully supported, backwards compatible feature in Chalice.
Accepted features still appear in the table for auditing purposes.
* ``Rejected`` - This feature has been removed.
.. list-table:: Experimental APIs
:header-rows: 1
* - Feature
- Feature Flag Name
- Version Added
- Version Finalized
- Status
- GitHub Issue(s)
* - :doc:`blueprints`
- ``BLUEPRINTS``
- 1.7.0
- 1.15.0
- Accepted
- `#1023 <https://github.com/aws/chalice/pull/1023>`__,
`#651 <https://github.com/aws/chalice/pull/651>`__
* - :doc:`websockets`
- ``WEBSOCKETS``
- 1.10.0
- n/a
- Trial
- `#1041 <https://github.com/aws/chalice/pull/1041>`__,
`#1017 <https://github.com/aws/chalice/issues/1017>`__
See the `original discussion <https://github.com/aws/chalice/issues/1019>`__
for more background information and alternative proposals.
---
### Source Topics Index (`docs/source/topics/index.rst`)
Topics
======
.. toctree::
:maxdepth: 2
routing
views
configfile
multifile
logging
sdks
stages
packaging
pyversion
cfn
tf
authorizers
events
purelambda
blueprints
websockets
cd
domainname
experimental
testing
middleware
---
### Source Topics Logging (`docs/source/topics/logging.rst`)
Logging
=======
You have several options for logging in your
application. You can use any of the options
available to lambda functions as outlined
in the
`AWS Lambda Docs <https://docs.aws.amazon.com/lambda/latest/dg/python-logging.html>`_.
The simplest option is to just use print statements.
Anything you print will be accessible in cloudwatch logs
as well as in the output of the ``chalice logs`` command.
In addition to using the stdlib ``logging`` module directly,
the framework offers a preconfigured logger designed to work
nicely with Lambda. This is offered purely as a convenience,
you can use ``print`` or the ``logging`` module directly if you prefer.
You can access this logger via the ``app.log``
attribute, which is a logger specifically for your application.
This attribute is an instance of ``logging.getLogger(your_app_name_)``
that's been preconfigured with reasonable defaults:
* StreamHandler associated with ``sys.stdout``.
* Log level set to ``logging.ERROR`` by default.
You can also manually set the logging level by setting
``app.log.setLevel(logging.DEBUG)``.
* A logging formatter that displays the app name, level name,
and message.
Examples
--------
In the following application, we're using the application logger
to emit two log messages, one at ``DEBUG`` and one at the ``ERROR``
level:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='demolog')
@app.route('/')
def index():
app.log.debug("This is a debug statement")
app.log.error("This is an error statement")
return {'hello': 'world'}
If we make a request to this endpoint, and then look at
``chalice logs`` we'll see the following log message::
2016-11-06 20:24:25.490000 9d2a92 demolog - ERROR - This is an error statement
As you can see, only the ``ERROR`` level log is emitted because
the default log level is ``ERROR``. Also note the log message formatting.
This is the default format that's been automatically configured.
We can make a change to set our log level to debug:
.. code-block:: python
import logging
from chalice import Chalice
app = Chalice(app_name='demolog')
# Enable DEBUG logs.
app.log.setLevel(logging.DEBUG)
@app.route('/')
def index():
app.log.debug("This is a debug statement")
app.log.error("This is an error statement")
return {'hello': 'world'}
Now if we make a request to the ``/`` URL and look at the
output of ``chalice logs``, we'll see the following log message::
2016-11-07 12:29:15.714 431786 demolog - DEBUG - This is a debug statement
2016-11-07 12:29:15.714 431786 demolog - ERROR - This is an error statement
As you can see here, both the debug and error log message are shown.
You can use the ``-n/--name`` option to view the logs for a specific lambda
function. By default, the logs for the API handler lambda function are shown.
This corresponds to any log statements made within an ``@app.route()`` call.
The name option is the logical name of the lambda function. This is the
name of the python function by default, or whatever name you provided
as the ``name`` kwarg to the ``@app.lambda_function()`` call. For example,
given this app:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='multilog')
@app.lambda_function()
def foo(event, context):
app.log.debug("Invoking from function foo")
return {'hello': 'world'}
@app.lambda_function(name='MyFunction)
def bar(event, context):
incr_counter()
app.log.debug("Invoking from function bar")
return {'hello': 'world'}
You can retrieve logs for the above function by running::
$ chalice logs --name foo
$ chalice logs --name MyFunction
---
### Source Topics Middleware (`docs/source/topics/middleware.rst`)
==========
Middleware
==========
Chalice provides numerous features and capabilities right out of the box, but
there are often times where you'll want to customize the behavior of Chalice
for your specific needs. You can accomplish this by using middleware, which
lets you alter the request and response lifecycle. Chalice middleware
is a function that you register as part of your application that will
automatically be invoked by Chalice whenever your Lambda functions are called.
Below is an example of Chalice middleware:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='demo-middleware')
@app.middleware('all')
def my_middleware(event, get_response):
app.log.info("Before calling my main Lambda function.")
response = get_response(event)
app.log.info("After calling my main Lambda function.")
return response
@app.route('/')
def index():
return {'hello': 'world'}
@app.on_sns_message('mytopic')
def sns_handler(event):
pass
In this example, our middleware is emitting a log message before and after
our Lambda function has been invoked. Because we specified an event type of
``all``, the ``my_middleware`` function will be called when either our REST
API's ``index()`` or our ``sns_handler()`` Lambda function is invoked.
Writing Middleware
==================
Middleware must adhere to these requirements:
* Must be a callable object that accepts two parameters, an ``event``, and
a ``get_response`` function. The ``event`` type will depend on what type
of handlers the middleware has been registered for (see "Registering
Middleware" below).
* Must return a response. This will be the response that gets returned back
to the caller.
* In order to invoke the next middleware in the chain and eventually call the
actual Lambda handler, it must invoke ``get_response(event)``.
* Middleware can short-circuit the request by returning its own response.
It does not have to invoke ``get_response(event)`` if not needed. The
response type should match the response type of the underlying Lambda
handler.
Below is the simplest middleware in Chalice that does nothing:
.. code-block:: python
@app.middleware('all')
def noop_middleware(event, get_response):
# The `event` type will depend on what type of
# Lambda handler is being invoked.
return get_response(event)
Error Handling
--------------
With the exception of middleware for REST APIs, all middleware follow the same
error handling strategy. Any exceptions from a Lambda handler are propagated
back to each middleware. You can then catch these exceptions in your
middleware and process them as needed. For example:
.. code-block:: python
@app.middleware('all')
def handle_errors(event, get_response):
try:
return get_response(event)
except MyCustomError as e:
# We don't want MyCustomError to propagate, instead
# we'll convert this to an error response dictionary.
return {"Error": e.__class__.__name__,
"Message": str(e)}
@app.lambda_function()
def noop_middleware(event, context):
raise MyCustomError("Raising an error.")
If an exception is raised in a Lambda handler and no middleware catches the
exception, the exception will be returned back to the client that invoked
the Lambda function.
Rest APIs
~~~~~~~~~
Rest APIs have special error processing for backwards compatibility purposes.
If a chalice view function (decorated via ``@app.route``) raises an exception
Chalice will automatically catch this exception and convert to a ``Response``
object with an appropriately set status code (see :ref:`view-error-handling`).
As a result, middleware for Rest APIs won't see exceptions propagate, they will
instead see a `Response` object as a result of calling ``get_response(event)``.
In the case where you want to allow an exception to propagate out of a view
function, you can raise a ``chalice.ChaliceUnhandledError`` exception.
For example:
.. code-block:: python
from chalice import ChaliceUnhandledError
@app.middleware('all')
def handle_errors(event, get_response):
try:
return get_response(event)
except ChaliceUnhandledError as e:
return Response(status_code=500, body=str(e),
headers={'Content-Type': 'text/plain'})
@app.route('/')
def index():
# The handle_errors middleware will never see this exception.
# This will automatically be converted to a ``Response`` object
# with a status code of ``500``.
raise MyCustomError("Raising an error.")
@app.route('/error')
def unhandled_error():
# The handle_errors middleware will see this exception because it's
# of type ChaliceUnhandledError.
raise ChaliceUnhandledError("Raising an error.")
This is useful if you want to have middleware that applies to all event types
that has consistent error handling behavior. If a
``chalice.ChaliceUnhandledError`` error is raised and no middleware catches
and processes this error, then the standard error processing behavior will
apply (a 500 response is returned back to the user, and if debug mode
is enabled, the traceback is sent as the response body).
Registering Middleware
----------------------
In order to register middleware, you use the ``@app.middleware()`` decorator.
This function accepts a single arg that specifies what type of Lambda function
it wants to be registered for. This allows you to apply middleware to only
specific type of event handlers, e.g. only for REST APIs, or Websockets, or
S3 event handlers. To register middleware for all Lambda functions, you can
specify ``all``. Below are the supported event types along with the
corresponding type of event that will be provided to the middleware:
* ``all`` - ``Any``
* ``s3`` - :class:`S3Event`
* ``sns`` - :class:`SNSEvent`
* ``sqs`` - :class:`SQSEvent`
* ``cloudwatch`` - :class:`CloudWatchEvent`
* ``scheduled`` - :class:`CloudWatchEvent`
* ``websocket`` - :class:`WebsocketEvent`
* ``http`` - :class:`Request`
* ``pure_lambda`` - :class:`LambdaFunctionEvent`
.. note::
The ``chalice.LambdaFunctionEvent`` is the only case where the
event type for the middleware does not match the event type of the
corresponding Lambda handler. For backwards compatibility reasons,
the existing signature of the ``@app.lambda_function()`` decorator
is preserved (it accepts an ``event`` and ``context``) whereas for
middleware, a consistent signature is needed, which is why the
``chalice.LambdaFunctionEvent`` is used.
You can also use the :meth:`Chalice.register_middleware` method, which
has the same behavior as :meth:`Chalice.middleware` except you provide
the middleware function as an argument instead of decorating a function.
This is useful when you want to import third party functions and use
them as middleware.
.. code-block:: python
import thirdparty
app.register_middleware(thirdparty.func, 'all')
You can also use the :class:`ConvertToMiddleware` class to convert an
existing Lambda wrapper to middleware. For example, if you had the
following logging decorator:
.. code-block:: python
def log_invocation(func):
def wrapper(event, context):
logger.debug("Before lambda function.")
response = func(event, context)
logger.debug("After lambda function.")
return wrapper
@app.lambda_function()
@log_invocation
def myfunction(event, context):
logger.debug("In myfunction().")
Rather than decorate every Lambda function with the ``@log_invocation``
decorator, you can instead use ``ConvertToMiddleware`` to automatically
apply this wrapper to every Lambda function in your app.
.. code-block:: python
from chalice import ConvertToMiddleware
app.register_middleware(ConvertToMiddleware(log_invoation))
This is also useful to integrate with existing libraries that provide
Lambda wrappers. See :ref:`powertools-example` for a more complete
example.
Examples
========
Below are some examples of common middleware patterns.
Short Circuiting a Request
--------------------------
In this example, we want to return a 400 bad response if a specific
header is missing from a request. Because this is HTTP specific, we only
want to register this handler for our ``http`` event type.
.. code-block:: python
from chalice import Response
@app.middleware('http')
def require_header(event, get_response):
# From the list above, because this is an ``http`` event
# type, we know that event will be of type ``chalice.Request``.
if 'X-Custom-Header' not in event.headers:
return Response(
status_code=400,
body={"Error": "Missing required 'X-Custom-Header'"})
# If the header exists then we'll defer to our normal request flow.
return get_response(event)
Modifying a Response
--------------------
In this example, we want to measure the processing time and inject it as
a key in our Lambda response.
.. code-block:: python
import time
@app.middleware('pure_lambda')
def inject_time(event, get_response):
start = time.time()
response = get_response(event)
total = time.time() - start
response.setdefault('metadata', {})['duration'] = total
return response
.. _powertools-example:
Integrating with AWS Lambda Powertools
--------------------------------------
`AWS Lambda Powertools
<https://docs.powertools.aws.dev/lambda/python/latest/>`__ is a suite of
utilities for AWS Lambda functions that makes tracing with AWS X-Ray,
structured logging and creating custom metrics asynchronously easier.
You can use Chalice middleware to easily integrate Lambda Powertools with
your Chalice apps. In this example, we'll use the
`Logger
<https://docs.powertools.aws.dev/lambda/python/latest/core/logger/>`__
and `Tracer <https://docs.powertools.aws.dev/lambda/python/latest/core/tracer/>`__
and convert them to Chalice middleware so they will be automatically applied
to all Lambda functions in our application.
.. code-block:: python
from chalice import Chalice
from chalice.app import ConvertToMiddleware
# First, instead of using Chalice's built in logger, we'll instead use
# the structured logger from powertools. In addition to automatically
# injecting lambda context, let's say we also want to inject which
# route is being invoked.
from aws_lambda_powertools import Logger
from aws_lambda_powertools import Tracer
app = Chalice(app_name='chalice-powertools')
logger = Logger(service=app.app_name)
tracer = Tracer(service=app.app_name)
# This will automatically convert any decorator on a lambda function
# into middleware that will be connected to every lambda function
# in our app. This lets us avoid decoratoring every lambda function
# with this behavior, but it also works in cases where we don't control
# the code (e.g. registering blueprints).
app.register_middleware(ConvertToMiddleware(logger.inject_lambda_context))
app.register_middleware(
ConvertToMiddleware(
tracer.capture_lambda_handler(capture_response=False))
)
# Here we're writing Chalice specific middleware where for any HTTP
# APIs, we want to add the request path to our structured log message.
# This shows how we can combine both Chalice-style middleware with
# other existing tools.
@app.middleware('http')
def inject_route_info(event, get_response):
logger.structure_logs(append=True, request_path=event.path)
return get_response(event)
@app.route('/')
def index():
logger.info("In index() function, this will have a 'path' key.")
return {'hello': 'world'}
@app.route('/foo/bar')
def foobar():
logger.info("In foobar() function")
return {'foo': 'bar'}
@app.lambda_function()
def myfunction(event, context):
logger.info("In myfunction().")
tracer.put_annotation(key="Status", value="SUCCESS")
return {}
For a more detailed walkthrough of configuring Chalice with Lambda Powertools,
see
`Following serverless best practices with AWS Chalice and Lambda Powertools <https://aws.amazon.com/blogs/developer/following-serverless-best-practices-with-aws-chalice-and-lambda-powertools/>`__.
---
### Source Topics Multifile (`docs/source/topics/multifile.rst`)
Multifile Support
=================
The ``app.py`` file contains all of your view functions and route
information, but you don't have to keep all of your application
code in your ``app.py`` file.
As your application grows, you may reach out a point where you'd
prefer to structure your application in multiple files.
You can create a ``chalicelib/`` directory, and anything
in that directory is recursively included in the deployment
package. This means that you can have files besides just
``.py`` files in ``chalicelib/``, including ``.json`` files
for config, or any kind of binary assets.
Let's take a look at a few examples.
Consider the following app directory structure layout::
.
โโโ app.py
โโโ chalicelib
โย ย โโโ __init__.py
โโโ requirements.txt
Where ``chalicelib/__init__.py`` contains:
.. code-block:: python
MESSAGE = 'world'
and the ``app.py`` file contains:
.. code-block:: python
:linenos:
:emphasize-lines: 2
from chalice import Chalice
from chalicelib import MESSAGE
app = Chalice(app_name="multifile")
@app.route("/")
def index():
return {"hello": MESSAGE}
Note in line 2 we're importing the ``MESSAGE`` variable from
the ``chalicelib`` package, which is a top level directory
in our project. We've created a ``chalicelib/__init__.py``
file which turns the ``chalicelib`` directory into a python
package.
We can also use this directory to store config data. Consider
this app structure layout::
.
โโโ app.py
โโโ chalicelib
โย ย โโโ config.json
โโโ requirements.txt
With ``chalicelib/config.json`` containing::
{"message": "world"}
In our ``app.py`` code, we can load and use our config file:
.. code-block:: python
:linenos:
import os
import json
from chalice import Chalice
app = Chalice(app_name="multifile")
filename = os.path.join(
os.path.dirname(__file__), 'chalicelib', 'config.json')
with open(filename) as f:
config = json.load(f)
@app.route("/")
def index():
# We can access ``config`` here if we want.
return {"hello": config['message']}
---
### Source Topics Packaging (`docs/source/topics/packaging.rst`)
App Packaging
=============
In order to deploy your Chalice app, a zip file is created that
contains your application and all third party packages your application
requires. This file is used by AWS Lambda and is referred
to as a deployment package.
Chalice will automatically create this deployment package for you, and offers
several features to make this easier to manage. Chalice allows you to
clearly separate application specific modules and packages you are writing
from 3rd party package dependencies.
By default, Chalice will create a single zip file containing everything
necessary to deploy your application to Lambda. Chalice also has
the ability to split your code into multiple files to leverage
`AWS Lambda layers <https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html>`__.
This is discussed in the :ref:`package-3rd-party` section below.
App Directories
---------------
You have two options to structure application specific code/config:
* **app.py** - This file includes all your route information and is always
included in the deployment package.
* **chalicelib/** - This directory (if it exists) is included in the
deployment package. This is where you can add config files and additional
application modules if you prefer not to have all your app code in the
``app.py`` file.
See :doc:`multifile` for more info on the ``chalicelib/`` directory. Both the
``app.py`` and the ``chalicelib/`` directory are intended for code that you
write yourself.
.. _package-3rd-party:
3rd Party Packages
------------------
When handling third party packages, you can have Chalice manage
these files as part of the deployment package of your Lambda function,
or as a separate Lambda layer that's shared by all your Lambda functions.
See the :ref:`package-examples` section for examples of how this works.
There are two options for handling python package dependencies:
* **requirements.txt** - During the packaging process, Chalice will
install any packages it finds or can build compatible wheels for.
Specifically all pure python packages as well as all packages that upload
``manylinux`` wheel files for the architecture of your Lambda functions
(``x86_64`` by default, or ``aarch64`` when ``lambda_architecture`` is set
to ``arm64``) will be automatically installable.
* **vendor/** - The *contents* of this directory are automatically added to
your deployment package and its location in your Lambda functions will
depend on whether you are using automatic Lambda Layers, described in the
:ref:`package-auto-layers` section.
Chalice will also check for an optional ``vendor/`` directory in the project
root directory. The contents of this directory are automatically included in
the top level of the deployment package (see :ref:`package-examples` for
specific examples). The ``vendor/`` directory is helpful in these scenarios:
* You need to include custom packages or binary content that is not accessible
via ``pip``. These may be internal packages that aren't public.
* Wheel files are not available for a package you need from pip.
* A package is installable with ``requirements.txt`` but has optional c
extensions. Chalice can build the dependency without the c extensions, but
if you want better performance you can vendor a version that is compiled.
As a general rule of thumb, code that you write goes in either ``app.py`` or
``chalicelib/``, and dependencies are either specified in ``requirements.txt``
or placed in the ``vendor/`` directory.
.. _package-auto-layers:
Automatic Lambda Layers
~~~~~~~~~~~~~~~~~~~~~~~
By default, Chalice will create a single zip file that contains all the
code needed to run your application.
You can set the :ref:`automatic-layer-option` in your ``.chalice/config.json``
file which will instruct Chalice to create your 3rd party packages as a
separate Lambda layer.
There are several benefits to this approach:
* The layer is created once and then shared across all Lambda functions in
your application.
* When creating or updating a Lambda function, you send the entire contents
of the zip file that contains your app. This is repeated for each Lambda
function. As a result, there is unnecessary time and network bandwidth
used to send the same 3rd party dependencies for each Lambda function.
When using layers, Chalice will specify the layer ARN when creating or
updating your Lambda function, which cuts down on the time it takes
to package and deploy your application.
* Saves storage space in Lambda. There is a 75GB maximum size for all
your Lambda functions. If you're not using layers, each Lambda function
stores its own copies of your 3rd party dependencies.
We recommend setting ``"automatic_layer": true`` in your
``.chalice/config.json`` due to these benefits.
Migrating to ``"automatic_layer": true`` is mostly backwards compatible
with one notable exception: the location of the vendor files is different.
When not using automatic layers, any files placed in ``vendor/`` will be
available in your CWD of your application. However, when using layers,
these files will be unzipped to ``/opt/python/lib/pythonX.Y/site-packages``.
If you are using the ``vendor/`` directory to include custom built python
packages then this change is transparent as that directory is automatically
added to the python path. However, if you are trying to read a file from
``vendor/`` directly, then this will no longer work. For example, if you
have::
.
โโโ app.py
โโโ vendor
โโโ myimage.png
And your ``app.py`` attempts to read this file:
.. code-block:: python
@app.lambda_function()
def handler(event, context):
with open('myimage.png') as f:
do_something(f)
This code will no longer work. You have two options. You can either place
static assets in ``chalicelib/`` or you'll have to check both directories for
your file::
'/opt/python/lib/python%s.%s/site-packages' % sys.version_info[:2]
.. code-block:: python
@app.lambda_function()
def handler(event, context):
with open_vendor_file('myimage.png') as f:
do_something(f)
def open_vendor_file(filename):
directories = [
'.',
'/opt/python/lib/python%s.%s/site-packages' % sys.version_info[:2]
]
for dirname in directories:
full_path = os.path.join(dirname, filename)
if os.path.isfile(full_path):
return open(full_path)
Environment Variables
---------------------
As part of the packaging and deployment process, Chalice will import your
``app.py`` file. This will result in any top level module code being
executed. This can sometimes have undesirable behavior.
When running any Chalice CLI commands, a ``AWS_CHALICE_CLI_MODE`` environment
variable is set. You can check if this env var is set in your ``app.py``
if you have code that you don't want to run whenever your app is packaged
and deployed.
.. code-block:: python
import os
app = Chalice(app_name='testimport')
expensive_connection = None
if 'AWS_CHALICE_CLI_MODE' not in os.environ:
# We're running in Lambda, we want to start up
# our connection to our DB.
expensive_connection = ConnectToDB()
Chalice will also set any environment variables specified in your global or
stage specific configuration whenever your app is packaged and deployed.
Per-Lambda function environment variables are not set when importing your app
(this would require importing your application for each Lambda function). For
example, given the config below you would be able to access the ``STAGE_VAR``
environment variable but not the ``PER_FUNCTION`` variable during the
building/packaging process when Chalice imports your application. This
can be useful if you want to move configuration or resource names out of
your app.py file.
::
{
"stages": {
"dev": {
"environment_variables": {
"STAGE_VAR": "stage-var"
}
"api_gateway_stage": "api",
"lambda_functions": {
"foo": {
"environment_variables": {"PER_FUNCTION": "per-function"}
}
}
}
},
"version": "2.0",
"app_name": "demo"
}
This only applies to the packaging stage. When the ``foo`` function is invoked
on Lambda, the ``PER_FUNCTION`` environment variable will be set as expected.
.. _package-examples:
Examples
--------
Suppose I have the following app structure::
.
โโโ app.py
โโโ chalicelib
โย ย โโโ __init__.py
โย ย โโโ utils.py
โโโ requirements.txt
โโโ vendor
ย โโโ myimage.png
โโโ internalpackage
โโโ __init__.py
And the ``requirements.txt`` file had one requirement::
$ cat requirements.txt
sortedcontainers==1.5.4
With the default behavior of not using layers (``"automatic_layer": false``),
the final deployment package directory structure would look like this::
deployment.zip
.
โโโ app.py
โโโ chalicelib
โย ย โโโ __init__.py
โย ย โโโ utils.py
โโโ myimage.png
โโโ internalpackage
โย ย โโโ __init__.py
โโโ sortedcontainers
โโโ __init__.py
This directory structure is then zipped up and sent to AWS Lambda during the
deployment process. Suppose our application had two Lambda functions.
Each Lambda function has its own copy of the application deployment package,
as shown in the architecture diagram below.
.. image:: ../img/no-auto-layer.png
:width: 50%
:align: center
:alt: Default behavior with no layers.
If you are using ``"automatic_layer": true``, then two zip files will be
created. The deployment package used for the Lambda function will be::
deployment.zip
.
โโโ app.py
โโโ chalicelib
ย ย โโโ __init__.py
ย ย โโโ utils.py
And the zip file for the shared lambda layer will look like this::
layer-deployment.zip
.
โโโ python
โโโ lib
โโโ python3.7
โโโ site-packages
โโโ myimage.png
โโโ internalpackage
โย ย โโโ __init__.py
โโโ sortedcontainers
โโโ __init__.py
Below is an updated diagram of the same Chalice application using automatic
layers. Both functions now share the same Lambda layer that contains the third
party packages used by the application.
.. image:: ../img/auto-layer.png
:width: 80%
:align: center
:alt: Shared layer for 3rd party code.
Cryptography Example
--------------------
.. note::
Since the original version of this example was written, cryptography has
released version 2.0 which does have manylinux1 wheels available. This
means if you want to use cryptography in a Chalice app all you need to
do is add ``cryptography`` or ``cryptography>=2.0`` in your
requirements.txt file.
This example will use version 1.9 of Cryptography
because it is a good example of a library with C extensions and no wheel
files.
Below shows an example of how to use the
`cryptography 1.9 <https://pypi.org/project/cryptography/1.9/>`__ package
in a Chalice app for the ``python3.6`` lambda environment.
Suppose you are on a Mac or Windows and want to deploy a Chalice app that
depends on the ``cryptography==1.9`` package. If you simply add it to your
``requirements.txt`` file and try to deploy it with ``chalice deploy`` you will
get the following warning during deployment::
$ cat requirements.txt
cryptography==1.9
$ chalice deploy
Updating IAM policy.
Updating lambda function...
Creating deployment package.
Could not install dependencies:
cryptography==1.9
You will have to build these yourself and vendor them in
the chalice vendor folder.
Your deployment will continue but may not work correctly
if missing dependencies are not present. For more information:
http://aws.github.io/chalice/topics/packaging.html
This happened because the ``cryptography`` version 1.9 does not have wheel
files available on PyPi, and has C extensions. Since we are not on the same
platform as AWS Lambda, the compiled C extensions Chalice built were not
compatible. To get around this we are going to leverage the ``vendor/``
directory, and build the ``cryptography`` package on a compatible linux system.
You can do this yourself by building ``cryptography`` on an Amazon Linux
instance running in EC2. All of the following commands were run inside a
``python 3.6`` virtual environment.
* Download the source first::
$ pip download cryptography==1.9
This will download all the requirements into the current working directory.
The directory should have the following contents:
* ``asn1crypto-0.22.0-py2.py3-none-any.whl``
* ``cffi-1.10.0-cp36-cp36m-manylinux1_x86_64.whl``
* ``cryptography-1.9.tar.gz``
* ``idna-2.5-py2.py3-none-any.whl``
* ``pycparser-2.17.tar.gz``
* ``six-1.10.0-py2.py3-none-any.whl``
This is a complete set of dependencies required for the cryptography package.
Most of these packages have wheels that were downloaded, which means they can
simply be put in the ``requirements.txt`` and Chalice will take care of
downloading them. That leaves ``cryptography`` itself and ``pycparser`` as
the only two that did not have a wheel file available for download.
* Next build the ``cryptography`` source package into a wheel file::
$ pip wheel cryptography-1.9.tar.gz
This will take a few seconds and build a wheel file for both ``cryptography``
and ``pycparser``. The directory should now have two additional wheel files:
* ``cryptography-1.9-cp36-cp36m-linux_x86_64.whl``
* ``pycparser-2.17-py2.py3-none-any.whl``
The ``cryptography`` wheel file has been built with a compatible
architecture for Lambda (``linux_x86_64``) and the ``pycparser`` has been
built for ``any`` architecture which means it can also be automatically
packaged by Chalice if it is listed in the ``requirements.txt`` file.
* Download the ``cryptography`` wheel file from the Amazon Linux instance and
unzip it into the ``vendor/`` directory in the root directory of your Chalice
app.
You should now have a project directory that looks like this::
$ tree
.
โโโ app.py
โโโ requirements.txt
โโโ vendor
โโโ cryptography
โย ย โโโ ... Lots of files
โ
โโโ cryptography-1.9.dist-info
โโโ DESCRIPTION.rst
โโโ METADATA
โโโ RECORD
โโโ WHEEL
โโโ entry_points.txt
โโโ metadata.json
โโโ top_level.txt
The ``requirements.txt`` file should look like this::
$ cat requirements.txt
cffi==1.10.0
six==1.10.0
asn1crypto==0.22.0
idna==2.5
pycparser==2.17
In your ``app.py`` file you can now import ``cryptography``, and these
dependencies will all get included when the ``chalice deploy`` command is
run.
---
### Source Topics Purelambda (`docs/source/topics/purelambda.rst`)
=====================
Pure Lambda Functions
=====================
Chalice provides abstractions over AWS Lambda functions, including:
* An API handler that coordinates with API Gateway for creating rest APIs.
* A custom authorizer that allows you to integrate custom auth logic in your
rest API.
* A scheduled event that includes managing the CloudWatch Event rules, targets,
and permissions.
However, chalice also supports managing pure Lambda functions that don't have
any abstractions built on top. This is useful if you want to create a Lambda
function for something that's not supported by chalice or if you just want to
create Lambda functions but don't want to manage handling dependencies and
deployments yourself.
In order to do this, you can use the :meth:`Chalice.lambda_function` decorator
to denote that this python function is a pure lambda function that should
be invoked as is, without any input or output mapping. When you use
this function, you must provide a function that maps to the same function
signature expected by AWS Lambda as `defined here`_.
Let's look at an example.
.. code-block:: python
app = chalice.Chalice(app_name='foo')
@app.route('/')
def index():
return {'hello': 'world'}
@app.lambda_function()
def custom_lambda_function(event, context):
# Anything you want here.
return {}
@app.lambda_function(name='MyFunction')
def other_lambda_function(event, context):
# Anything you want here.
return {}
In this example, we've updated the starter hello world app with
two extra Lambda functions. When you run ``chalice deploy`` Chalice will create
three Lambda functions. The first lambda function is for the API handler
used by API gateway. The second and third lambda functions will be pure lambda
functions. These two additional lambda functions won't be hooked up to anything.
You'll need to manage connecting them to any additional AWS Resources on your
own.
.. _defined here: https://docs.aws.amazon.com/lambda/latest/dg/python-programming-model-handler-types.html
---
### Source Topics Pyversion (`docs/source/topics/pyversion.rst`)
Python Version Support
======================
Chalice supports Python 3.10 through Python 3.15. You can see the list of
supported python versions for Lambda in their
`docs <https://docs.aws.amazon.com/lambda/latest/dg/lambda-python.html>`__.
Chalice will automatically pick which version of python to use for Lambda
based on the major/minor version of python you are using. You don't have to
explicitly configure which version of python you want to use. For example::
$ python --version
Python 3.10.20
$ chalice new-project test-versions
$ cd test-versions
$ chalice package test-package
$ grep -C 3 python test-package/sam.json
"APIHandler": {
"Type": "AWS::Serverless::Function",
"Properties": {
"Runtime": "python3.10",
"Handler": "app.app",
"CodeUri": "./deployment.zip",
"Events": {
# Similarly, if we were to run "chalice deploy" we'd
# use python3.10 for the runtime.
$ chalice --debug deploy
Initiating first time deployment...
Deploying to: dev
...
"Runtime":"python3.10"
...
https://rest-api-id.execute-api.us-west-2.amazonaws.com/api/
In the example above, we're using python 3.10.20 so chalice automatically
selects the ``python3.10`` runtime for lambda. If we were using python 3.15,
chalice would automatically select ``python3.15`` as the runtime.
Chalice will emit a warning if the minor version does not match a python
version supported by Lambda. Chalice will select the closest Lambda version
in this scenario, as shown in the table below.
We strongly encourage you to develop your application using the same
major/minor version of python you plan on using on AWS Lambda.
Changing Python Runtime Versions
================================
The version of the python runtime to use in AWS Lambda can be reconfigured
whenever you deploy your chalice app. This allows you to migrate to newer
Python versions in AWS Lambda by creating a new virtual environment that uses
python3. For example, suppose you have an existing chalice app that uses
Python 3.10 ::
$ python --version
Python 3.10.20
$ chalice deploy
...
https://endpoint/api
To upgrade the application to use Python 3.15, create a python3 virtual
environment and redeploy.
::
$ deactivate
$ python3 -m venv /tmp/venv3
$ source /tmp/venv3/bin/activate
$ python --version
Python 3.15.0
$ chalice deploy
...
---
### Source Topics Routing (`docs/source/topics/routing.rst`)
Routing
=======
The :meth:`Chalice.route` method is used to construct which routes
you want to create for your API. The concept is the same
mechanism used by `Flask <https://palletsprojects.com/p/flask/>`__ and
`bottle <http://bottlepy.org/docs/dev/index.html>`__.
You decorate a function with ``@app.route(...)``, and whenever
a user requests that URL, the function you've decorated is called.
For example, suppose you deployed this app:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.route('/')
def index():
return {'view': 'index'}
@app.route('/a')
def a():
return {'view': 'a'}
@app.route('/b')
def b():
return {'view': 'b'}
If you go to ``https://endpoint/``, the ``index()`` function would be called.
If you went to ``https://endpoint/a`` and ``https://endpoint/b``, then the
``a()`` and ``b()`` function would be called, respectively.
.. note::
Do not end your route paths with a trailing slash. If you do this, the
``chalice deploy`` command will raise a validation error.
You can also create a route that captures part of the URL. This captured value
will then be passed in as arguments to your view function:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.route('/users/{name}')
def users(name):
return {'name': name}
If you then go to ``https://endpoint/users/james``, then the view function
will be called as: ``users('james')``. The parameters are passed as
keyword parameters based on the name as they appear in the URL. The argument
names for the view function must match the name of the captured
argument:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.route('/a/{first}/b/{second}')
def users(first, second):
return {'first': first, 'second': second}
Other Request Metadata
----------------------
The route path can only contain ``[a-zA-Z0-9._-]`` chars and curly braces for
parts of the URL you want to capture. You do not need to model other parts of
the request you want to capture, including headers and query strings. Within
a view function, you can introspect the current request using the
:attr:`app.current_request <Chalice.current_request>` attribute. This also
means you cannot control the routing based on query strings or headers.
Here's an example for accessing query string data in a view function:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.route('/users/{name}')
def users(name):
result = {'name': name}
if app.current_request.query_params.get('include-greeting') == 'true':
result['greeting'] = 'Hello, %s' % name
return result
In the function above, if the user provides a ``?include-greeting=true`` in the
HTTP request, then an additional ``greeting`` key will be returned::
$ http https://endpoint/api/users/bob
{
"name": "bob"
}
$ http https://endpoint/api/users/bob?include-greeting=true
{
"greeting": "Hello, bob",
"name": "bob"
}
---
### Source Topics Sdks (`docs/source/topics/sdks.rst`)
SDK Generation
==============
The ``@app.route(...)`` information you provide chalice allows
it to create corresponding routes in API Gateway. One of the benefits of this
approach is that we can leverage API Gateway's SDK generation process.
Chalice offers a ``chalice generate-sdk`` command that will automatically
generate an SDK based on your declared routes.
.. note::
The only supported language at this time is javascript.
Keep in mind that chalice itself does not have any logic for generating
SDKs. The SDK generation happens service side in `API Gateway`_, the
``chalice generate-sdk`` is just a high level wrapper around that
functionality.
To generate an SDK for a chalice app, run this command from the project
directory::
$ chalice generate-sdk /tmp/sdk
You should now have a generated javascript sdk in ``/tmp/sdk``.
API Gateway includes a ``README.md`` as part of its SDK generation
which contains details on how to use the javascript SDK.
Example
-------
Suppose we have the following chalice app:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='sdktest')
@app.route('/', cors=True)
def index():
return {'hello': 'world'}
@app.route('/foo', cors=True)
def foo():
return {'foo': True}
@app.route('/hello/{name}', cors=True)
def hello_name(name):
return {'hello': name}
@app.route('/users/{user_id}', methods=['PUT'], cors=True)
def update_user(user_id):
return {"msg": "fake updated user", "userId": user_id}
Let's generate a javascript SDK and test it out in the browser.
Run the following command from the project dir::
$ chalice generate-sdk /tmp/sdkdemo
$ cd /tmp/sdkdemo
$ ls -la
-rw-r--r-- 1 jamessar r 3227 Nov 21 17:06 README.md
-rw-r--r-- 1 jamessar r 9243 Nov 21 17:06 apigClient.js
drwxr-xr-x 6 jamessar r 204 Nov 21 17:06 lib
You should now be able to follow the instructions from API Gateway in the
``README.md`` file. Below is a snippet that shows how the generated
javascript SDK methods correspond to the ``@app.route()`` calls in chalice.
.. code-block:: html
<script type="text/javascript">
// Below are examples of how the javascript SDK methods
// correspond to chalice @app.routes()
var apigClient = apigClientFactory.newClient();
// @app.route('/')
apigClient.rootGet().then(result => {
document.getElementById('root-get').innerHTML = JSON.stringify(result.data);
});
// @app.route('/foo')
apigClient.fooGet().then(result => {
document.getElementById('foo-get').innerHTML = JSON.stringify(result.data);
});
// @app.route('/hello/{name}')
apigClient.helloNameGet({name: 'jimmy'}).then(result => {
document.getElementById('helloname-get').innerHTML = JSON.stringify(result.data);
});
// @app.route('/users/{user_id}', methods=['PUT'])
apigClient.usersUserIdPut({user_id: '123'}, 'body content').then(result => {
document.getElementById('users-userid-put').innerHTML = JSON.stringify(result.data);
});
</script>
Example HTML File
~~~~~~~~~~~~~~~~~
If you want to try out the example above, you can use the following index.html
page to test:
.. code-block:: html
<!DOCTYPE html>
<html lang="en">
<head>
<title>SDK Test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css">
<script type="text/javascript" src="lib/axios/dist/axios.standalone.js"></script>
<script type="text/javascript" src="lib/CryptoJS/rollups/hmac-sha256.js"></script>
<script type="text/javascript" src="lib/CryptoJS/rollups/sha256.js"></script>
<script type="text/javascript" src="lib/CryptoJS/components/hmac.js"></script>
<script type="text/javascript" src="lib/CryptoJS/components/enc-base64.js"></script>
<script type="text/javascript" src="lib/url-template/url-template.js"></script>
<script type="text/javascript" src="lib/apiGatewayCore/sigV4Client.js"></script>
<script type="text/javascript" src="lib/apiGatewayCore/apiGatewayClient.js"></script>
<script type="text/javascript" src="lib/apiGatewayCore/simpleHttpClient.js"></script>
<script type="text/javascript" src="lib/apiGatewayCore/utils.js"></script>
<script type="text/javascript" src="apigClient.js"></script>
<script type="text/javascript">
// Below are examples of how the javascript SDK methods
// correspond to chalice @app.routes()
var apigClient = apigClientFactory.newClient();
// @app.route('/')
apigClient.rootGet().then(result => {
document.getElementById('root-get').innerHTML = JSON.stringify(result.data);
});
// @app.route('/foo')
apigClient.fooGet().then(result => {
document.getElementById('foo-get').innerHTML = JSON.stringify(result.data);
});
// @app.route('/hello/{name}')
apigClient.helloNameGet({name: 'jimmy'}).then(result => {
document.getElementById('helloname-get').innerHTML = JSON.stringify(result.data);
});
// @app.route('/users/{user_id}', methods=['PUT'])
apigClient.usersUserIdPut({user_id: '123'}, 'body content').then(result => {
document.getElementById('users-userid-put').innerHTML = JSON.stringify(result.data);
});
</script>
</head>
<h5>result of rootGet()</h5>
<h5>result of fooGet()</h5>
<h5>result of helloNameGet({name: 'jimmy'})</h5>
<h5>result of usersUserIdPut({user_id: '123'})</h5>
</html>
.. _API Gateway: https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-generate-sdk.html
---
### Source Topics Stages (`docs/source/topics/stages.rst`)
Chalice Stages
==============
Chalice has the concept of stages, which are completely
separate sets of AWS resources. When you first create a chalice
project and run commands such as ``chalice deploy`` and ``chalice url``,
you don't have to specify any stage values or stage configuration.
This is because chalice will use a stage named ``dev`` by default.
You may eventually want to have multiple stages of your application. A
common configuration would be to have a ``dev``, ``beta`` and ``prod``
stage. A ``dev`` stage would be used by developers to test out new
features. Completed features would be deployed to ``beta``, and the
``prod`` stage would be used for serving production traffic.
Chalice can help you manage this.
To create a new chalice stage, specify the ``--stage`` argument.
If the stage does not exist yet, it will be created for you::
$ chalice deploy --stage prod
By creating a new chalice stage, a new API Gateway rest API, Lambda
function, and potentially (depending on config settings) a new IAM role
will be created for you.
Example
-------
Let's say we have a new app::
$ chalice new-project myapp
$ cd myapp
$ chalice deploy
...
https://mmnkdi.execute-api.us-west-2.amazonaws.com/api/
We've just created our first stage, ``dev``. We can iterate on our
application and continue to run ``chalice deploy`` to deploy our code
to the ``dev`` stage. Let's say we want to now create a ``prod`` stage.
To do this, we can run::
$ chalice deploy --stage prod
...
https://wk9fhx.execute-api.us-west-2.amazonaws.com/api/
We now have two completely separate rest APIs::
$ chalice url --stage dev
https://mmnkdi.execute-api.us-west-2.amazonaws.com/api/
$ chalice url --stage prod
https://wk9fhx.execute-api.us-west-2.amazonaws.com/api/
Additionally, we can see all our deployed values by looking
at the ``.chalice/deployed/dev.json`` or ``.chalice/deployed/prod.json`` files::
$ cat .chalice/deployed/dev.json
{
"resources": [
{
"name": "api_handler",
"resource_type": "lambda_function",
"lambda_arn": "arn:aws:lambda:...:function:myapp-dev"
},
{
"name": "rest_api",
"resource_type": "rest_api",
"rest_api_id": "wk9fhx",
"rest_api_url": "https://wk9fhx.execute-api.us-west-2.amazonaws.com/api/"
}
],
"schema_version": "2.0",
"backend": "api"
}
$ cat .chalice/deployed/prod.json
{
"resources": [
{
"name": "api_handler",
"resource_type": "lambda_function",
"lambda_arn": "arn:aws:lambda:...:function:myapp-prod"
},
{
"name": "rest_api",
"resource_type": "rest_api",
"rest_api_id": "mmnkdi",
"rest_api_url": "https://mmnkdi.execute-api.us-west-2.amazonaws.com/api/"
}
],
"schema_version": "2.0",
"backend": "api"
}
---
### Source Topics Testing (`docs/source/topics/testing.rst`)
Testing
=======
Chalice provides a :ref:`test client <testing-api>` in ``chalice.test`` that
you can use to test your Chalice applications. This client lets you invoke
Lambda function and event handlers directly, as well as test your REST APIs.
.. _testing-lambda-functions:
Lambda Functions
----------------
To test lambda functions, use the
:meth:`Client.lambda_.invoke <TestLambdaClient.invoke>` method. The
test client is intended to be used as a context manager. For example,
given this sample app:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name="testclient")
@app.lambda_function()
def foo(event, context):
return {'hello': 'world'}
@app.lambda_function()
def bar(event, context):
return {'event': event}
Here's how you can test these functions with the test client. In our
example, we'll be using `pytest <https://docs.pytest.org/en/stable/>`__,
but the Chalice test client will work with any testing framework.
We'll create a new ``tests/`` directory and create a ``tests/__init__.py``
and a ``tests/test_app.py`` file.
::
$ mkdir tests
$ touch tests/{__init__.py,test_app.py}
The ``tests/test_app.py`` file should have the following contents:
.. code-block:: python
from chalice.test import Client
from app import app
def test_foo_function():
with Client(app) as client:
result = client.lambda_.invoke('foo')
assert result.payload == {'hello': 'world'}
def test_bar_function():
with Client(app) as client:
result = client.lambda_.invoke(
'bar', {'my': 'event'})
assert result.payload == {'event': {'my': 'event'}}
Now we can run our tests with ``pytest``::
$ pip install pytest
$ py.test tests/test_app.py
========================= test session starts ==========================
platform darwin -- Python 3.7.3, pytest-5.3.1, py-1.5.3, pluggy-0.12.0
rootdir: /tmp/testclient
plugins: hypothesis-4.43.1, cov-2.8.1
collected 2 items
test_app.py .. [100%]
========================= 2 passed in 0.32s ============================
.. note::
See the :ref:`testing-pytest-fixtures` section for how to use pytest
fixtures with the Chalice test client.
For testing Lambda functions that are connected to specific events,
you can use the :attr:`Client.events` attribute to generate
sample events. For example:
.. code-block:: python
from chalice import Chalice
@app.on_sns_message(topic='mytopic')
def foo(event):
return {'message': event.message}
# Test code
from chalice.test import Client
def test_sns_handler():
with Client(app) as client:
response = client.lambda_.invoke(
"foo",
client.events.generate_sns_event(message="hello world")
)
assert response.payload == {'message': 'hello world'}
Environment Variables
~~~~~~~~~~~~~~~~~~~~~
The Chalice test client will also configure any environment variables you
have configured with your Lambda functions in your ``.chalice/config.json``
file. For example, suppose you had these config file:
.. code-block:: json
{
"version": "2.0",
"app_name": "testenv",
"stages": {
"prod": {
"api_gateway_stage": "api",
"environment_variables": {
"MY_ENV_VAR": "TOP LEVEL"
},
"lambda_functions": {
"bar": {
"environment_variables": {
"MY_ENV_VAR": "OVERRIDE"
}
}
}
}
}
}
These sets a ``MY_ENV_VAR`` environment variable for the ``prod`` stage.
The ``bar`` function overrides this environment variable with its own
custom value. To test this, we need to specify the ``prod`` stage when
we create our test client:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name="testclient")
@app.lambda_function()
def foo(event, context):
return {'value': os.environ.get('MY_ENV_VAR')}
@app.lambda_function()
def bar(event, context):
return {'value': os.environ.get('MY_ENV_VAR')}
# Test code
from chalice.test import Client
def test_foo_function():
with Client(app, stage_name='prod') as client:
result = client.lambda_.invoke('foo')
assert result.payload == {'value': 'TOP LEVEL'}
def test_bar_function():
with Client(app) as client:
result = client.lambda_.invoke('bar')
assert result.payload == {'value': 'OVERRIDE'}
REST APIs
---------
You can test your REST API with the Chalice test client using the
:attr:`Client.http` attribute. For example, given this REST API:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name="testclient")
@app.route('/')
def index():
return {'hello': 'world'}
You can test this route with:
.. code-block:: python
from chalice.test import Client
from app import app
def test_index():
with Client(app) as client:
response = client.http.get('/')
assert response.json_body == {'hello': 'world'}
If you want to access the response body's raw bytes, you can use the
``body`` attribute:
.. code-block:: python
from chalice.test import Client
from app import app
def test_index():
with Client(app) as client:
response = client.http.get('/')
assert response.body == b'{"hello":"world"}'
You can also test other HTTP methods by using the corresponding
``post()``, ``put()``, ``delete()``, etc. method calls.
.. code-block:: python
import json
from chalice import Chalice
app = Chalice(app_name="testclient")
@app.route('/', methods=['POST'])
def index()
return app.current_request.json_body
def test_index():
with Client(app) as client:
response = client.http.post(
'/myview',
headers={'Content-Type':'application/json'},
body=json.dumps({'example':'json'})
)
assert response.json_body == {'example': 'json'}
You can also test builtin authorizers with the test client:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name="testclient")
@app.authorizer()
def myauth(event)
if event.token == 'allow':
return AuthResponse(['*'], principal_id='id')
return AuthResponse([], principal_id='noone')
@app.route('/needs-auth', authorizer=myauth)
def needs_auth()
return {'success': True}
# Test code:
from chalice.test import Client
def test_needs_auth():
with Client(app) as client:
response = client.http.get(
'/needs-auth', headers={'Authorization': 'allow'})
assert response.json_body == {'success': True}
assert client.http.get(
'/needs-auth',
headers={'Authorization': 'deny'}).status_code == 403
.. _testing-boto3-client-calls:
Testing Boto3 Client Calls
--------------------------
If your event handlers are making AWS API calls using boto3 or botocore,
you can use the `botocore stubber
<https://botocore.amazonaws.com/v1/documentation/api/latest/reference/stubber.html>`__
to test your API calls. For example, suppose we have an app that makes an
API call to Amazon Rekognition whenever an object is uploaded to S3:
.. code-block:: python
import boto3
from chalice import Chalice
app = Chalice(app_name='testclient')
_REKOGNITION_CLIENT = None
def get_rekognition_client():
global _REKOGNITION_CLIENT
if _REKOGNITION_CLIENT is None:
_REKOGNITION_CLIENT = boto3.client('rekognition')
return _REKOGNITION_CLIENT
@app.on_s3_event(bucket='mybucket',
events=['s3:ObjectCreated:*'])
def handle_object_created(event):
client = get_rekognition_client()
response = client.detect_labels(
Image={
'S3Object': {
'Bucket': event.bucket,
'Name': event.key,
},
},
MinConfidence=50.0
)
labels = [label['Name'] for label in response['Labels']]
# In the real app we'd now do something with these labels
# (e.g. store than in a database so we can query them later).
return labels
To test this, we'll combine the botocore stubber and the Chalice test client:
.. code-block:: python
from chalice.test import Client
import app
from botocore.stub import Stubber
def test_calls_rekognition():
client = app.get_rekognition_client()
stub = Stubber(client)
stub.add_response(
'detect_labels',
expected_params={
'Image': {
'S3Object': {
'Bucket': 'mybucket',
'Name': 'mykey',
}
},
'MinConfidence': 50.0,
},
service_response={
'Labels': [
{'Name': 'Dog', 'Confidence': 75.0},
{'Name': 'Mountain', 'Confidence': 80.0},
{'Name': 'Snow', 'Confidence': 85.0},
]
},
)
with stub:
with Client(app.app) as client:
event = client.events.generate_s3_event(
bucket='mybucket', key='mykey')
response = client.lambda_.invoke('handle_object_created', event)
assert response.payload == ['Dog', 'Mountain', 'Snow']
stub.assert_no_pending_responses()
In the testcase above, we first tell the stubber what API call we're expecting,
along with the parameters we'll send and the response we expect back from the
Rekognition service. Next we use the ``with stub:`` line to activate our stubs.
This also ensures that when our test exits that we'll deactivate the stubs for
this client. Now we the ``client.lambda_.invoke`` method is called, our
stubbed client will return the preconfigured response data instead of making
an actual API call to the Rekognition service.
.. _testing-pytest-fixtures:
Pytest Fixtures
---------------
Both the Botocore stubber and the Chalice test client are used within
a context manager. In our previous example, this resulted in multiple
levels of nesting, which is required for every test we write. If you're
using pytest as your test framework, you can create
`test fixtures <https://docs.pytest.org/en/stable/fixture.html>`__ to
reduce the boiler plate code. Let's rewrite several of these tests to use
pytest fixtures.
First we'll create a test fixture for the Chalice test client:
.. code-block:: python
import app
from pytest import fixture
from chalice.test import Client
@fixture
def test_client():
with Client(app.app) as client:
yield client
Now our original tests for the ``foo`` and ``bar`` Lambda functions
from the :ref:`testing-lambda-functions` section can be rewritten
to use this fixture:
.. code-block:: python
def test_foo_function(test_client):
result = test_client.lambda_.invoke('foo')
assert result.payload == {'hello': 'world'}
def test_bar_function(test_client):
result = test_client.lambda_.invoke(
'bar', {'my': 'event'})
assert result.payload == {'event': {'my': 'event'}}
We can also create a fixture for the botocore stubber. This allows us
to rewrite the ``test_calls_rekognition()`` test from the
:ref:`previous section <testing-boto3-client-calls>` in a more simplified
manner. Below is the entire test file using both the botocore and Chalice
test client fixtures:
.. code-block:: python
import app
from pytest import fixture
from chalice.test import Client
@fixture
def test_client():
with Client(app.app) as client:
yield client
@fixture
def rekognition_stub():
client = app.get_rekognition_client()
stub = Stubber(client)
with stub:
yield stub
def test_calls_rekognition(test_client, rekognition_stub):
rekognition_stub.add_response(
'detect_labels',
expected_params={
'Image': {
'S3Object': {
'Bucket': 'mybucket',
'Name': 'mykey',
}
},
'MinConfidence': 50.0,
},
service_response={
'Labels': [
{'Name': 'Dog', 'Confidence': 75.0},
{'Name': 'Mountain', 'Confidence': 80.0},
{'Name': 'Snow', 'Confidence': 85.0},
]
},
)
event = test_client.events.generate_s3_event(
bucket='mybucket', key='mykey')
response = test_client.lambda_.invoke('handle_object_created', event)
assert response.payload == ['Dog', 'Mountain', 'Snow']
stub.assert_no_pending_responses()
Next Steps
----------
For reference documentation on the methods and attributes of the Chalice test
client, see the :ref:`test client <testing-api>` section in the API
documentation.
---
### Source Topics Tf (`docs/source/topics/tf.rst`)
Terraform Support
=================
When you run ``chalice deploy``, chalice will deploy your application using the
`AWS SDK for Python <http://boto3.readthedocs.io/en/docs/>`__. Chalice also
provides functionality that allows you to manage deployments yourself using
terraform. This is provided via the ``chalice package --pkg-format terraform``
command.
When you run this command, chalice will generate the AWS Lambda
deployment package that contains your application and a `Terraform
<https://www.terraform.io>`__ configuration file. You can then use the
terraform cli to deploy your chalice application.
Considerations
--------------
Using the ``chalice package`` command is useful when you don't want to
use ``chalice deploy`` to manage your deployments. There's several reasons
why you might want to do this:
* You have pre-existing infrastructure and tooling set up to manage
Terraform deployments.
* You want to integrate with other Terraform resources to manage all
your application resources, including resources outside of your
chalice app.
* You'd like to integrate with `AWS CodePipeline
<https://aws.amazon.com/codepipeline/>`__ to automatically deploy
changes when you push to a git repo.
Keep in mind that you can't switch between ``chalice deploy`` and
``chalice package`` + Terraform for deploying your app.
If you choose to use ``chalice package`` and Terraform to deploy
your app, you won't be able to switch back to ``chalice deploy``.
Running ``chalice deploy`` would create an entirely new set of AWS
resources (API Gateway Rest API, AWS Lambda function, etc).
Example
-------
In this example, we'll create a chalice app and deploy it using
the AWS CLI.
First install the necessary packages::
$ virtualenv /tmp/venv
$ . /tmp/venv/bin/activate
$ pip install chalice awscli
$ chalice new-project test-tf-deploy
$ cd test-tf-deploy
At this point we've installed chalice and the AWS CLI and we have
a basic app created locally. Next we'll run the ``package`` command::
$ chalice package --pkg-format terraform /tmp/packaged-app/
Creating deployment package.
$ ls -la /tmp/packaged-app/
-rw-r--r-- 1 j wheel 3355270 May 25 14:20 deployment.zip
-rw-r--r-- 1 j wheel 3068 May 25 14:20 chalice.tf.json
$ unzip -l /tmp/packaged-app/deployment.zip | tail -n 5
17292 05-25-17 14:19 chalice/app.py
283 05-25-17 14:19 chalice/__init__.py
796 05-25-17 14:20 app.py
-------- -------
9826899 723 files
As you can see in the above example, the ``package --pkg-format``
command created a directory that contained two files, a
``deployment.zip`` file, which is the Lambda deployment package, and a
``chalice.tf.json`` file, which is the Terraform template that can be
deployed using Terraform. Next we're going to use the Terraform CLI
to deploy our app.
Note terraform will deploy run against all terraform files in this
directory, so we can add additional resources for our application by
adding terraform additional files here. The Chalice terraform template
includes two static data values (`app` and `stage` names) that we can
optionally use when constructing these additional resources,
ie. `${data.null_data_source.chalice.outputs.app}`
First let's run Terraform init to install the AWS Terraform Provider::
$ cd /tmp/packaged-app
$ terraform init
Now we can deploy our app using the ``terraform apply`` command::
$ terraform apply
data.aws_region.chalice: Refreshing state...
data.aws_caller_identity.chalice: Refreshing state...
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
... (omit plan output)
Plan: 14 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
... (omit apply output)
Apply complete! Resources: 14 added, 0 changed, 0 destroyed.
Outputs:
EndpointURL = https://7bnxriulj5.execute-api.us-east-1.amazonaws.com/dev
RestApiId = 7bnxriulj5
This will take a minute to complete, but once it's done, the endpoint url
will be available as an output which we can then curl::
$ http "$(terraform output EndpointURL)"
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 18
Content-Type: application/json
...
{
"hello": "world"
}
---
### Source Topics Views (`docs/source/topics/views.rst`)
Views
=====
A view function in chalice is the function attached to an
``@app.route()`` decorator. In the example below, ``index``
is the view function:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.route('/')
def index():
return {'view': 'index'}
View Function Parameters
------------------------
A view function's parameters correspond to the number of captured
URL parameters specified in the ``@app.route`` call. In the example above,
the route ``/`` specifies no captured parameters so the ``index`` view
function accepts no parameters. However, in the view function below,
a single URL parameter, ``{city}`` is specified, so the view function
must accept a single parameter:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.route('/cities/{city}')
def index(city):
return {'city': city}
This indicates that the value of ``{city}`` is variable, and whatever
value is provided in the URL is passed to the ``index`` view function.
For example::
GET /cities/seattle --> index('seattle')
GET /cities/portland --> index('portland')
If you want to access any other metadata of the incoming HTTP request,
you can use the ``app.current_request`` property, which is an instance of
the the :class:`Request` class.
View Function Return Values
---------------------------
The response returned back to the client depends on the behavior
of the view function. There are several options available:
* Returning an instance of :class:`Response`. This gives you
complete control over what gets returned back to the customer.
* A ``bytes`` type response body must have a ``Content-Type`` header value
that is present in the ``app.api.binary_types`` list in order to be handled
properly.
* Any other return value will be serialized as JSON and sent back
as the response body with content type ``application/json``.
* Any subclass of ``ChaliceViewError`` will result in an HTTP
response being returned with the status code associated with that
response, and a JSON response body containing a ``Code`` and a ``Message``.
This is discussed in more detail below.
* Any other exception raised will result in a 500 HTTP response.
The body of that response depends on whether debug mode is enabled.
.. _view-error-handling:
Error Handling
--------------
Chalice provides a built in set of exception classes that map to common
HTTP errors including:
* ``BadRequestError``- returns a status code of 400
* ``UnauthorizedError``- returns a status code of 401
* ``ForbiddenError``- returns a status code of 403
* ``NotFoundError``- returns a status code of 404
* ``ConflictError``- returns a status code of 409
* ``TooManyRequestsError``- returns a status code of 429
* ``ChaliceViewError``- returns a status code of 500
You can raise these anywhere in your view functions and chalice will convert
these to the appropriate HTTP response. The default chalice error responses
will send the error back as ``application/json`` with the response body
containing a ``Code`` corresponding to the exception class name and a
``Message`` key corresponding to the string provided when the exception
was instantiated. For example:
.. code-block:: python
from chalice import Chalice
from chalice import BadRequestError
app = Chalice(app_name="badrequest")
@app.route('/badrequest')
def badrequest():
raise BadRequestError("This is a bad request")
This view function will generate the following HTTP response::
$ http https://endpoint/api/badrequest
HTTP/1.1 400 Bad Request
{
"Code": "BadRequestError",
"Message": "This is a bad request"
}
In addition to the built in chalice exceptions, you can use the
:class:`Response` class to customize the HTTP errors if you prefer to
either not have JSON error responses or customize the JSON response body
for errors. For example:
.. code-block:: python
from chalice import Chalice, Response
app = Chalice(app_name="badrequest")
@app.route('/badrequest')
def badrequest():
return Response(body='Plain text error message',
headers={'Content-Type': 'text/plain'},
status_code=400)
Specifying HTTP Methods
-----------------------
So far, our examples have only allowed GET requests. It's actually possible
to support additional HTTP methods. Here's an example of a view function that
supports PUT:
.. code-block:: python
@app.route('/resource/{value}', methods=['PUT'])
def put_test(value):
return {"value": value}
We can test this method using the ``http`` command::
$ http PUT https://endpoint/api/resource/foo
HTTP/1.1 200 OK
{
"value": "foo"
}
Note that the ``methods`` kwarg accepts a list of methods. Your view function
will be called when any of the HTTP methods you specify are used for the
specified resource. For example:
.. code-block:: python
@app.route('/myview', methods=['POST', 'PUT'])
def myview():
pass
The above view function will be called when either an HTTP POST or
PUT is sent to ``/myview`` as shown below::
POST /myview --> myview()
PUT /myview --> myview()
Alternatively if you do not want to share the same view function across
multiple HTTP methods for the same route url, you may define separate view
functions to the same route url but have the view functions differ by
HTTP method. For example:
.. code-block:: python
@app.route('/myview', methods=['POST'])
def myview_post():
pass
@app.route('/myview', methods=['PUT'])
def myview_put():
pass
This setup will route all HTTP POST's to ``/myview`` to the ``myview_post()``
view function and route all HTTP PUT's to ``/myview`` to the ``myview_put()``
view function as shown below::
POST /myview --> myview_post()
PUT /myview --> myview_put()
If you do chose to use separate view functions for the same route path, it is
important to know:
* View functions that share the same route cannot have the same names.
For example, two view functions that both share the same route path cannot
both be named ``view()``.
* View functions that share the same route cannot overlap in supported HTTP
methods. For example if two view functions both share the same route path,
they both cannot contain ``'PUT'`` in their route ``methods`` list.
* View functions that share the same route path and have CORS configured cannot
have differing CORS configuration. For example, if two view functions that
both share the same route path, the route configuration for one of the
view functions cannot set ``cors=True`` while having the route
configuration of the other view function be set to
``cors=app.CORSConfig(allow_origin='https://foo.example.com')``.
Binary Content
--------------
Chalice supports binary payloads through its ``app.api.binary_types`` list. Any
type in this list is considered a binary ``Content-Type``. Whenever a request
with a ``Content-Type`` header is encountered that matches an entry in the
``binary_types`` list, its body will be available as a ``bytes`` type on the
property ``app.current_request.raw_body``. Similarly, in order to send binary
data back in a response, simply set your ``Content-Type`` header to something
present in the ``binary_types`` list. Note that you can override the default
types by modifying the ``app.api.binary_types`` list at the module level.
Here is an example app which simply echoes back binary content:
.. code-block:: python
from chalice import Chalice, Response
app = Chalice(app_name="binary-response")
@app.route('/bin-echo', methods=['POST'],
content_types=['application/octet-stream'])
def bin_echo():
raw_request_body = app.current_request.raw_body
return Response(body=raw_request_body,
status_code=200,
headers={'Content-Type': 'application/octet-stream'})
You can see this app echo back binary data sent to it::
$ echo -n -e "\xFE\xED" | http POST $(chalice url)bin-echo \
Accept:application/octet-stream Content-Type:application/octet-stream | xxd
0000000: feed ..
Note that both the ``Accept`` and ``Content-Type`` headers are required. If
you fail to set the ``Content-Type`` header on the request will result in a
``415 UnsupportedMediaType`` error. Care must be taken when configuring what
``content_types`` a route accepts, they must all be valid binary types, or they
must all be non-binary types. The ``Accept`` header must also be set if the
data returned is to be the raw binary, if is omitted the call return a ``400``
Bad Request response.
For example, here is the same call as above without the ``Accept`` header::
$ echo -n -e "\xFE\xED" | http POST $(chalice url)bin-echo \
Content-Type:application/octet-stream
HTTP/1.1 400 Bad Request
Connection: keep-alive
Content-Length: 270
Content-Type: application/json
Date: Sat, 27 May 2017 07:09:51 GMT
{
"Code": "BadRequest",
"Message": "Request did not specify an Accept header with
application/octet-stream, The response has a Content-Type of
application/octet-stream. If a response has a binary Content-Type then
the request must specify an Accept header that matches."
}
Usage Recommendations
---------------------
If you want to return a JSON response body, just return the corresponding
python types directly. You don't need to use the :class:`Response` class.
Chalice will automatically convert this to a JSON HTTP response as a
convenience for you.
Use the :class:`Response` class when you want to return non-JSON content, or
when you want to inject custom HTTP headers to your response.
For errors, raise the built in ``ChaliceViewError`` subclasses (e.g
``BadRequestError``, ``NotFoundError``, ``ConflictError`` etc) when you
want to return a HTTP error response with a preconfigured JSON body containing
a ``Code`` and ``Message``.
Use the :class:`Response` class when you want to customize the error responses
to either return a different JSON error response body, or to return an HTTP
response that's not ``application/json``.
---
### Source Topics Websockets (`docs/source/topics/websockets.rst`)
Websockets
==========
.. warning::
Websockets are considered an experimental API. You'll need to opt-in
to this feature using the ``WEBSOCKETS`` feature flag:
.. code-block:: python
app = Chalice('myapp')
app.experimental_feature_flags.update([
'WEBSOCKETS'
])
See :doc:`experimental` for more information.
Chalice supports websockets through integration with an API Gateway Websocket
API. If any of the decorators are present in a Chalice app, then an API
Gateway Websocket API will be deployed and wired to Lambda Functions.
Responding to websocket events
------------------------------
In a Chalice app the websocket API is accessed through the three decorators
``on_ws_connect``, ``on_ws_message``, ``on_ws_disconnect``. These handle a new
websocket connection, an incoming message on an existing connection, and a
connection being cleaned up respectively.
A decorated websocket handler function takes one argument ``event`` with the
type :ref:`WebsocketEvent <websocket-api>`. This class allows easy access to
information about the API Gateway Websocket API, and information about the
particular socket the handler is being invoked to serve.
Below is a simple working example application that prints to CloudWatch Logs
for each of the events.
.. code-block:: python
from boto3.session import Session
from chalice import Chalice
app = Chalice(app_name='test-websockets')
app.experimental_feature_flags.update([
'WEBSOCKETS',
])
app.websocket_api.session = Session()
@app.on_ws_connect()
def connect(event):
print('New connection: %s' % event.connection_id)
@app.on_ws_message()
def message(event):
print('%s: %s' % (event.connection_id, event.body))
@app.on_ws_disconnect()
def disconnect(event):
print('%s disconnected' % event.connection_id)
Setting the websocket protocol on new connections
-------------------------------------------------
You can return a dictionary or an instance of :class:`Response` in the
``on_ws_connect`` handler, similar to what you'd do in a Rest API. Note that
API Gateway does not forward arbitrary headers or a response body back to the
client, so this is primarily used to set a ``Sec-WebSocket-Protocol`` header
value.
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='test-websockets')
app.experimental_feature_flags.update([
'WEBSOCKETS',
])
@app.on_ws_connect()
def connect(event):
print('New connection: %s' % event.connection_id)
# We don't need to explicitly set a statusCode.
return {
'headers': {'Sec-WebSocket-Protocol': 'My-Protocol'},
}
You don't need to explicitly set a ``statusCode`` if you return a
dictionary from the ``on_ws_connect`` header, but if want to return one you
should **not** set the status code to ``101``. API Gateway will automatically
do this for you. For successful connection handling you should return a
``200`` status code if you want to explicitly set a ``statusCode``.
Sending a message over a websocket
----------------------------------
To send a message to a websocket client Chalice, use the
:ref:`app.websocket_api.send() <websocket-send>` method. This method will work in any
of the decorated functions outlined in the above section.
Two pieces of information are needed to send a message. The identifier of the
websocket, and the contents for the message. Below is a simple example that
when it receives a message, it sends back the message ``"I got your message!"``
over the same socket.
.. code-block:: python
from boto3.session import Session
from chalice import Chalice
app = Chalice(app_name='test-websockets')
app.experimental_feature_flags.update([
'WEBSOCKETS',
])
app.websocket_api.session = Session()
@app.on_ws_message()
def message(event):
app.websocket_api.send(event.connection_id, 'I got your message!')
See :ref:`websocket-tutorial` for completely worked example applications.
---
### Source Tutorials Basicrestapi (`docs/source/tutorials/basicrestapi.rst`)
REST API Tutorial
=================
In this tutorial, we create a REST API and explore
Chalice features that help us write REST APIs.
Installation and Configuration
------------------------------
If you haven't already setup and configured Chalice, see the
:doc:`../quickstart` for a step by step guide. In a nutshell, you can get a
basic Chalice app created with::
$ python3 --version
Python 3.10.20
$ python3 -m venv .venv
$ . .venv/bin/activate
$ python3 -m pip install chalice
$ chalice new-project helloworld
$ cd helloworld
URL Parameters
--------------
The default template when you run the ``new-project`` generates a sample
REST API for you:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
@app.route('/')
def index():
return {'hello': 'world'}
We're going to make a few changes to our ``app.py`` file that
demonstrate the capabilities provided by Chalice.
Our application so far has a single view that allows you to make
an HTTP GET request to ``/``. Now let's suppose we want to capture
parts of the URI:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
CITIES_TO_STATE = {
'seattle': 'WA',
'portland': 'OR',
}
@app.route('/')
def index():
return {'hello': 'world'}
@app.route('/cities/{city}')
def state_of_city(city):
return {'state': CITIES_TO_STATE[city]}
In the example above, we've now added a ``state_of_city`` view that allows
a user to specify a city name. The view function takes the city
name and returns the name of the state the city is in. Notice that the
``@app.route`` decorator has a URL pattern of ``/cities/{city}``. This
means that the value of ``{city}`` is captured and passed to the view
function. You can also see that the ``state_of_city`` takes a single
argument. This argument is the name of the city provided by the user.
For example::
GET /cities/seattle --> state_of_city('seattle')
GET /cities/portland --> state_of_city('portland')
Now that we've updated our ``app.py`` file with this new view function,
let's redeploy our application. You can run ``chalice deploy`` from
the ``helloworld`` directory and it will deploy your application::
$ chalice deploy
Let's try it out. Note the examples below use the ``http`` command from the
``httpie`` package. You can install this using ``pip install httpie``::
$ http https://endpoint/api/cities/seattle
HTTP/1.1 200 OK
{
"state": "WA"
}
$ http https://endpoint/api/cities/portland
HTTP/1.1 200 OK
{
"state": "OR"
}
Notice what happens if we try to request a city that's not in our
``CITIES_TO_STATE`` map::
$ http https://endpoint/api/cities/vancouver
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
X-Cache: Error from cloudfront
{
"Code": "ChaliceViewError",
"Message": "An internal server error occurred."
}
In the next section, we'll see how to fix this and provide better
error messages.
Error Messages
--------------
In the example above, you'll notice that when our app raised
an uncaught exception, a 500 internal server error was returned.
In this section, we're going to show how you can debug and improve
these error messages.
The first thing we're going to look at is how we can debug this
issue. By default, debugging is turned off, but you can
enable debugging to get more information:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='helloworld')
app.debug = True
The ``app.debug = True`` enables debugging for your app.
Save this file and redeploy your changes::
$ chalice deploy
...
https://endpoint/api/
Now, when you request the same URL that returned an internal
server error, you'll get back the original stack trace::
$ http https://endpoint/api/cities/vancouver
Traceback (most recent call last):
File "/var/task/chalice/app.py", line 304, in _get_view_function_response
response = view_function(*function_args)
File "/var/task/app.py", line 18, in state_of_city
return {'state': CITIES_TO_STATE[city]}
KeyError: u'vancouver'
We can see that the error is caused from an uncaught ``KeyError`` resulting
from trying to access the ``vancouver`` key.
Now that we know the error, we can fix our code. What we'd like to do is
catch this exception and instead return a more helpful error message
to the user. Here's the updated code:
.. code-block:: python
from chalice import BadRequestError
@app.route('/cities/{city}')
def state_of_city(city):
try:
return {'state': CITIES_TO_STATE[city]}
except KeyError:
raise BadRequestError("Unknown city '%s', valid choices are: %s" % (
city, ', '.join(CITIES_TO_STATE.keys())))
Save and deploy these changes::
$ chalice deploy
$ http https://endpoint/api/cities/vancouver
HTTP/1.1 400 Bad Request
{
"Code": "BadRequestError",
"Message": "Unknown city 'vancouver', valid choices are: portland, seattle"
}
We can see now that we have received a ``Code`` and ``Message`` key, with the
message being the value we passed to ``BadRequestError``. Whenever you raise a
``BadRequestError`` from your view function, the framework will return an HTTP
status code of 400 along with a JSON body with a ``Code`` and ``Message``.
There are a few additional exceptions you can raise from your python code::
* BadRequestError - return a status code of 400
* UnauthorizedError - return a status code of 401
* ForbiddenError - return a status code of 403
* NotFoundError - return a status code of 404
* ConflictError - return a status code of 409
* UnprocessableEntityError - return a status code of 422
* TooManyRequestsError - return a status code of 429
* ChaliceViewError - return a status code of 500
You can import these directly from the ``chalice`` package:
.. code-block:: python
from chalice import UnauthorizedError
Additional Routing
------------------
So far, our examples have only allowed GET requests.
It's actually possible to support additional HTTP methods.
Here's an example of a view function that supports PUT:
.. code-block:: python
@app.route('/resource/{value}', methods=['PUT'])
def put_test(value):
return {"value": value}
We can test this method using the ``http`` command::
$ http PUT https://endpoint/api/resource/foo
HTTP/1.1 200 OK
{
"value": "foo"
}
Note that the ``methods`` kwarg accepts a list of methods. Your view function
will be called when any of the HTTP methods you specify are used for the
specified resource. For example:
.. code-block:: python
@app.route('/myview', methods=['POST', 'PUT'])
def myview():
pass
The above view function will be called when either an HTTP POST or
PUT is sent to ``/myview``.
Alternatively if you do not want to share the same view function across
multiple HTTP methods for the same route url, you may define separate view
functions to the same route url but have the view functions differ by
HTTP method. For example:
.. code-block:: python
@app.route('/myview', methods=['POST'])
def myview_post():
pass
@app.route('/myview', methods=['PUT'])
def myview_put():
pass
This setup routes HTTP POST requests to ``/myview`` to the ``myview_post()``
view function and routes HTTP PUT requests to ``/myview`` to the ``myview_put()``
view function. It is also important to note that the view functions
**must** have unique names. For example, both view functions cannot be
named ``myview()``.
In the next section we'll go over how you can introspect the given request
in order to differentiate between various HTTP methods.
Request Metadata
----------------
In the examples above, you saw how to create a view function that supports
an HTTP PUT request as well as a view function that supports both POST and
PUT via the same view function. However, there's more information we
might need about a given request:
* In a PUT/POST, you frequently send a request body. We need some
way of accessing the contents of the request body.
* For view functions that support multiple HTTP methods, we'd like
to detect which HTTP method was used so we can have different
code paths for PUTs vs. POSTs.
All of this and more is handled by the current request object that the
``chalice`` library makes available to each view function when it's called.
Let's see an example of this. Suppose we want to create a view function
that allowed you to PUT data to an object and retrieve that data
via a corresponding GET. We could accomplish that with the
following view function:
.. code-block:: python
from chalice import NotFoundError
OBJECTS = {
}
@app.route('/objects/{key}', methods=['GET', 'PUT'])
def myobject(key):
request = app.current_request
if request.method == 'PUT':
OBJECTS[key] = request.json_body
elif request.method == 'GET':
try:
return {key: OBJECTS[key]}
except KeyError:
raise NotFoundError(key)
Save this in your ``app.py`` file and rerun ``chalice deploy``.
Now, you can make a PUT request to ``/objects/your-key`` with a request
body, and retrieve the value of that body by making a subsequent
``GET`` request to the same resource. Here's an example of its usage::
# First, trying to retrieve the key will return a 404.
$ http GET https://endpoint/api/objects/mykey
HTTP/1.1 404 Not Found
{
"Code": "NotFoundError",
"Message": "mykey"
}
# Next, we'll create that key by sending a PUT request.
$ echo '{"foo": "bar"}' | http PUT https://endpoint/api/objects/mykey
HTTP/1.1 200 OK
null
# And now we no longer get a 404, we instead get the value we previously
# put.
$ http GET https://endpoint/api/objects/mykey
HTTP/1.1 200 OK
{
"mykey": {
"foo": "bar"
}
}
You might see a problem with storing the objects in a module level
``OBJECTS`` variable. We address this in the next section.
The ``app.current_request`` object is an instance of the :class:`Request`
class, which also has the following properties.
* ``current_request.query_params`` - A dict of the query params.
* ``current_request.headers`` - A dict of the request headers.
* ``current_request.uri_params`` - A dict of the captured URI params.
* ``current_request.method`` - The HTTP method (as a string).
* ``current_request.json_body`` - The parsed JSON body.
* ``current_request.raw_body`` - The raw HTTP body as bytes.
* ``current_request.context`` - A dict of additional context information
* ``current_request.stage_vars`` - Configuration for the API Gateway stage
The ``current_request`` object also has a ``to_dict`` method, which returns all
the information about the current request as a dictionary. Let's use this
method to write a view function that returns everything it knows about the
request:
.. code-block:: python
@app.route('/introspect')
def introspect():
return app.current_request.to_dict()
Save this to your ``app.py`` file and redeploy with ``chalice deploy``.
Here's an example of hitting the ``/introspect`` URL. Note how we're
sending a query string as well as a custom ``X-TestHeader`` header::
$ http 'https://endpoint/api/introspect?query1=value1&query2=value2' 'X-TestHeader: Foo'
HTTP/1.1 200 OK
{
"context": {
"apiId": "apiId",
"httpMethod": "GET",
"identity": {
"accessKey": null,
"accountId": null,
"apiKey": null,
"caller": null,
"cognitoAuthenticationProvider": null,
"cognitoAuthenticationType": null,
"cognitoIdentityId": null,
"cognitoIdentityPoolId": null,
"sourceIp": "1.1.1.1",
"userAgent": "HTTPie/0.9.3",
"userArn": null
},
"requestId": "request-id",
"resourceId": "resourceId",
"resourcePath": "/introspect",
"stage": "dev"
},
"headers": {
"accept": "*/*",
...
"x-testheader": "Foo"
},
"method": "GET",
"query_params": {
"query1": "value1",
"query2": "value2"
},
"raw_body": null,
"stage_vars": null,
"uri_params": null
}
Request Content Types
---------------------
The default behavior of a view function supports
a request body of ``application/json``. When a request is
made with a ``Content-Type`` of ``application/json``, the
``app.current_request.json_body`` attribute is automatically
set for you. This value is the parsed JSON body.
You can also configure a view function to support other
content types. You can do this by specifying the
``content_types`` parameter value to your ``app.route``
function. This parameter is a list of acceptable content
types. Here's an example of this feature:
.. code-block:: python
import sys
from chalice import Chalice
from urllib.parse import urlparse, parse_qs
app = Chalice(app_name='helloworld')
@app.route('/', methods=['POST'],
content_types=['application/x-www-form-urlencoded'])
def index():
parsed = parse_qs(app.current_request.raw_body.decode())
return {
'states': parsed.get('states', [])
}
There's a few things worth noting in this view function.
First, we've specified that we only accept the
``application/x-www-form-urlencoded`` content type. If we
try to send a request with ``application/json``, we'll now
get a ``415 Unsupported Media Type`` response::
$ http POST https://endpoint/api/ states=WA states=CA --debug
...
>>> requests.request(**{'allow_redirects': False,
'headers': {'Accept': 'application/json',
'Content-Type': 'application/json',
...
HTTP/1.1 415 Unsupported Media Type
{
"message": "Unsupported Media Type"
}
If we use the ``--form`` argument, we can see the
expected behavior of this view function because ``httpie`` sets the
``Content-Type`` header to ``application/x-www-form-urlencoded``::
$ http --form POST https://endpoint/api/formtest states=WA states=CA --debug
...
>>> requests.request(**{'allow_redirects': False,
'headers': {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
...
HTTP/1.1 200 OK
{
"states": [
"WA",
"CA"
]
}
The second thing worth noting is that ``app.current_request.json_body``
**is only available for the application/json content type.**
In our example above, we used ``app.current_request.raw_body`` to access
the raw body bytes:
.. code-block:: python
parsed = parse_qs(app.current_request.raw_body)
``app.current_request.json_body`` is set to ``None`` whenever the
``Content-Type`` is not ``application/json``. This means that
you will need to use ``app.current_request.raw_body`` and parse
the request body as needed.
Customizing the HTTP Response
-----------------------------
The return value from a chalice view function is serialized as JSON as the
response body returned back to the caller. This makes it easy to create
rest APIs that return JSON response bodies.
Chalice allows you to control this behavior by returning an instance of
a chalice specific ``Response`` class. This behavior allows you to:
* Specify the status code to return
* Specify custom headers to add to the response
* Specify response bodies that are not ``application/json``
Here's an example of this:
.. code-block:: python
from chalice import Chalice, Response
app = Chalice(app_name='custom-response')
@app.route('/')
def index():
return Response(body='hello world!',
status_code=200,
headers={'Content-Type': 'text/plain'})
This will result in a plain text response body::
$ http https://endpoint/api/
HTTP/1.1 200 OK
Content-Length: 12
Content-Type: text/plain
hello world!
GZIP compression for JSON
-------------------------
The return value from a chalice view function is serialized as JSON as the
response body returned back to the caller. This makes it easy to create
rest APIs that return JSON response bodies.
Chalice allows you to control this behavior by returning an instance of
a chalice specific ``Response`` class. This behavior allows you to:
* Add ``application/json`` to binary_types
* Specify the status code to return
* Specify custom header ``Content-Type: application/json``
* Specify custom header ``Content-Encoding: gzip``
Here's an example of this:
.. code-block:: python
import json
import gzip
from chalice import Chalice, Response
app = Chalice(app_name='compress-response')
app.api.binary_types.append('application/json')
@app.route('/')
def index():
blob = json.dumps({'hello': 'world'}).encode('utf-8')
payload = gzip.compress(blob)
custom_headers = {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip'
}
return Response(body=payload,
status_code=200,
headers=custom_headers)
CORS Support
------------
You can specify whether a view supports CORS by adding the
``cors=True`` parameter to your ``@app.route()`` call. By
default this value is ``False``. Global CORS can be set by
setting ``app.api.cors = True``.
.. code-block:: python
@app.route('/supports-cors', methods=['PUT'], cors=True)
def supports_cors():
return {}
Setting ``cors=True`` has similar behavior to enabling CORS
using the AWS Console. This includes:
* Injecting the ``Access-Control-Allow-Origin: *`` header to your
responses, including all error responses you can return.
* Automatically adding an ``OPTIONS`` method to support preflighting
requests.
The preflight request will return a response that includes:
* ``Access-Control-Allow-Origin: *``
* The ``Access-Control-Allow-Methods`` header will return a list of all HTTP
methods you've called out in your view function. In the example above,
this will be ``PUT,OPTIONS``.
* ``Access-Control-Allow-Headers: Content-Type,X-Amz-Date,Authorization,
X-Api-Key,X-Amz-Security-Token``.
If more fine grained control of the CORS headers is desired, set the ``cors``
parameter to an instance of ``CORSConfig`` instead of ``True``. The
``CORSConfig`` object can be imported from from the ``chalice`` package it's
constructor takes the following keyword arguments that map to CORS headers:
================= ==== ================================
Argument Type Header
================= ==== ================================
allow_origin str Access-Control-Allow-Origin
allow_headers list Access-Control-Allow-Headers
expose_headers list Access-Control-Expose-Headers
max_age int Access-Control-Max-Age
allow_credentials bool Access-Control-Allow-Credentials
================= ==== ================================
Code sample defining more CORS headers:
.. code-block:: python
from chalice import CORSConfig
cors_config = CORSConfig(
allow_origin='https://foo.example.com',
allow_headers=['X-Special-Header'],
max_age=600,
expose_headers=['X-Special-Header'],
allow_credentials=True
)
@app.route('/custom-cors', methods=['GET'], cors=cors_config)
def supports_custom_cors():
return {'cors': True}
There's a couple of things to keep in mind when enabling cors for a view:
* An ``OPTIONS`` method for preflighting is always injected. Ensure that
you don't have ``OPTIONS`` in the ``methods=[...]`` list of your
view function.
* Even though the ``Access-Control-Allow-Origin`` header can be set to a
string that is a space separated list of origins, this behavior does not
work on all clients that implement CORS. You should only supply a single
origin to the ``CORSConfig`` object. If you need to supply multiple origins
you will need to define a custom handler for it that accepts ``OPTIONS``
requests and matches the ``Origin`` header against a whitelist of origins.
If the match is successful then return just their ``Origin`` back to them
in the ``Access-Control-Allow-Origin`` header.
Example:
.. code-block:: python
from chalice import Chalice, Response
app = Chalice(app_name='multipleorigincors')
_ALLOWED_ORIGINS = set([
'http://allowed1.example.com',
'http://allowed2.example.com',
])
@app.route('/cors_multiple_origins', methods=['GET', 'OPTIONS'])
def supports_cors_multiple_origins():
method = app.current_request.method
if method == 'OPTIONS':
headers = {
'Access-Control-Allow-Method': 'GET,OPTIONS',
'Access-Control-Allow-Origin': ','.join(_ALLOWED_ORIGINS),
'Access-Control-Allow-Headers': 'X-Some-Header',
}
origin = app.current_request.headers.get('origin', '')
if origin in _ALLOWED_ORIGINS:
headers.update({'Access-Control-Allow-Origin': origin})
return Response(
body=None,
headers=headers,
)
elif method == 'GET':
return 'Foo'
---
### Source Tutorials Cdk (`docs/source/tutorials/cdk.rst`)
Deploying with the AWS CDK
==========================
In this tutorial, we're going to create a REST API with an Amazon DynamoDB
table as our data store. We'll be using the `AWS Cloud Development Kit (CDK)
<https://aws.amazon.com/cdk/>`__
to deploy our application, and we'll show how to use the integration between
Chalice and the CDK in order to build and deploy our application.
By combining Chalice and the CDK together, you can use Chalice to
write your application code using its familiar, decorator-based APIs, and
use the CDK and the full breadth of its construct libraries to create the
service infrastructure and resources needed for your application.
We'll also see how we can use the Chalice construct to manipulate our
Chalice application using the CDK APIs as well as take resources from
CDK constructs and map them into our Chalice application.
Installation and Configuration
------------------------------
This tutorial requires that both Chalice and the AWS CDK is installed.
The CDK is written in Typescript and requires node and npm to be installed.
See the `Getting started with the AWS CDK <https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html#getting_started_prerequisites>`__
for more details on install the CDK.
First, we'll install the CDK.
::
$ npm install aws-cdk
You should now have a ``cdk`` executable you can run.
::
$ cdk --version
1.83.0 (build 827c5f4)
Next we'll create a Python virtual environment and install Chalice. Be sure
to use Python 3.6 or greater.
::
$ python3 -m venv demo
$ . demo/bin/activate
$ python3 -m pip install chalice
$ chalice --version
chalice 1.22.0, python 3.7.8, darwin 19.6.0
CDK integration with Chalice is available as an optional package installation.
To install the necessary dependencies run the following command:
::
$ python3 -m pip install "chalice[cdkv2]"
**Note:** Please use CDK version 2, support for CDK version 1 ended
on June 1, 2023. See
`Working with the AWS CDK in Python Doc <https://docs.aws.amazon.com/cdk/v2/guide/work-with-cdk-python.html>`__
for more information.
You're now ready to create your first Chalice and CDK application.
Project Creation
----------------
To create a new project we'll use the ``chalice new-project`` command with no
arguments. Enter a name for your project and select
``[CDK] REST API with DynamoDB backend`` for the project type.
::
$ chalice new-project
___ _ _ _ _ ___ ___ ___
/ __|| || | /_\ | | |_ _|/ __|| __|
| (__ | __ | / _ \ | |__ | || (__ | _|
\___||_||_|/_/ \_\|____||___|\___||___|
The python serverless microframework for AWS allows
you to quickly create and deploy applications using
Amazon API Gateway and AWS Lambda.
Please enter the project name
[?] Enter the project name: cdkdemo
[?] Select your project type: [CDK] REST API with DynamoDB backend
REST API
S3 Event Handler
Lambda Functions only
Legacy REST API Template
[CDK] REST API with DynamoDB backend
Your project has been generated in ./cdkdemo
Next, we'll ``cd`` into the ``cdkdemo`` directory and see what Chalice has
generated.
::
$ cd cdkdemo
$ tree
.
โโโ README.rst
โโโ infrastructure # CDK Application
โย ย โโโ app.py
โย ย โโโ cdk.json
โย ย โโโ requirements.txt
โย ย โโโ stacks
โย ย โโโ __init__.py
โย ย โโโ chaliceapp.py
โโโ requirements.txt
โโโ runtime # Chalice Application
โโโ app.py
โโโ requirements.txt
There's two top level directories, ``infrastructure`` and ``runtime``, which
correspond to the CDK application and the Chalice application. The
``infrastructure`` directory is where we can add additional AWS resources
needed by our application, and the ``runtime`` directory is where we write
our application code for our Lambda functions. We'll look at these in more
detail, but first we'll deploy our application.
In order to build and deploy our application, we need to install the
dependencies used by our application. We can do this by installing the
requirements file in the top level directory of our project.
::
$ python3 -m pip install -r requirements.txt
If this is your first time using the CDK, you'll need to bootstrap your
account, which will deploy an AWS CloudFormation stack that contains
resources needed to store our application. You can do this by running the
``cdk bootstrap`` command from the ``infrastructure`` directory.
::
$ cd infrastructure
$ cdk bootstrap
Packaging Chalice app for cdkdemo
Creating deployment package.
The stack cdkdemo already includes a CDKMetadata resource
โณ Bootstrapping environment aws://12345/us-west-2...
CDKToolkit: creating CloudFormation changeset...
[โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ] (3/3)
โ
Environment aws://12345/us-west-2 bootstrapped.
We can now deploy our application using the ``cdk deploy`` command. Make sure
you're still in the ``infrastructure`` directory.
::
$ cdk deploy
Packaging Chalice app for cdkdemo
Creating deployment package.
Reusing existing deployment package.
The stack cdkdemo already includes a CDKMetadata resource
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:
...
Do you wish to deploy these changes (y/n)? y
cdkdemo: deploying...
[0%] start: Publishing abcd:current
[100%] success: Published abcd:current
cdkdemo: creating CloudFormation changeset...
[โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ] (10/10)
โ
cdkdemo
Outputs:
cdkdemo.APIHandlerArn = arn:aws:lambda:us-west-2:12345:function:cdkdemo-APIHandler-C8OLGQT9YIDO
cdkdemo.APIHandlerName = cdkdemo-APIHandler-C8OLGQT9YIDO
cdkdemo.AppTableName = cdkdemo-AppTable815C50BC-1OPGOPFYODZOJ
cdkdemo.EndpointURL = https://abcd.execute-api.us-west-2.amazonaws.com/api/
cdkdemo.RestAPIId = abcd
Stack ARN:
arn:aws:cloudformation:us-west-2:12345:stack/cdkdemo/574c4850-1d23-11eb-8cae-0aea264da24f
We've now deployed a Chalice application powered by the CDK. We can now test
our REST API.
.. note::
If you've Chalice before, you may be familiar with the ``chalice deploy``
command. When we use the AWS CDK to deploy our application we no longer
use ``chalice deploy`` and instead we run ``cdk deploy`` from the
``infrastructure/`` directory. You should not use ``chalice deploy``
to deploy your application when using Chalice's CDK integration.
Testing
-------
To test our application, we make HTTP requests to our ``EndpointUrl``, which is
shown as the value for ``cdkdemo.EndpointUrl`` in the output section above.
We're using `httpie <https://httpie.io/>`__ to make our HTTP requests from the
command line.
::
$ python3 -m pip install httpie
$ http POST https://abcd.execute-api.us-west-2.amazonaws.com/api/users/ username=jamesls name=James
HTTP/1.1 200 OK
...
{}
$ http https://abcd.execute-api.us-west-2.amazonaws.com/api/users/jamesls
HTTP/1.1 200 OK
Content-Type: application/json
...
{
"name": "James",
"username": "jamesls"
}
Now that we have our sample application up and running, let's walk through the
project code so we can better understand what's happening.
Code Walkthrough
----------------
The ``runtime/`` directory contains code where you define your Lambda event
handlers (e.g. ``@app.route()``, ``@app.on_s3_event()``, etc.). When you
create a Chalice application without the CDK, this is normally the root
directory for your application. You should also see your Chalice config file
in ``.chalice/config.json``. The ``infrastructure/`` directory contains the
definitions for the AWS resources used by your application. This is the
directory structure that would be generated if you were only using the
CDK and not Chalice. This is why the combined Chalice/CDK application template
has a new top level directory with separate sub directories for the CDK app
and the Chalice app.
To better understand how the two applications communicate with each other,
we'll examine how the DynamoDB table was added to the application.
First, letโs look at the code for our REST API in ``runtime/app.py``.
.. code-block:: python
import os
import boto3
from chalice import Chalice
app = Chalice(app_name='cdkdemo')
dynamodb = boto3.resource('dynamodb')
dynamodb_table = dynamodb.Table(os.environ.get('APP_TABLE_NAME', ''))
@app.route('/users', methods=['POST'])
def create_user():
...
@app.route('/users/{username}', methods=['GET'])
def get_user(username):
...
The name of the DynamoDB table is passed through an environment variable,
``APP_TABLE_NAME``. We then create a ``dynamodb.Table`` resource given this
name. This environment variable is generated and mapped in the CDK stack that
Chalice generated for us. This is located in
``../infrastructure/stacks/chaliceapp.py``.
Let's look at the contents of the ``../infrastructure/stacks/chaliceapp.py``
file now.
.. code-block:: python
import os
from aws_cdk import (
aws_dynamodb as dynamodb,
core as cdk
)
from chalice.cdk import Chalice
RUNTIME_SOURCE_DIR = os.path.join(
os.path.dirname(os.path.dirname(__file__)), os.pardir, 'runtime')
class ChaliceApp(cdk.Stack):
def __init__(self, scope: cdk.Construct, id: str, **kwargs) -> None:
super().__init__(scope, id, **kwargs)
self.dynamodb_table = self._create_ddb_table()
self.chalice = Chalice(
self, 'ChaliceApp', source_dir=RUNTIME_SOURCE_DIR,
stage_config={
'environment_variables': {
'APP_TABLE_NAME': self.dynamodb_table.table_name
}
}
)
self.dynamodb_table.grant_read_write_data(
self.chalice.get_role('DefaultRole')
)
def _create_ddb_table(self):
dynamodb_table = dynamodb.Table(
self, 'AppTable',
partition_key=dynamodb.Attribute(
name='PK', type=dynamodb.AttributeType.STRING),
sort_key=dynamodb.Attribute(
name='SK', type=dynamodb.AttributeType.STRING
),
removal_policy=cdk.RemovalPolicy.DESTROY)
cdk.CfnOutput(self, 'AppTableName',
value=dynamodb_table.table_name)
return dynamodb_table
Our CDK stack is using the Chalice construct from the ``chalice.cdk``
package. This provides us two benefits. First, we can generate CDK resources
and pass them into our Chalice application by mapping environment variables.
Second, we can take resources generated in our Chalice application and
reference them with the CDK API. For example, weโre generating a DynamoDB
table in the ``self._create_ddb_table()`` method, and then mapping it into our
Chalice application by providing a ``stage_config`` override. This dictionary
is merged with the existing Chalice configuration located in
./runtime/.chalice/config.json. If we want to pass additional values into our
Chalice application we can update the environment_variables dictionary in our
stage_config.
Weโre also able to retrieve references to our resources in our Chalice
application and reference them in our CDK stack. For example, once weโve
created our DynamoDB table we also need to grant the IAM role associated with
your Lambda function access to this table. We do this by using the
``grant_read_write_data`` method on our table resource, and we provide a
reference to the default role that Chalice creates for us by using the
``self.chalice.get_role()`` method.
Next Steps
----------
Feel free to experiment with this sample app. Add new resources to your
application by updating the ``infrastructure/stacks/chaliceapp.py`` file, map
CDK resources into your Chalice app through environment variables, and
redeploy your application by running ``cdk deploy`` from the
``infrastructure/`` directory.
---
### Source Tutorials Customdomain (`docs/source/tutorials/customdomain.rst`)
Custom Domain Names
===================
In this tutorial, we're going to create a REST API and associate our own custom
domain with this REST API. This allows us to not use the auto-generated domain
name that API Gateway automatically creates when we deploy our REST API.
Installation and Configuration
------------------------------
If you haven't already setup and configured Chalice, see the
:doc:`../quickstart` for a step by step guide. You can run these commands
to create a basic Chalice app::
$ python3 --version
Python 3.10.20
$ python3 -m venv .venv
$ . .venv/bin/activate
$ python3 -m pip install chalice
$ chalice new-project customdomain
$ cd customdomain
Configure and Deploy a REGIONAL Endpoint
----------------------------------------
Before we configure a custom domain for our REST API, we'll deploy
our REST API so we can see the auto-generated domain name that API Gateway
creates for us.
First, we'll change our endpoint type from EDGE (the default) to REGIONAL.
Update your ``.chalice/config.json`` file so it looks like this::
$ cat .chalice/config.json
{
"version": "2.0",
"app_name": "customdomain",
"api_gateway_endpoint_type": "REGIONAL",
"stages": {
"dev": {
"api_gateway_stage": "api"
}
}
}
Now we'll deploy our application. Note the URL that's printed when
your application is deployed.
::
$ chalice deploy
Creating deployment package.
Creating IAM role: customdomain-dev
Creating lambda function: customdomain-dev
Creating Rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:12345:function:customdomain-dev
- Rest API URL: https://qxea58abcd.execute-api.us-west-2.amazonaws.com/api/
You now have an API up and running using API Gateway and Lambda::
$ curl https://qxea58abcd.execute-api.us-west-2.amazonaws.com/api/
{"hello": "world"}
The ``qxea58abcd.execute-api.us-west-2.amazonaws.com`` domain name was
auto-generated by API Gateway. Replacing this domain name with our own
custom domain name allows us to use simpler and more intuitive URLs that
we can provide to our API users.
Configuring a Custom Domain
---------------------------
In this tutorial, we're using Amazon Route53 to manage our DNS configuration.
If you're using a third-party domain registrar, the steps will be similar, but
you will have to create your DNS records using your provider's web interface or
API.
For this tutorial, we'll configure the domain ``chalice-demo-app.com``. Be
sure to replace this value with your own domain name.
Creating a Hosted Zone
~~~~~~~~~~~~~~~~~~~~~~
First, we'll need to create a hosted zone in Route 53. If you already have
a hosted zone created for your domain, you can skip this step.
We'll be using the AWS CLI V2 to configure our domain. You can follow
the
`installation instructions <https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html>`__
if you don't have the AWS CLI installed.
::
$ aws route53 create-hosted-zone --name chalice-demo-app.com --caller-reference 12345
{
"Location": "https://route53.amazonaws.com/2013-04-01/hostedzone/ZABCDEFGABCDEFGLDO822",
"HostedZone": {
"Id": "/hostedzone/ZABCDEFGABCDEFGLDO822",
"Name": "chalice-demo-app.com.",
"CallerReference": "12345",
"Config": {
"PrivateZone": false
},
"ResourceRecordSetCount": 2
},
"ChangeInfo": {
"Id": "/change/C07395431VDLB0CY65VP",
"Status": "PENDING",
"SubmittedAt": "2020-07-21T17:13:54.709000+00:00"
},
"DelegationSet": {
"NameServers": [
"ns-123.awsdns-31.net",
"ns-123.awsdns-05.com",
"ns-123.awsdns-09.org",
"ns-123.awsdns-40.co.uk"
]
}
}
You'll need to save the value of the hosted zone id for later.
From the output above the line ``"Id": "/hostedzone/ZABCDEFGABCDEFGLDO822",``
contains our hosted zone id of ``ZABCDEFGABCDEFGLDO822``. We'll refer
to this value as ``$OUR_HOSTED_ZONE_ID`` later.
You'll now need to register the ``"NameServers"`` shown in the output above
with your domain registrar.
Creating an ACM Certificate
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now that we have our hosted zone, we'll need to create an ACM certificate
associated with this domain. This is the SSL/TLS certificate that will be
used when requests are made to our custom domain. In this example, we'll
create a wildcard certificate for ``*.chalice-demo-app.com``. Note that
we're creating a ``REGIONAL`` endpoint type for our REST API, which means
that our ACM certificate **must** be in the same region as our REST API.
In this example, we're using ``us-west-2``. If you're using the default
``EDGE`` endpoint type, the ACM cert must be in ``us-east-1``. You can
explicitly specify the region using the ``--region`` CLI parameter if needed.
::
$ aws acm request-certificate --domain-name "*.chalice-demo-app.com" \
--validation-method DNS --idempotency-token 12345 \
--options CertificateTransparencyLoggingPreference=DISABLED
{
"CertificateArn": "arn:aws:acm:us-west-2:0123456789:certificate/578efbda-6bc7-4ae2-9964-6e6c3f58008b"
}
Save the value of ``CertificateArn`` shown in the output above. We'll
need this value when we configure our app to use this custom domain.
Before we can use this certificate, we need to validate this certificate.
This process demonstrates that we own or control the domain name associated
with the certificate.
In the command above, we used the ``--validation-method DNS``, which
requires us to add CNAME records to validate we control our domain name.
ACM supports both `DNS validation <https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html>`__
as well as `email validation <https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-email.html>`__.
To validate our domain, we'll now create the necessary CNAME records in our
hosted zone using the Route53 API. First, we need to retrieve the values
for our CNAME record. Be sure to replace the value of ``--certificate-arn``
with your own certificate ARN in the command below::
$ aws acm describe-certificate --certificate-arn arn:aws:acm:us-west-2:0123456789:certificate/578efbda-6bc7-4ae2-9964-6e6c3f58008b \
--query Certificate.DomainValidationOptions[0]
{
"DomainName": "*.chalice-demo-app.com",
"ValidationDomain": "*.chalice-demo-app.com",
"ValidationStatus": "PENDING_VALIDATION",
"ResourceRecord": {
"Name": "_1234567891234567897eb5512d9fb554.chalice-demo-app.com.",
"Type": "CNAME",
"Value": "_123456789123456789e7495341c27cd1.jfrzftwwjs.acm-validations.aws."
},
"ValidationMethod": "DNS"
}
Next we'll create a CNAME record for
``_1234567891234567897eb5512d9fb554.chalice-demo-app.com.`` with a value of
``_123456789123456789e7495341c27cd1.jfrzftwwjs.acm-validations.aws.``::
$ aws route53 change-resource-record-sets \
--hosted-zone-id $OUR_HOSTED_ZONE_ID --change-batch \
'{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "_0073e080112eb8de8c7eb5512d9fb554.chalice-demo-app.com.",
"Type": "CNAME",
"TTL": 300,
"ResourceRecords": [{"Value": "_6e560a5a9831aad210e7495341c27cd1.jfrzftwwjs.acm-validations.aws."}]
}
}
]
}'
# Command output:
{
"ChangeInfo": {
"Id": "/change/C0339874QPDDRA8TKT7U",
"Status": "PENDING",
"SubmittedAt": "2020-07-21T17:36:39.902000+00:00"
}
}
It will take a few minutes before ACM validates your domain. You can
move on to the next steps, or if you'd like to wait until the domain is
validated you can use the CLI's ``certificate-validated`` waiter, which
will block until the ACM certificate is validated::
$ aws acm wait certificate-validated \
--certificate-arn arn:aws:acm:us-west-2:0123456789:certificate/578efbda-6bc7-4ae2-9964-6e6c3f58008b
Chalice App Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~
Now that we have our hosted zone and ACM certificate created, we can configure
our Chalice application with our custom domain. To do so we need to add
`api_gateway_custom_domain <https://aws.github.io/chalice/topics/configfile.html#api-gateway-custom-domain>`__
configuration option and specify our ACM certificate ARN as well as the our
custom domain name. You're ``.chalice/config.json`` file should look like
this:
.. code-block:: json
{
"version": "2.0",
"app_name": "customdomain",
"api_gateway_endpoint_type": "REGIONAL",
"stages": {
"dev": {
"api_gateway_custom_domain": {
"domain_name": "api.chalice-demo-app.com",
"certificate_arn": "arn:aws:acm:us-west-2:0123456789:certificate/578efbda-6bc7-4ae2-9964-6e6c3f58008b"
},
"api_gateway_stage": "api"
}
}
}
We we rerun the ``chalice deploy`` command you'll notice there's a new
``Custom domain name:`` line in the output::
$ chalice deploy
Creating deployment package.
Updating policy for IAM role: customdomain-dev
Updating lambda function: customdomain-dev
Updating rest API
Creating custom domain name: api.chalice-demo-app.com
Creating api mapping: /
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:0123456789:function:customdomain-dev
- Rest API URL: https://qxea58abcd.execute-api.us-west-2.amazonaws.com/api/
- Custom domain name:
HostedZoneId: Z1UJRXOUMOOFQ8
AliasDomainName: d-6vj4cynstd.execute-api.us-west-2.amazonaws.com
Now that we've configured our Chalice app with our custom domain, there's one
step left. We need to update our DNS configuration to point to our REST API.
To do this, we'll use the values of ``HostedZoneId`` and ``AliasDomainName``
in the output above to create an alias record in our hosted zone.
Alias Record Configuration
~~~~~~~~~~~~~~~~~~~~~~~~~~
You can run the following command to create an alias record to your REST API.
Note that there are two different hosted zone id values here. The value
specified as the ``--hosted-zone-id`` value is the ID of our hosted zone
ID (``$OUR_HOSTED_ZONE_ID``) that we created earlier in this example.
The value of the ``HostedZoneId`` in the ``AliasTarget`` section is the
value of the ``HostedZoneId`` generated by API Gateway shown in the output
of ``chalice deploy`` above.
::
$ aws route53 change-resource-record-sets --hosted-zone-id ZABCDEFGABCDEFGLDO822 --change-batch \
'{
"Changes": [
{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "api.chalice-demo-app.com",
"Type": "A",
"AliasTarget": {
"DNSName": "d-6vj4cynstd.execute-api.us-west-2.amazonaws.com",
"HostedZoneId": "Z1UJRXOUMOOFQ8",
"EvaluateTargetHealth": false
}
}
}
]
}'
# Command output:
{
"ChangeInfo": {
"Id": "/change/C0539657Y0HMX8XBC5EH",
"Status": "PENDING",
"SubmittedAt": "2020-07-21T17:52:34.935000+00:00"
}
}
Verification
------------
Our Chalice application is now configured to use our custom domain.
We can verify this by making a request to our custom domain. In this
example, this is ``api.chalice-demo-app.com``::
$ curl -i https://api.chalice-demo-app.com/
HTTP/1.1 200 OK
Date: Tue, 21 Jul 2020 17:56:00 GMT
Content-Type: application/json
Content-Length: 17
Connection: keep-alive
x-amzn-RequestId: 9f33fbb9-6b10-469e-827f-f287199c9bc5
x-amz-apigw-id: QCPXoEwPIAMFi8Q=
X-Amzn-Trace-Id: Root=1-5f172c30-dccc232932a16a539dfc01b9;Sampled=0
{"hello":"world"}
Next Steps
----------
For more information on configuring custom domains, check out our
:doc:`topic guide <../topics/domainname>` on custom domains as well
as the config file reference for the
:ref:`custom-domain-config-options` and the
:ref:`custom-domain-ws-config-options` options.
---
### Source Tutorials Events (`docs/source/tutorials/events.rst`)
Event Sources Tutorial
======================
In the :doc:`../quickstart` guide, we looked at how to create a
REST API using the ``@app.route()`` decorator. Chalice also has
additional decorators that connects your code to specific event sources.
This results in your code being invoked when a specific event occurs.
In this tutorial we'll look at a few examples.
Installation and Configuration
------------------------------
If you haven't already setup and configured Chalice, see the
:doc:`../quickstart` for a step by step guide. In a nutshell, you can get a
basic Chalice app created with::
$ python3 --version
Python 3.10.20
$ python3 -m venv .venv
$ . .venv/bin/activate
$ python3 -m pip install chalice
$ chalice new-project chalice-sns-demo
$ cd chalice-sns-demo
We'll also be using the AWS CLI in this tutorial. You can follow
`these instructions <https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html>`__
for installing the AWS CLI v2.
Amazon SNS Topics
-----------------
In this first example, we'll create a Chalice application that will
call our Lambda function whenever a message is published to an
`SNS Topic <https://aws.amazon.com/sns/>`__.
First, we'll create an SNS topic. This is what we'll connect to our
Lambda function::
$ aws sns create-topic --name MyDemoTopic
{
"TopicArn": "arn:aws:sns:us-west-2:12345:MyDemoTopic"
}
Be sure to save the ``TopicArn`` value for later. In this example
that would be ``arn:aws:sns:us-west-2:12345:MyDemoTopic``.
Next, we'll update the ``app.py`` to create a lambda function that
connects to an SNS topic:
.. code-block:: python
from chalice import Chalice
app = Chalice(app_name='chalice-sns-demo', debug=True)
@app.on_sns_message(topic='MyDemoTopic')
def handle_sns_message(event):
app.log.debug("Received message with subject: %s, message: %s",
event.subject, event.message)
In the code above, we're using the ``@app.on_sns_message()`` decorator to
connect the SNS topic named ``MyDemoTopic`` with the ``handle_sns_message``
function. Note that we're using the name of the topic and not the
``TopicArn``.
Now we can deploy our chalice app::
$ chalice deploy
Creating deployment package.
Creating IAM role: chalice-demo-sns-dev
Creating lambda function: chalice-demo-sns-dev-handle_sns_message
Subscribing chalice-demo-sns-dev-handle_sns_message to SNS topic my-demo-topic
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-west-2:123:function:...
Now we can test our app by publishing a few SNS messages to our topic.
::
$ aws sns publish --topic-arn arn:aws:sns:us-west-2:12345:MyDemoTopic \
--subject TestSubject --message TestMessage
{
"MessageId": "abcdefgh-3e56-54bd-a471-72477b5388af"
}
$ aws sns publish --topic-arn arn:aws:sns:us-west-2:12345:MyDemoTopic \
--subject TestSubject2 --message TestMessage2
{
"MessageId": "abcdefgh-3e56-54bd-a471-72477b5388ag"
}
We should now see log messages showing that our Lambda function was invoked.
We can wait for the messages using the ``chalice logs`` command.
::
$ chalice logs --follow -n handle_sns_message
... 217378 chalice-sns-demo - DEBUG - Received message with subject: TestSubject, message: TestMessage
... 217378 chalice-sns-demo - DEBUG - Received message with subject: TestSubject2, message: TestMessage2
Next Steps
----------
In addition to SNS, chalice supports other event sources including Amazon S3,
Amazon SQS, as well as scheduled events. You can check out the topic guide
on :doc:`../topics/events` for more details.
Cleaning Up
-----------
Once you're done experimenting you can clean up by deleting the Chalice
app and deleting the SNS topic::
$ chalice delete
Deleting function: arn:aws:lambda:us-west-2:21345:function:chalice-sns-demo...
Deleting IAM role: chalice-sns-demo-dev
$ aws sns delete-topic --topic-arn arn:aws:sns:us-west-2:12345:MyDemoTopic
---
### Source Tutorials Index (`docs/source/tutorials/index.rst`)
Tutorials
=========
These step-by-step tutorials show you how to use various features of Chalice.
These are perfect if you're new to Chalice and want to learn what Chalice can
do. If you want more complete, real-world examples, you can check out
our :doc:`../samples/index`.
Rest API Tutorials
------------------
:doc:`basicrestapi`
This tutorial walks you through creating a REST API in
Chalice. It covers features such as routing, URL parameters, error handling,
etc.
:doc:`customdomain`
In this tutorial, we show you how to configure
a REST API with your own custom domain name.
.. _websocket-tutorial:
Websocket Tutorials
-------------------
:doc:`wsecho`
Learn the basics of creating websocket APIs in Chalice. This tutorial
creates an echo server that echoes back any message that the client sends to
the websocket server.
:doc:`wschat`
In this more complete example, learn how to create a basic chat application
based on websockets.
Event Source Tutorials
----------------------
:doc:`events`
This tutorial shows you how to create an event handler
that's triggered whenever a message is published to an SNS topic.
.. toctree::
:hidden:
:glob:
*
AWS CDK Tutorials
-----------------
:doc:`cdk`
This tutorial walks you through creating a REST API with a DynamoDB data
store that's deployed using the AWS CDK. It shows you how you can combine
the APIs of Chalice with CDK construct APIs
---
### Source Tutorials Wschat (`docs/source/tutorials/wschat.rst`)
Chat Server Example
===================
.. note::
This example is for illustration purposes and does not represent best
practices.
A simple chat server example application. This example will walk through
deploying a chat application with separate chat rooms and nicknames. It uses
a DynamoDB table to store state like connection IDs between websocket messages.
First install a copy of Chalice in a fresh environment, create a new project
and cd into the directory::
$ pip install -U chalice
$ chalice new-project chalice-chat-example
$ cd chalice-chat-example
Our Chalice application will need boto3 as a dependency for both DynamoDB
access and in order to communicate back with API Gateway to send websocket
messages. Let's add a boto3 to the ``requirements.txt`` file::
$ echo "boto3>=1.9.91" > requirements.txt
Now that the requirement has been added. Let's install it locally since our
next script will need it as well::
$ pip install -r requirements.txt
Unlike our previous example where we used ``chalice deploy``, we will use
``chalice package`` to create a CloudFormation template. The AWS CLI will be
used to deploy the template. To install the AWS CLI run the command::
$ pip install -U awscli
Starting in Chalice 1.10, the package command has a ``--merge-template``
argument that allows us to merge in a custom JSON file to the generated
CloudFormation template. Since Chalice does not have any built-in support for
DynamoDB currently, we will make a ``resources.json`` file with the DynamoDB
definition. The template file will set the environment variable TABLE in all
our Lambda functions as a CloudFormatiion reference to the DynamoDB table.
Finally, the template will also override our IAM policy with a custom one to
allow all the DynamoDB operations our application will need.
Below is the JSON file that contains all of our custom Cloudformation.
.. code-block:: json
:caption: resources.json
{
"Resources": {
"ChaliceChatTable": {
"Type": "AWS::DynamoDB::Table",
"Properties": {
"AttributeDefinitions": [
{
"AttributeName": "PK",
"AttributeType": "S"
},
{
"AttributeName": "SK",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "PK",
"KeyType": "HASH"
},
{
"AttributeName": "SK",
"KeyType": "RANGE"
}
],
"GlobalSecondaryIndexes": [
{
"IndexName": "ReverseLookup",
"KeySchema": [
{
"AttributeName": "SK",
"KeyType": "HASH"
},
{
"AttributeName": "PK",
"KeyType": "RANGE"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
}
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
},
"TableName": "ChaliceChat"
}
},
"WebsocketConnect": {
"Properties": {
"Environment": {
"Variables": {
"TABLE": {
"Ref": "ChaliceChatTable"
}
}
}
}
},
"WebsocketMessage": {
"Properties": {
"Environment": {
"Variables": {
"TABLE": {
"Ref": "ChaliceChatTable"
}
}
}
}
},
"WebsocketDisconnect": {
"Properties": {
"Environment": {
"Variables": {
"TABLE": {
"Ref": "ChaliceChatTable"
}
}
}
}
},
"DefaultRole": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
},
"Policies": [
{
"PolicyName": "DefaultRolePolicy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
{
"Effect": "Allow",
"Action": [
"execute-api:ManageConnections"
],
"Resource": "arn:aws:execute-api:*:*:*/@connections/*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:DeleteItem",
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:UpdateItem",
"dynamodb:Query",
"dynamodb:Scan"
],
"Resource": [
{
"Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ChaliceChatTable}"
},
{
"Fn::Sub": "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${ChaliceChatTable}/index/ReverseLookup"
}
]
}
]
}
}
]
}
}
}
}
The current directory layout should now look like this::
$ tree -a .
.
โโโ .chalice
โย ย โโโ config.json
โโโ .gitignore
โโโ app.py
โโโ resources.json
โโโ requirements.txt
1 directory, 5 files
Next let's fill out the ``app.py`` file since it is pretty simple. Most of this
example code is contained in the ``chalicelib/`` directory.
.. code-block:: python
:caption: chalice-chat-example/app.py
from boto3.session import Session
from chalice import Chalice
from chalicelib import Storage
from chalicelib import Sender
from chalicelib import Handler
app = Chalice(app_name="chalice-chat-example")
app.websocket_api.session = Session()
app.experimental_feature_flags.update([
'WEBSOCKETS'
])
STORAGE = Storage.from_env()
SENDER = Sender(app, STORAGE)
HANDLER = Handler(STORAGE, SENDER)
@app.on_ws_connect()
def connect(event):
STORAGE.create_connection(event.connection_id)
@app.on_ws_disconnect()
def disconnect(event):
STORAGE.delete_connection(event.connection_id)
@app.on_ws_message()
def message(event):
HANDLER.handle(event.connection_id, event.body)
Similar to the previous example. We need to use ``boto3`` to construct a
Session and pass it to ``app.websocket_api.session``. We opt into the
usage of the ``WEBSOCKET`` experimental feature. Most of the actual work is
done in some classes that we import from ``chalicelib/``. These classes are
detailed below, and the various parts are explained in comments and doc
strings. In addition to the previous example, we register a handler for
``on_ws_connect`` and ``on_ws_disconnect`` to handle events from API gateway
when a new socket is trying to connect, or an existing socket is disconnected.
Finally before being able to deploy and test the app out, we need to fill out
the chalicelib directory. This is the bulk of the app and it is explained
inline in comments. Create a new directory called ``chalicelib`` and inside
that directory create an ``__init__.py`` file and fill it out with the
following file.
.. code-block:: python
:caption: chalice-chat-example/chalicelib/__init__.py
import os
import boto3
from boto3.dynamodb.conditions import Key
from chalice import WebsocketDisconnectedError
class Storage(object):
"""An abstraction to interact with the DynamoDB Table."""
def __init__(self, table):
"""Initialize Storage object
:param table: A boto3 dynamodb Table resource object.
"""
self._table = table
@classmethod
def from_env(cls):
"""Create table from the environment.
The environment variable TABLE is present for a deployed application
since it is set in all of the Lambda functions by a CloudFormation
reference. We default to '', which will happen when we run
``chalice package`` since it loads the application, and no
environment variable has been set. For local testing, a value should
be manually set in the environment if '' will not suffice.
"""
table_name = os.environ.get('TABLE', '')
table = boto3.resource('dynamodb').Table(table_name)
return cls(table)
def create_connection(self, connection_id):
"""Create a new connection object in the dtabase.
When a new connection is created, we create a stub for
it in the table. The stub uses a primary key of the
connection_id and a sort key of username_. This translates
to a connection with an unset username. The first message
sent over the wire from the connection is to be used as the
username, and this entry will be re-written.
:param connection_id: The connection id to write to
the table.
"""
self._table.put_item(
Item={
'PK': connection_id,
'SK': 'username_',
},
)
def set_username(self, connection_id, old_name, username):
"""Set the username.
The SK entry that goes with this connection id that starts
with username_ is taken to be the username. The previous
entry needs to be deleted, and a new entry needs to be
written.
:param connection_id: Connection id of the user trying to
change their name.
:param old_name: The original username. Since this is part of
the key, it needs to be deleted and re-created rather than
updated.
:param username: The new username the user wants.
"""
self._table.delete_item(
Key={
'PK': connection_id,
'SK': 'username_%s' % old_name,
},
)
self._table.put_item(
Item={
'PK': connection_id,
'SK': 'username_%s' % username,
},
)
def list_rooms(self):
"""Get a list of all rooms that exist.
Scan through the table looking for SKs that start with room_
which indicates a room that a user is in. Collect a unique set
of those and return them.
"""
r = self._table.scan()
rooms = set([item['SK'].split('_', 1)[1] for item in r['Items']
if item['SK'].startswith('room_')])
return rooms
def set_room(self, connection_id, room):
"""Set the room a user is currently in.
The room a user is in is in the form of an SK that starts with
room_ prefix.
:param connection_id: The connection id to move to a room.
:param room: The room name to join.
"""
self._table.put_item(
Item={
'PK': connection_id,
'SK': 'room_%s' % room,
},
)
def remove_room(self, connection_id, room):
"""Remove a user from a room.
The room a user is in is in the form of an SK that starts with
room_ prefix. To leave a room we need to delete this entry.
:param connection_id: The connection id to move to a room.
:param room: The room name to join.
"""
self._table.delete_item(
Key={
'PK': connection_id,
'SK': 'room_%s' % room,
},
)
def get_connection_ids_by_room(self, room):
"""Find all connection ids that go to a room.
This is needed whenever we broadcast to a room. We collect all
their connection ids so we can send messages to them. We use a
ReverseLookup table here which inverts the PK, SK relationship
creating a partition called room_{room}. Everything in that
partition is a connection in the room.
:param room: Room name to get all connection ids from.
"""
r = self._table.query(
IndexName='ReverseLookup',
KeyConditionExpression=(
Key('SK').eq('room_%s' % room)
),
Select='ALL_ATTRIBUTES',
)
return [item['PK'] for item in r['Items']]
def delete_connection(self, connection_id):
"""Delete a connection.
Called when a connection is disconnected and all its entries need
to be deleted.
:param connection_id: The connection partition to delete from
the table.
"""
try:
r = self._table.query(
KeyConditionExpression=(
Key('PK').eq(connection_id)
),
Select='ALL_ATTRIBUTES',
)
for item in r['Items']:
self._table.delete_item(
Key={
'PK': connection_id,
'SK': item['SK'],
},
)
except Exception as e:
print(e)
def get_record_by_connection(self, connection_id):
"""Get all the properties associated with a connection.
Each connection_id creates a partition in the table with multiple
SK entries. Each SK entry is in the format {property}_{value}.
This method reads all those records from the database and puts them
all into dictionary and returns it.
:param connection_id: The connection to get properties for.
"""
r = self._table.query(
KeyConditionExpression=(
Key('PK').eq(connection_id)
),
Select='ALL_ATTRIBUTES',
)
r = {
entry['SK'].split('_', 1)[0]: entry['SK'].split('_', 1)[1]
for entry in r['Items']
}
return r
class Sender(object):
"""Class to send messages over websockets."""
def __init__(self, app, storage):
"""Initialize a sender object.
:param app: A Chalice application object.
:param storage: A Storage object.
"""
self._app = app
self._storage = storage
def send(self, connection_id, message):
"""Send a message over a websocket.
:param connection_id: API Gateway Connection ID to send a
message to.
:param message: The message to send to the connection.
"""
try:
# Call the chalice websocket api send method
self._app.websocket_api.send(connection_id, message)
except WebsocketDisconnectedError as e:
# If the websocket has been closed, we delete the connection
# from our database.
self._storage.delete_connection(e.connection_id)
def broadcast(self, connection_ids, message):
""""Send a message to multiple connections.
:param connection_id: A list of API Gateway Connection IDs to
send the message to.
:param message: The message to send to the connections.
"""
for cid in connection_ids:
self.send(cid, message)
class Handler(object):
"""Handler object that handles messages received from a websocket.
This class implements the bulk of our app behavior.
"""
def __init__(self, storage, sender):
"""Initialize a Handler object.
:param storage: Storage object to interact with database.
:param sender: Sender object to send messages to websockets.
"""
self._storage = storage
self._sender = sender
# Command table to translate a string command name into a
# method to call.
self._command_table = {
'help': self._help,
'nick': self._nick,
'join': self._join,
'room': self._room,
'quit': self._quit,
'ls': self._list,
}
def handle(self, connection_id, message):
"""Entry point for our application.
:param connection_id: Connection id that the message came from.
:param message: Message we got from the connection.
"""
# First look the user up in the database and get a record for it.
record = self._storage.get_record_by_connection(connection_id)
if record['username'] == '':
# If the user does not have a username, we assume that the message
# is the username they want and we call _handle_login_message.
self._handle_login_message(connection_id, message)
else:
# Otherwise we assume the user is logged in. So we call
# a method to handle the message. We pass along the
# record we loaded from the database so we don't need to
# again.
self._handle_message(connection_id, message, record)
def _handle_login_message(self, connection_id, message):
"""Handle a login message.
The message is the username to give the user. Re-write the
database entry for this user to reset their username from ''
to {message}. Once that is done send a message back to the user
to confirm the name choice. Also send a /help prompt.
"""
self._storage.set_username(connection_id, '', message)
self._sender.send(
connection_id,
'Using nickname: %s\nType /help for list of commands.' % message
)
def _handle_message(self, connection_id, message, record):
""""Handle a message from a connected and logged in user.
If the message starts with a / it's a command. Otherwise its a
text message to send to all rooms in the room.
:param connection_id: Connection id that the message came from.
:param message: Message we got from the connection.
:param record: A data record about the sender.
"""
if message.startswith('/'):
self._handle_command(connection_id, message[1:], record)
else:
self._handle_text(connection_id, message, record)
def _handle_command(self, connection_id, message, record):
"""Handle a command message.
Check the command name and look it up in our command table.
If there is an entry, we call that method and pass along
the connection_id, arguments, and the loaded record.
:param connection_id: Connection id that the message came from.
:param message: Message we got from the connection.
:param record: A data record about the sender.
"""
args = message.split(' ')
command_name = args.pop(0).lower()
command = self._command_table.get(command_name)
if command:
command(connection_id, args, record)
else:
# If no command method is found, send an error message
# back to the user.
self._sender(
connection_id, 'Unknown command: %s' % command_name)
def _handle_text(self, connection_id, message, record):
"""Handle a raw text message.
:param connection_id: Connection id that the message came from.
:param message: Message we got from the connection.
:param record: A data record about the sender.
"""
if 'room' not in record:
# If the user is not in a room send them an error message
# and return early.
self._sender.send(
connection_id, 'Cannot send message if not in chatroom.')
return
# Collect a list of connection_ids in the same room as the message
# sender.
connection_ids = self._storage.get_connection_ids_by_room(
record['room'])
# Prefix the message with the sender's name.
message = '%s: %s' % (record['username'], message)
# Broadcast the new message to everyone in the room.
self._sender.broadcast(connection_ids, message)
def _help(self, connection_id, _message, _record):
"""Send the help message.
Build a help message and send back to the same connection.
:param connection_id: Connection id that the message came from.
"""
self._sender.send(
connection_id,
'\n'.join([
'Commands available:',
' /help',
' Display this message.',
' /join {chat_room_name}',
' Join a chatroom named {chat_room_name}.',
' /nick {nickname}',
' Change your name to {nickname}. If no {nickname}',
' is provided then your current name will be printed',
' /room',
' Print out the name of the room you are currently ',
' in.',
' /ls',
' If you are in a room, list all users also in the',
' room. Otherwise, list all rooms.',
' /quit',
' Leave current room.',
'',
'If you are in a room, raw text messages that do not start ',
'with a / will be sent to everyone else in the room.',
]),
)
def _nick(self, connection_id, args, record):
"""Change or check nickname (username).
:param connection_id: Connection id that the message came from.
:param args: Argument list that came after the command.
:param record: A data record about the sender.
"""
if not args:
# If a nickname argument was not provided, we just want to
# report the current nickname to the user.
self._sender.send(
connection_id, 'Current nickname: %s' % record['username'])
return
# The first argument is assumed to be the new desired nickname.
nick = args[0]
# Change the username from record['username'] to nick in the storage
# layer.
self._storage.set_username(connection_id, record['username'], nick)
# Send a message to the requestor to confirm the nickname change.
self._sender.send(connection_id, 'Nickname is: %s' % nick)
# Get the room the user is in.
room = record.get('room')
if room:
# If the user was in a room, announce to the room they have
# changed their name. Don't send this me sage to the user since
# they already got a name change message.
room_connections = self._storage.get_connection_ids_by_room(room)
room_connections.remove(connection_id)
self._sender.broadcast(
room_connections,
'%s is now known as %s.' % (record['username'], nick))
def _join(self, connection_id, args, record):
"""Join a chat room.
:param connection_id: Connection id that the message came from.
:param args: Argument list. The first argument should be the
name of the room to join.
:param record: A data record about the sender.
"""
# Get the room name to join.
room = args[0]
# Call quit to leave the current room we are in if there is any.
self._quit(connection_id, '', record)
# Get a list of connections in the target chat room.
room_connections = self._storage.get_connection_ids_by_room(room)
# Join the target chat room.
self._storage.set_room(connection_id, room)
# Send a message to the requestor that they have joined the room.
# At the same time send an announcement to everyone who was already
# in the room to alert them of the new user.
self._sender.send(
connection_id, 'Joined chat room "%s"' % room)
message = '%s joined room.' % record['username']
self._sender.broadcast(room_connections, message)
def _room(self, connection_id, _args, record):
"""Report the name of the current room.
:param connection_id: Connection id that the message came from.
:param record: A data record about the sender.
"""
if 'room' in record:
# If the user is in a room send them the name back.
self._sender.send(connection_id, record['room'])
else:
# If the user is not in a room. Tell them so, and how to
# join a room.
self._sender.send(
connection_id,
'Not currently in a room. Type /join {room_name} to do so.'
)
def _quit(self, connection_id, _args, record):
"""Quit from a room.
:param connection_id: Connection id that the message came from.
:param record: A data record about the sender.
"""
if 'room' not in record:
# If the user is not in a room there is nothing to do.
return
# Find the current room name, and delete that entry from
# the database.
room_name = record['room']
self._storage.remove_room(connection_id, room_name)
# Send a message to the user to inform them they left the room.
self._sender.send(
connection_id, 'Left chat room "%s"' % room_name)
# Tell everyone in the room that the user has left.
self._sender.broadcast(
self._storage.get_connection_ids_by_room(room_name),
'%s left room.' % record['username'],
)
def _list(self, connection_id, _args, record):
"""Show a context dependent listing.
:param connection_id: Connection id that the message came from.
:param record: A data record about the sender.
"""
room = record.get('room')
if room:
# If the user is in a room, get a listing of everyone
# in the room.
result = [
self._storage.get_record_by_connection(c_id)['username']
for c_id in self._storage.get_connection_ids_by_room(room)
]
else:
# If they are not in a room. Get a listing of all rooms
# currently open.
result = self._storage.list_rooms()
# Send the result list back to the requestor.
self._sender.send(connection_id, '\n'.join(result))
The final directory layout should be ::
$ tree -a .
.
โโโ .chalice
โย ย โโโ config.json
โโโ .gitignore
โโโ app.py
โโโ chalicelib
โย ย โโโ __init__.py
โโโ resources.json
โโโ requirements.txt
2 directories, 6 files
Deploying our app with CloudFormation requires 3 steps. First we use Chalice
to package our app into a JSON CloudFormation template::
$ chalice package --merge-template resources.json out
This will result in a new directory called ``out`` being created, inside which
there is a ``sam.json`` file. This template contains our Chalice app as a
CloudFormation template, merged with our ``resources.json`` template.
Next we use the AWS CLI to package this template, and prepare it for
deployment. In order for this to work you will need to replace ``$BUCKET``
with the name of a bucket you control::
$ aws cloudformation package --template-file out/sam.json \
--s3-bucket $BUCKET --output-template-file out/template.yml
Once this is complete, a new template should be located at ``out/template.yml``
this is the final CloudFormation template which is ready for deployment.
Deploying it with the AWS CLI can be done with the following command::
$ aws cloudformation deploy --template-file out/template.yml \
--stack-name ChaliceChat --capabilities CAPABILITY_IAM
This command should wait awhile, and once it exits the app should be ready. To
get the websocket connection URL, we can use the AWS CLI again to check the
stack output ``WebsocketConnectEndpointURL``::
$ aws cloudformation describe-stacks --stack-name ChaliceChat \
--query "Stacks[0].Outputs[?OutputKey=='WebsocketConnectEndpointURL'].OutputValue" \
--output text
wss://{id}.execute-api.{region}.amazonaws.com/api/
Once deployed we can take the result of the previous command and connect to it
using ``wsdump.py``. Below is a sample of two running clients, the first
message sent to the server is used as the client's username.
.. code-block:: bash
:caption: client-1
$ wsdump.py wss://{id}.execute-api.{region}.amazonaws.com/api/
Press Ctrl+C to quit
> John
< Using nickname: John
Type /help for list of commands.
> /help
< Commands available:
/help
Display this message.
/join {chat_room_name}
Join a chatroom named {chat_room_name}.
/nick {nickname}
Change your name to {nickname}. If no {nickname}
is provided then your current name will be printed
/room
Print out the name of the room you are currently
in.
/ls
If you are in a room, list all users also in the
room. Otherwise, list all rooms.
/quit
Leave current room.
If you are in a room, raw text messages that do not start
with a / will be sent to everyone else in the room.
> /join chalice
< Joined chat room "chalice"
< Jenny joined room.
> Hi
< John: Hi
< Jenny is now known as JennyJones.
> /quit
< Left chat room "chalice"
> /ls
< chalice
> Ctrl-D
.. code-block:: bash
:caption: client-2
$ wsdump.py wss://{id}.execute-api.{region}.amazonaws.com/api/
Press Ctrl+C to quit
> Jenny
< Using nickname: Jenny
Type /help for list of commands.
> /help
< Commands available:
/help
Display this message.
/join {chat_room_name}
Join a chatroom named {chat_room_name}.
/nick {nickname}
Change your name to {nickname}. If no {nickname}
is provided then your current name will be printed
/room
Print out the name of the room you are currently
in.
/ls
If you are in a room, list all users also in the
room. Otherwise, list all rooms.
/quit
Leave current room.
If you are in a room, raw text messages that do not start
with a / will be sent to everyone else in the room.
> /join chalice
< Joined chat room "chalice"
> /ls
< John
Jenny
< John: Hi
> /nick JennyJones
< Nickname is: JennyJones
< John left room.
> /ls
< JennyJones
> /room
< chalice
> /nick
< Current nickname: JennyJones
> Ctrl-D
To delete the resources you can run use the AWS CLI to delete the stack::
$ aws cloudformation delete-stack --stack-name ChaliceChat
---
### Source Tutorials Wsecho (`docs/source/tutorials/wsecho.rst`)
Echo Server Example
===================
An echo server is a simple server that echos any message it receives back to
the client that sent it.
First install a copy of Chalice in a fresh environment, create a new project
and cd into the directory::
$ pip install -U chalice
$ chalice new-project echo-server
$ cd echo-server
Our Chalice application will need boto3 as a dependency for both API Gateway
to send websocket messages. Let's add a boto3 to the ``requirements.txt``
file::
$ echo "boto3>=1.9.91" > requirements.txt
Now that the requirement has been added. Let's install it locally since our
next script will need it as well::
$ pip install -r requirements.txt
Next replace the contents of the ``app.py`` file with the code below.
.. code-block:: python
:caption: app.py
:linenos:
from boto3.session import Session
from chalice import Chalice
from chalice import WebsocketDisconnectedError
app = Chalice(app_name="echo-server")
app.websocket_api.session = Session()
app.experimental_feature_flags.update([
'WEBSOCKETS'
])
@app.on_ws_message()
def message(event):
try:
app.websocket_api.send(
connection_id=event.connection_id,
message=event.body,
)
except WebsocketDisconnectedError as e:
pass # Disconnected so we can't send the message back.
Stepping through this app line by line, the first thing to note is that we
need to import and instantiate a boto3 session. This session is manually
assigned to ``app.websocket_api.session``.
This is needed because in order to send websocket responses to API Gateway we
need to construct a boto3 client. Chalice does not take a direct dependency
on boto3 or botocore, so we need to provide the Session ourselves.
.. code-block:: python
from boto3.session import Session
app.websocket_api.session = Session()
Next we enable the experimental feature ``WEBSOCKETS``. Websockets are an
experimental feature and are subject to API changes. This includes all aspects
of the Websocket API exposted in Chalice. Including any public members of
``app.websocket_api``, and the three decorators ``on_ws_connect``,
``on_ws_message``, and ``on_ws_disconnect``.
.. code-block:: python
app.experimental_feature_flags.update([
'WEBSOCKETS'
])
To register a websocket handler, and cause Chalice to deploy an
API Gateway Websocket API we use the ``app.on_ws_message()`` decorator.
The event parameter here is a wrapper object with some convenience
parameters attached. The most useful are ``event.connection_id`` and
``event.body``. The ``connection_id`` is an API Gateway specific identifier
that allows you to refer to the connection that sent the message. The ``body``
is the content of the message.
.. code-block:: python
@app.on_ws_message()
def message(event):
Since this is an echo server, the message handler simply reads the content it
received on the socket, and rewrites it back to the same socket. To send a
message to a socket we call ``app.websocket_api.send(connection_id, message)``.
In this case, we just use the same ``connection_id`` we got the message from,
and use the ``body`` we got from the event as the ``message`` to send.
.. code-block:: python
app.websocket_api.send(
connection_id=event.connection_id,
message=event.body,
)
Finally, we catch the exception ``WebsocketDisconnectedError`` which is raised
by ``app.websocket_api.send`` if the provided ``connection_id`` is not
connected anymore. In our case this doesn't really matter since we don't have
anything tracking our connections. The error has a ``connection_id`` property
that contains the offending connection id.
.. code-block:: python
except WebsocketDisconnectedError as e:
pass # Disconnected so we can't send the message back.
Now that we understand the code, lets deploy it with ``chalice deploy``::
$ chalice deploy
Creating deployment package.
Creating IAM role: echo-server-dev
Creating lambda function: echo-server-dev-websocket_message
Creating websocket api: echo-server-dev-websocket-api
Resources deployed:
- Lambda ARN: arn:aws:lambda:region:0123456789:function:echo-server-dev-websocket_message
- Websocket API URL: wss://{websocket_api_id}.execute-api.region.amazonaws.com/api/
To test out the echo server we will use the ``websocket-client`` package. You
install it from PyPI::
$ pip install websocket-client
After deploying the Chalice app the output will contain a URL for connecting
to the websocket API labeled: ``- Websocket API URL:``. The
``websocket-client`` package installs a command line tool called ``wsdump.py``
which can be used to test websocket echo server::
$ wsdump wss://{websocket_api_id}.execute-api.region.amazonaws.com/api/
Press Ctrl+C to quit
> foo
< foo
> bar
< bar
> foo bar baz
< foo bar baz
>
Every message sent to the server (lines that start with ``>``) result in a
message sent to us (lines that start with ``<``) with the same content.
If something goes wrong, you can check the chalice error logs using the
following command::
$ chalice logs -n websocket_message
.. note::
If you encounter an Internal Server Error here it is likely that you forgot
to include ``boto3>=1.9.91`` in the ``requirements.txt`` file.
To tear down the example. Just run::
$ chalice delete
Deleting Websocket API: {websocket_api_id}
Deleting function: arn:aws:lambda:us-west-2:0123456789:function:echo-server-dev-websocket_message
Deleting IAM role: echo-server-dev
Next Steps
----------
In this tutorial, we created an echo server with websockets.
If you'd like to try something more ambitious, you can follow our
tutorial for creating a sample :doc:`Chat application with websocket <wschat>`.
---
### Source Samples Media Query Index (`docs/source/samples/media-query/index.rst`)
=======================
Media Query Application
=======================
.. youtube:: UCZXJpI1dKw
:width: 75%
This sample application shows how to combine multiple event handlers in Chalice
to create an image processing pipeline. It takes as input any image or video
and it will identify objects, people, text, scenes, and activities. This
results of this analysis can then be queried with a REST API.
.. image:: docs/assets/appexample.jpg
:width: 100%
:alt: Application Example
There are several components of this application. The first part is an image
processing pipeline. The application is registered to automatically process
any media that's uploaded to an Amazon S3 bucket. The application will then
use Amazon Rekognition to automatically detect labels in either the image
or the video. The returned labels are then stored in an Amazon DynamoDB
table.
For videos, an asynchronous job is started. This is because the analysis
for videos takes longer than analyzing images so we don't want our Lambda
function to block until the job is complete. To handle this asynchronous
job, we subscribe to an Amazon SNS topic. When the asynchronous job
is finished analyzing our uploaded video, an event handler is called that
will retrieve the results and store the labels in Amazon DynamoDB.
The final component is the REST API. This allows users to query for
labels associated with the media that has been uploaded.
You can find the full source code for this application in our
`samples directory on GitHub <https://github.com/aws/chalice/tree/master/docs/source/samples/media-query/code/>`__.
::
$ git clone git://github.com/aws/chalice
$ cd chalice/docs/source/samples/media-query/code
We'll now walk through the architecture of the application, how to
deploy and use the application, and go over the application code.
.. note::
This sample application is also available as a `workshop
<https://chalice-workshop.readthedocs.io/en/latest/media-query/index.html>`__.
The main difference between the sample apps here and the Chalice workshops
is that the workshop is a detailed step by step process for how to create
this application from scratch. You build the app by gradually adding each
feature piece by piece. It takes several hours to work through all the
workshop material. In this document we review the architecture,
the deployment process, then walk through the main sections of the code.
Architecture
============
Below is the architecture for the application.
.. image:: docs/assets/architecture.png
:width: 100%
:alt: Architecture diagram
The main components of the application are as follows:
* ``handle_object_created``: A Lambda function that is triggered when an
object is uploaded to a S3 bucket. If the object is an image, it will
call Amazon Rekognition's ``DetectLabels`` API to detect objects in the
image. With the detected objects, the Lambda function will then add the
object to an Amazon DynamoDB table. If the object is a video, it will call
Rekognition's ``StartLabelDetection`` API to initiate an asynchronous
job to detect labels in the video. When the job is completed, a completion
notification is pushed to an SNS topic.
* ``handle_object_deleted``: A Lambda function that removes the object from
the DynamoDB table if the object is deleted from the S3 bucket.
* ``add_video_labels``: A Lambda function that is triggered on video label
detection SNS messages. On invocation, it will call Rekognition's
``GetLabelDetection`` API to retrieve all detected objects from the video.
It then adds the video with its labels to the DynamoDB Table
* ``api_handler``: A Lambda function that is invoked by HTTP requests to
Amazon API Gateway. On invocation, it will query the database based on the
received HTTP request and return the results to the user through API Gateway.
Deployment
==========
First, we'll setup our development environment by cloning the Chalice
GitHub repository and copying the sample code in a new directory::
$ git clone git://github.com/aws/chalice
$ mkdir /tmp/demo
$ cp -r chalice/docs/source/samples/media-query/code/ /tmp/demo/media-query
$ cd /tmp/demo/media-query/
Next configure a virtual environment that uses Python 3. In this example
we're using Python 3.7.
::
$ python3 -m venv /tmp/venv37
$ . /tmp/venv37/bin/activate
To deploy the application, first install the necessary requirements and
install Chalice::
$ pip install -r requirements.txt
$ pip install chalice
We'll also be using the AWS CLI to help deploy our application, you can
follow the `installation instructions <https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html>`__
if you don't have the AWS CLI installed.
Next, we'll use the AWS CLI to deploy a CloudFormation stack containing the S3
bucket, DynamoDB table, and SNS topic needed to run this application::
$ aws cloudformation deploy --template-file resources.json \
--stack-name media-query --capabilities CAPABILITY_IAM
Record the deployed resources as environment variables in the Chalice
application by running the `recordresources.py` script::
$ python recordresources.py --stack-name media-query
You can see these values by looking at the ``.chalice/config.json`` file.
Once those resources are created and recorded, deploy the Chalice application::
$ chalice deploy
Using the Application
=====================
Once the application is deployed, use the AWS CLI to fetch the name of the
bucket that is storing the media files::
$ aws cloudformation describe-stacks --stack-name media-query \
--query "Stacks[0].Outputs[?OutputKey=='MediaBucketName'].OutputValue" \
--output text
media-query-mediabucket-xtrhd3c4b59
Upload some sample media files to your Amazon S3 bucket so the system populates
information about the media files in your DynamoDB table. If you need sample
media files, you can use the included samples from the corresponding
`Chalice workshop <https://chalice-workshop.readthedocs.io/en/latest/media-query/index.html>`__ assets
`here <https://github.com/aws-samples/chalice-workshop/tree/master/code/media-query/final/assets>`__.
::
$ aws s3 cp assets/sample.jpg s3://media-query-mediabucket-xtrhd3c4b59/sample.jpg
$ aws s3 cp assets/sample.mp4 s3://media-query-mediabucket-xtrhd3c4b59/sample.mp4
Wait about a minute for the media files to be populated in the database and
then install HTTPie::
$ pip install httpie
Then, list out all if the media files using the application's API with HTTPie::
$ chalice url
https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/
$ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 279
Content-Type: application/json
Date: Tue, 10 Jul 2018 17:58:40 GMT
Via: 1.1 fa751ee53e2bf18781ae98b293ff9375.cloudfront.net (CloudFront)
X-Amz-Cf-Id: sNnrzvbdvgj1ZraySJvfSUbHthC_fok8l5GJ7glV4QcED_M1c8tlvg==
X-Amzn-Trace-Id: Root=1-5b44f3d0-4546157e8f5e35a008d06d88;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: J0sIlHs3vHcFj9g=
x-amzn-RequestId: e0aaf4e1-846a-11e8-b756-99d52d342d60
[
{
"labels": [
"Animal",
"Canine",
"Dog",
"German Shepherd",
"Mammal",
"Pet",
"Collie"
],
"name": "sample.jpg",
"type": "image"
},
{
"labels": [
"Human",
"Clothing",
"Dog",
"Nest",
"Person",
"Footwear",
"Bird Nest",
"People",
"Animal",
"Husky"
],
"name": "sample.mp4",
"type": "video"
}
]
You can include query string parameters as well to query all objects based
on what the file name starts with, the type of the media file, and the detected
objects in the media file::
$ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/ startswith==sample.m
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 153
Content-Type: application/json
Date: Tue, 10 Jul 2018 19:20:02 GMT
Via: 1.1 aa42484f82c16d99015c599631def20c.cloudfront.net (CloudFront)
X-Amz-Cf-Id: euqlOlWN5k5V_zKCJy4SL988Vcje6W5jDR88GrWr5uYGH-_ZvN4arg==
X-Amzn-Trace-Id: Root=1-5b4506e0-db041a3492ee56e8f3d9457c;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: J04DHE92PHcF--Q=
x-amzn-RequestId: 3d82319d-8476-11e8-86d9-a1e4585e5c26
[
{
"labels": [
"Human",
"Clothing",
"Dog",
"Nest",
"Person",
"Footwear",
"Bird Nest",
"People",
"Animal",
"Husky"
],
"name": "sample.mp4",
"type": "video"
}
]
$ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/ media-type==image
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 126
Content-Type: application/json
Date: Tue, 10 Jul 2018 19:20:53 GMT
Via: 1.1 88eb066576c1b47cd896ab0019b9f25f.cloudfront.net (CloudFront)
X-Amz-Cf-Id: rwuOwzLKDM4KgcSBXFihWeNNsYSpZDYVpc8IXdT0xOu8qz8aA2Pj3w==
X-Amzn-Trace-Id: Root=1-5b450715-de71cf04ca2900b839ff1194;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: J04LaE6YPHcF3VA=
x-amzn-RequestId: 5d29d59a-8476-11e8-a347-ebb5d5f47789
[
{
"labels": [
"Animal",
"Canine",
"Dog",
"German Shepherd",
"Mammal",
"Pet",
"Collie"
],
"name": "sample.jpg",
"type": "image"
}
]
$ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/ label==Person
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 153
Content-Type: application/json
Date: Tue, 10 Jul 2018 19:20:02 GMT
Via: 1.1 aa42484f82c16d99015c599631def20c.cloudfront.net (CloudFront)
X-Amz-Cf-Id: euqlOlWN5k5V_zKCJy4SL988Vcje6W5jDR88GrWr5uYGH-_ZvN4arg==
X-Amzn-Trace-Id: Root=1-5b4506e0-db041a3492ee56e8f3d9457c;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: J04DHE92PHcF--Q=
x-amzn-RequestId: 3d82319d-8476-11e8-86d9-a1e4585e5c26
[
{
"labels": [
"Human",
"Clothing",
"Dog",
"Nest",
"Person",
"Footwear",
"Bird Nest",
"People",
"Animal",
"Husky"
],
"name": "sample.mp4",
"type": "video"
}
]
You can also query for a specific object::
$ http https://qi5hf4djdg.execute-api.us-west-2.amazonaws.com/api/sample.jpg
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 126
Content-Type: application/json
Date: Tue, 10 Jul 2018 19:20:53 GMT
Via: 1.1 88eb066576c1b47cd896ab0019b9f25f.cloudfront.net (CloudFront)
X-Amz-Cf-Id: rwuOwzLKDM4KgcSBXFihWeNNsYSpZDYVpc8IXdT0xOu8qz8aA2Pj3w==
X-Amzn-Trace-Id: Root=1-5b450715-de71cf04ca2900b839ff1194;Sampled=0
X-Cache: Miss from cloudfront
x-amz-apigw-id: J04LaE6YPHcF3VA=
x-amzn-RequestId: 5d29d59a-8476-11e8-a347-ebb5d5f47789
[
{
"labels": [
"Animal",
"Canine",
"Dog",
"German Shepherd",
"Mammal",
"Pet",
"Collie"
],
"name": "sample.jpg",
"type": "image"
}
]
Code Walkthrough
================
We'll take a top-down approach with this application and start with the main
entry point, the ``app.py`` file. The source code for this application is
split between the ``app.py`` as well as supporting code in ``chalicelib/``.
Event Handlers
--------------
In the ``app.py`` we see four different decorator types, each corresponding
to Lambda functions that are triggered by different events. Note that
the line numbers correspond to the line numbers in the ``app.py`` file.
.. literalinclude:: code/app.py
:lineno-match:
:start-after: # Start of Event Handlers
:end-before: # End of Event Handlers
The first two decorators use ``@app.on_s3_event`` and are specifying
that these two Lambda functions should be invoked when an object is
created or deleted from S3, respectively. The name of the S3 bucket
is not hardcoded in the ``app.py`` file but instead pulled from the
environment variable ``MEDIA_BUCKET_NAME``. The ``recordresources.py``
script that was run as part of the deployment process described above
automatically created these resources and updated the Chalice config file
(``.chalice/config.json``) with these values. If you look at the
contents of your ``.chalice/config.json`` file, it should look something
like this:
.. code-block:: json
{
"version": "2.0",
"app_name": "media-query",
"stages": {
"dev": {
"api_gateway_stage": "api",
"autogen_policy": false,
"environment_variables": {
"MEDIA_TABLE_NAME": "media-query-MediaTable-10QEPR0O8DOT4",
"MEDIA_BUCKET_NAME": "media-query-mediabucket-fb8oddjbslv1",
"VIDEO_TOPIC_NAME": "media-query-VideoTopic-KU38EEHIIUV1",
"VIDEO_ROLE_ARN": "arn:aws:iam::123456789123:role/media-query-VideoRole-1GKK0CA30VCAD",
"VIDEO_TOPIC_ARN": "arn:aws:sns:us-west-2:123456789123:media-query-VideoTopic-KU38EEHIIUV1"
}
}
}
}
Next, the ``@app.on_sns_message`` is used to connect an SNS topic to our
Lambda function. This is only used for video processing with Rekognition.
Because of the longer processing times of video compared to images,
video analysis is performed by first starting a "video label job". When
you start this asynchronous job, we can specify an SNS topic that
Rekognition will publish to when the job is complete, as shown in the
``_handle_created_video`` function below.
.. literalinclude:: code/app.py
:linenos:
:lineno-match:
:pyobject: _handle_created_video
The ``add_video_file()`` function will then query for the results
of the job (the ``JobId`` is provided as part of the SNS message
that's published) and store the results in the DynamoDB table.
The final two decorators of this app creates a REST API with
Amazon API Gateway and defines two routes: ``/`` and ``/{name}``.
Requesting the root URL of ``/`` is equivalent to a "List" API call that will
return all the media files that have been analyzed so far. Request
``/{name}``, where ``{name}`` is the name of the media file that was uploaded
to S3 will return the detected labels for that single resource. This is
equivalent to a "Get" API call.
.. note::
This sample application returns all analyzed media files in its
List API call. In practice, you should paginate your List API calls
to ensure you don't return unbounded results.
Supporting Files
----------------
The event handlers described in the previous section interact with
Rekognition and DynamoDB through clients that are accessed through
``get_rekognition_client()`` and ``get_rekognition_client()``
respectively. These clients are high level wrappers to the corresponding
boto3 clients/resources for these services. The code for these
high level clients are in ``chalicelib/rekognition.py`` and
``chalicelib/db.py``. If we look at the ``DynamoMediaDB.add_media_file()``
method in the ``chalicelib/db.py`` file, we see that it's a small wrapper
around the ``put_item()`` operation of the underlying DynamoDB API:
.. literalinclude:: code/chalicelib/db.py
:linenos:
:lineno-match:
:pyobject: DynamoMediaDB.add_media_file
We see a similar pattern in ``chalicelib/rekognition.py``. Here's the
``start_video_label_job`` job that starts the asynchronous processing
discussed in the previous section.
.. literalinclude:: code/chalicelib/rekognition.py
:linenos:
:lineno-match:
:pyobject: RekognitonClient.start_video_label_job
As you can see, it's a small wrapper around the ``start_label_detection``
operation of the underlying Rekognition API.
We encourage you to look through the rest of the ``chalicelib/`` directory
to see how these high level clients are implemented.
Cleaning Up
===========
If you're done experimenting with this sample app, you can run these commands
to delete this app.
1. Delete the chalice application::
$ chalice delete
Deleting Rest API: kyfn3gqcf0
Deleting function: arn:aws:lambda:us-west-2:123456789123:function:media-query-dev
Deleting IAM role: media-query-dev-api_handler
Deleting function: arn:aws:lambda:us-west-2:123456789123:function:media-query-dev-add_video_file
Deleting IAM role: media-query-dev-add_video_file
Deleting function: arn:aws:lambda:us-west-2:123456789123:function:media-query-dev-handle_object_removed
Deleting IAM role: media-query-dev-handle_object_removed
Deleting function: arn:aws:lambda:us-west-2:123456789123:function:media-query-dev-handle_object_created
Deleting IAM role: media-query-dev-handle_object_created
2. Delete all objects in your S3 bucket::
$ aws s3 rm s3://$MEDIA_BUCKET_NAME --recursive
delete: s3://media-query-mediabucket-4b1h8anboxpa/sample.jpg
delete: s3://media-query-mediabucket-4b1h8anboxpa/sample.mp4
3. Delete the CloudFormation stack containing the additional AWS resources::
$ aws cloudformation delete-stack --stack-name media-query
---
### Source Samples Todo App Index (`docs/source/samples/todo-app/index.rst`)
================
Todo Application
================
This is a sample application that allows you to manage Todo items. This
tutorial will walk through creating a serverless web API to create, update,
get, and delete Todos, managing Todos in a database, and adding authorization
with JWT. AWS services covered include AWS Lambda, Amazon API Gateway, Amazon
DynamoDB, AWS CodeBuild, and AWS Systems Manager.
You can find the full source code for this application in our
`samples directory on GitHub <https://github.com/aws/chalice/tree/master/docs/source/samples/todo-app/code/>`__.
::
$ git clone git://github.com/aws/chalice
$ cd chalice/docs/source/samples/todo-app/code
We'll now walk through the architecture of this application,
how to deploy and use the application, and finally we'll go over
the main components of the application code.
.. note::
This sample application is also available as a `workshop
<https://chalice-workshop.readthedocs.io/en/latest/todo-app/index.html>`__.
The main difference between the sample apps here and the Chalice workshops
is that the workshop is a detailed step by step process for how to create
this application from scratch. You build the app by gradually adding each
feature piece by piece. In the workshop, we first create a REST API with
no authentication or data store. Then we introduce DynamoDB, then JWT
auth, etc. The workshop also shows you how to set up a CI/CD pipeline
to automatically deploy your application whenever you push to your git
repository. It takes several hours to work through all the workshop
material. In this document we review the architecture, the deployment process,
then walk through the main sections of the final version of this
application.
Architecture
============
The main component of this application is a REST API backed by Amazon API
Gateway and AWS Lambda. The rest API lets you manage a Todo list. It lets you
create a new Todo list as well as check off existing Todo items.
In order to see a list of your Todo items, you must first log in. Information
about our users is stored in an Amazon DynamoDB table. The authentication is
done using a builtin authorizer. This lets you define a Lambda function to
perform your custom auth process. For this sample app, we're using JSON Web
Tokens (JWT).
The Todo items are stored in a separate DynamoDB table. Below is
an architecture diagram of our sample app. It shows the API Gateway
REST API, along with a Lambda function for our authorizer, a Lambda function
for our REST API, and two DynamoDB tables.
.. image:: docs/assets/architecture.jpg
:width: 100%
:alt: Architecture diagram
.. _todo-sample-rest-api:
REST API
--------
The REST API supports the following resources:
* GET - ``/todos/`` - Gets a list of all todo items
* POST - ``/todos/`` - Creates a new Todo item
* GET - ``/todos/{id}`` - Gets a specific todo item
* DELETE - ``/todos/{id}`` - Deletes a specific todo item
* PUT - ``/todos/{id}`` - Updates the state of a todo item
A todo item has this schema::
{
"description": {"type": "str"},
"uid": {"type: "str"},
"state": {"type: "str", "enum": ["unstarted", "started", "completed"]},
"metadata": {
"type": "object"
}
}
Deployment
==========
To run and deploy this application, first create a virtual environment
and install the dependencies. Python 3.7 is used for this sample app.
::
$ python3 -m /tmp/venv37
$ . /tmp/venv37/bin/activate
$ pip install ./requirements-dev.txt
$ pip install ./requirements.txt
As part of this application, there are additional resources that
are created that are used by this application, including two DynamoDB
tables as well as an SSM parameter used to store our secret key used
in our JWT auth. To create these resources, you can run::
$ python create-resources.py
This will also update your ``.chalice/config.json`` file with environment
variables containing the name of the DynamoDB tables that were created.
At this point, you can either test the application by running ``chalice
local``. This will start a local HTTP server on port 8000 that emulates API
Gateway so that you can test without having to deploy your application to AWS.
You can also run ``chalice deploy`` to deploy your application to
AWS, which allows you to test on an actual API Gateway REST API::
$ chalice deploy
Creating deployment package.
Creating IAM role: mytodo-dev-api_handler
Creating lambda function: mytodo-dev
Creating IAM role: mytodo-dev-jwt_auth
Creating lambda function: mytodo-dev-jwt_auth
Creating Rest API
Resources deployed:
- Lambda ARN: arn:aws:lambda:us-east-1:12345:function:mytodo-dev
- Lambda ARN: arn:aws:lambda:us-east-1:12345:function:mytodo-dev-jwt_auth
- Rest API URL: https://abcd.execute-api.us-west-2.amazonaws.com/api/
Using the Application
=====================
If you've deployed your application using ``chalice deploy``, you can test
the REST API by making requests to the ``Rest API URL``, shown in the output
of ``chalice deploy``, in our example that would be
``https://abcd.execute-api.us-west-2.amazonaws.com/api/``. If you're using
``chalice local``, you'll make requests to ``http://localhost:8000/``.
Before we can make requests we need to authenticate with the API. In order
to authenticate with the API we need to create user accounts.
A helper script, ``users.py`` is included in the repository to help you
manage users. The first thing we'll need to do is create a user::
$ python users.py --create-user
Username: myusername
Password:
This will create a new entry in our users DynamoDB table.
You can then test that the password verification works by running::
$ python users.py --test-password
Username: myusername
Password:
Password verified.
Once we've created a test user, we can now login by sending a POST
request to the ``/login`` URL::
$ echo '{"username": "myusername", "password": "mypassword"}' | \
http POST https://abcd.execute-api.us-west-2.amazonaws.com/api/login/
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJteXVzZXJuYW1lIiwiaWF0IjoxNTk1NDU3Njg5LCJuYmYiOjE1OTU0NTc2ODksImp0aSI6IjMxNjc4YzFkLTdkZjEtNGEzOC04YmZiLTllZjZiMGM1YzAyNyJ9.w46RdtzZdk_P0LAh_St3wjsqgh-k-Hp1ykTpbDqad2k",
}
.. note::
We're using the HTTPie command line tool instead of cURL. You can
install this tool by running ``pip install httpie``.
Now whenever we make any requests to our REST API, we need to include
the token value in the output above as the value of our ``Authorization``
header. For example, we can list all of our Todos, which is initially
empty::
$ http https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/ 'Authorization: my.jwt.token'
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: application/json
[]
If you omit the ``Authorization`` header, you'll see this error response::
$ http https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/
HTTP/1.1 401 Unauthorized
Content-Length: 26
Content-Type: application/json
x-amzn-ErrorType: UnauthorizedException
{
"message": "Unauthorized"
}
We can create a new Todo::
$ echo '{"description": "My first Todo", "metadata": {}}' \
| http POST https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/ \
'Authorization: my.jwt.token'
HTTP/1.1 200 OK
Content-Length: 36
Content-Type: application/json
e25643f7-0b18-47d2-b124-4e6713ab527c
Now when we list our Todos, we'll see our new entry we created::
$ http https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/ 'Authorization: my.jwt.token'
HTTP/1.1 200 OK
Content-Length: 136
Content-Type: application/json
[
{
"description": "My first Todo",
"metadata": {},
"state": "unstarted",
"uid": "e25643f7-0b18-47d2-b124-4e6713ab527c",
"username": "myusername"
}
]
We can update our Todo and mark it completed::
$ echo '{"state": "completed"}' | \
http PUT https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/e25643f7-0b18-47d2-b124-4e6713ab527c \
'Authorization: my.jwt.token'
HTTP/1.1 200 OK
Content-Length: 4
Content-Type: application/json
null
And we can now verify that the Todo item shows up as completed::
$ http https://abcd.execute-api.us-west-2.amazonaws.com/api/todos/e25643f7-0b18-47d2-b124-4e6713ab527c \
'Authorization: my.jwt.token'
HTTP/1.1 200 OK
Content-Length: 134
Content-Type: application/json
{
"description": "My first Todo",
"metadata": {},
"state": "completed",
"uid": "e25643f7-0b18-47d2-b124-4e6713ab527c",
"username": "myusername"
}
Code Walkthrough
================
.. _todo-app-rest-api:
Rest API
--------
Below is the code for the five routes defined in the
:ref:`todo-sample-rest-api` section defined in the ``app.py`` file:
.. literalinclude:: code/app.py
:caption: app.py
:linenos:
:lineno-match:
:lines: 67-105
The first thing all of these routes do is extract the current username from the
request. This is done by examining the context associated with the current
request. This will include the ``principalId``, or the current username, which
is discussed in more detail in the :ref:`todo-app-jwt-auth` section below.
Each of these routes then makes a call into the data storage layer,
and either retrieves or updates data in the ``Todo`` DynamoDB table.
This is discussed in the next section on data storage.
The application DB is tracked as a module level variable that is
retrieved through the ``get_app_db()`` function. The name of the
DynamoDB table is provided through the ``APP_TABLE_NAME`` environment
variable, which is specified in your ``.chalice/config.json`` file.
This was automatically filled in for you when you ran the
``create-resources.py`` script.
User input is extracted from both the URL (the ``uid`` associated with
a Todo item is provided as part of the URL) as well as the JSON
request body. A key takeaway from these routes is that there's
minimal logic in the route definitions themselves. They're primarily
about extracting user input and then delegating the heavy lifting to
other objects that are independent of any routing information.
Data Storage
------------
Each route in this sample application app makes a call to the
data storage layer, which is backed by a DynamoDB table. This interface
is defined by the ``TodoDB`` interface, which is defined in the
``chalicelib/db.py`` file:
.. literalinclude:: code/chalicelib/db.py
:caption: chalicelib/db.py
:linenos:
:lineno-match:
:pyobject: TodoDB
There are two different implementations of this interface. The first one,
``InMemoryTodoDB``, is an in-memory implementation of this interface where
all data is stored within the process. The purpose of this implementation
is for testing purposes when you don't want to work with the real DynamoDB
service. This allows you to develop your application locally and test
using ``chalice local``. The other implementation of ``TodoDB`` interface is
``DynamoDBTodo``, which communicates with the actual DynamoDB service
to store and retrieve Todo items. It uses the Table resource of ``boto3``,
created via ``boto3.resource('dynamodb').Table(TABLE_NAME)``. This allows
us to use the `high level querying interface of boto3 <https://boto3.amazonaws.com/v1/documentation/api/latest/reference/customizations/dynamodb.html#dynamodb-conditions>`__.
The implementation is shown below.
.. literalinclude:: code/chalicelib/db.py
:caption: chalicelib/db.py
:linenos:
:lineno-match:
:pyobject: DynamoDBTodo
.. _todo-app-jwt-auth:
JWT Authentication
------------------
.. note::
This example is for illustration purposes and does not necessarily
represent best practices. Its intent is to show how custom
authentication can be implemented in a Chalice app.
Our REST API for our Todo items requires that you send an appropriate
``Authorization`` header when making HTTP requests. You can retrieve
a auth token by making a request to the ``/login`` route with your
user name and password. The underlying mechanism used to handle
our auth functionality is through issuing a
`JWT <https://datatracker.ietf.org/doc/html/rfc7519>`__ when you login.
Users Table
~~~~~~~~~~~
In order to login, we need a way to store and retrieve user information. This
is done through our ``Users`` DynamoDB table. This was created when you ran
the ``create-resoureces.py`` file. Each user record stores their username and
information about their password. We're using PBKDF2 as our key derivation
function for password hashing, which is available in Python's standard library
through the `hashlib.pbkdf2_hmac
<https://docs.python.org/3/library/hashlib.html#hashlib.pbkdf2_hmac>`__
function. The parameters needed by ``pbkdf2_hmac`` are stored in each user's
record, including the password hash, salt, number of rounds, and the hash used
for PBKDF2 (sha256 in our example). These user entries were created and stored
in the ``Users`` DynamoDB table when you ran the ``python users.py
--create-user`` command.
You can see the fields for a specific user by using the ``--get-user`` option
to the ``users.py`` script::
$ python users.py --get-user myusername
Entry for user: myusername
hash : sha256
username : myusername
hashed : Hym8Ss6WIArus+aZ6BucZ3sz6Wu5w8Tc3lPUivTuUi4=
salt : rXMPBx8ZriKU3SQTh58BlxQQtpcLHfmITTB2tpRs/sM=
rounds : 100000
Login Flow
~~~~~~~~~~
Below is the code for the ``/login`` route:
.. literalinclude:: code/app.py
:caption: app.py
:linenos:
:lineno-match:
:pyobject: login
In this login view, we first lookup the user record fom our users DB,
and then try to generate a JWT token for this entry. The
``auth.get_jwt_token`` will first verify that the password hash
matches what's stored in our users DB, and then generate a JWT token
for this user as shown in the code below:
.. literalinclude:: code/chalicelib/auth.py
:caption: chalicelib/auth.py
:linenos:
:lineno-match:
:pyobject: get_jwt_token
The call to ``jwt.encode()`` requires a payload and a secret.
This secret is a value that is only known to our application and is
used in our built-in authorizer to verify the JWT is valid.
This secret value is stored as an SSM parameter. A random secret
was automatically generated and stored in SSM for you when running the
``create-resources.py`` script. When we call ``auth.get_jwt_token`` we first
retrieve this value from SSM as shown in the ``get_auth_key()`` function
defined in our ``app.py`` file:
.. literalinclude:: code/app.py
:caption: app.py
:linenos:
:lineno-match:
:pyobject: get_auth_key
Once we've generated a JWT token, we return the token back to the caller.
They must then provide that same token in the ``Authorization`` header
whenever they make API calls to the REST API.
Custom Authorizer
~~~~~~~~~~~~~~~~~
In order to require that a specific route requires proper authorization,
we must first create an authorizer, and then associate it with any routes
that require auth. Chalice supports different types of
:doc:`../../topics/authorizers`, and in this example we're using the
:ref:`builtin-authorizers` type provided by Chalice. This lets us write
our custom authorization logic as part of our Chalice app. To do this,
we decorate our auth function with the ``@app.authorizer`` decorator.
Our custom authorizer logic takes the JWT token (accessible through the
``auth_request.token`` attribute, and verifies the token is valid
using our secret key retrieved via ``get_auth_key()``. The custom
authorizer is shown below:
.. literalinclude:: code/app.py
:caption: app.py
:linenos:
:lineno-match:
:pyobject: jwt_auth
Once we verify that JWT token is valid, we return an ``AuthResponse`` that
specifies what routes the user is allowed to access. In our example, we're
giving them access to all routes, denoted by a ``*``.
Now that we have our authorizer, we can associate with a route by providing
the function as the value of the ``authorizer=`` parameter. We saw this in the
:ref:`todo-app-rest-api` section above. For example, note that the
``@app.route()`` decorator is being provided an ``authorizer`` function:
.. literalinclude:: code/app.py
:caption: app.py
:linenos:
:lineno-match:
:pyobject: list_todos
Cleaning Up
===========
Once you're finished experimenting with this sample app, you can cleanup your
resources by deleting the Chalice app and deleting any additional resources
associated with this app. To do this, first delete your Chalice app::
$ chalice delete
Deleting Rest API: q7dc49grhk
Deleting function: arn:aws:lambda:us-west-w:12345:function:mytodo-dev-jwt_auth
Deleting IAM role: mytodo-dev-jwt_auth
Deleting function: arn:aws:lambda:us-west-w:12345:function:mytodo-dev
Deleting IAM role: mytodo-dev-api_handler
Then to cleanup the remaining resources, rerun the
``create-resources.py`` script with the ``--cleanup`` flag. This will delete
the DynamoDB tables and the SSM parameter, along with any additional resources
created as part of your Chalice app::
$ python create-resources.py --cleanup
Deleting table: todo-app-632a558c-8355-4c2d-a46e-24350f371389
Deleting table: users-app-05b34fa2-1ae6-4d81-95d1-7ced59878a2b
Deleting SSM param: /todo-sample-app/auth-key
Resources deleted. If you haven't already, be sure to run 'chalice delete' to delete your Chalice application.
---
### Source Samples Media Query Code Resources.Json (`docs/source/samples/media-query/code/resources.json`)
{
"Outputs": {
"MediaBucketName": {
"Value": {
"Ref": "MediaBucket"
}
},
"MediaTableName": {
"Value": {
"Ref": "MediaTable"
}
},
"VideoTopicArn": {
"Value": {
"Ref": "VideoTopic"
}
},
"VideoTopicName": {
"Value": {
"Fn::GetAtt": [
"VideoTopic",
"TopicName"
]
}
},
"VideoRoleArn": {
"Value": {
"Fn::GetAtt": [
"VideoRole",
"Arn"
]
}
}
},
"Resources": {
"MediaBucket": {
"Type": "AWS::S3::Bucket"
},
"MediaTable": {
"Properties": {
"AttributeDefinitions": [
{
"AttributeName": "name",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "name",
"KeyType": "HASH"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 5,
"WriteCapacityUnits": 5
}
},
"Type": "AWS::DynamoDB::Table"
},
"VideoTopic": {
"Type": "AWS::SNS::Topic"
},
"VideoRole": {
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"sts:AssumeRole"
],
"Principal": {
"Service": [
"rekognition.amazonaws.com"
]
}
}
]
},
"Policies": [
{
"PolicyName": "RekognitionPublish",
"PolicyDocument": {
"Statement": [
{
"Effect": "Allow",
"Action": [
"sns:Publish"
],
"Resource": [
{
"Ref": "VideoTopic"
}
]
}
]
}
}
]
},
"Type": "AWS::IAM::Role"
}
}
}
---
### Source Samples Media Query Code .Chalice Config.Json (`docs/source/samples/media-query/code/.chalice/config.json`)
{
"version": "2.0",
"app_name": "media-query",
"stages": {
"dev": {
"api_gateway_stage": "api",
"autogen_policy": false
}
}
}
---
### Source Samples Media Query Code .Chalice Policy Dev.Json (`docs/source/samples/media-query/code/.chalice/policy-dev.json`)
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
},
{
"Action": [
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:UpdateItem",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:Query"
],
"Resource": [
"arn:aws:dynamodb:*:*:table/media-query-*"
],
"Effect": "Allow"
},
{
"Action": [
"rekognition:DetectLabels",
"rekognition:StartLabelDetection",
"rekognition:GetLabelDetection"
],
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::media-query*",
"Effect": "Allow"
},
{
"Action": [
"iam:PassRole"
],
"Resource": "arn:aws:iam::*:role/media-query-*",
"Effect": "Allow"
}
]
}
---
### Source Samples Todo App Code .Chalice Config.Json (`docs/source/samples/todo-app/code/.chalice/config.json`)
{
"stages": {
"dev": {
"autogen_policy": false,
"api_gateway_stage": "api"
}
},
"version": "2.0",
"app_name": "mytodo"
}
---
### Source Samples Todo App Code .Chalice Policy Dev.Json (`docs/source/samples/todo-app/code/.chalice/policy-dev.json`)
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*",
"Effect": "Allow"
},
{
"Action": [
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:UpdateItem",
"dynamodb:GetItem",
"dynamodb:Scan",
"dynamodb:Query"
],
"Resource": [
"arn:aws:dynamodb:*:*:table/todo-app-*",
"arn:aws:dynamodb:*:*:table/users-app-*"
],
"Effect": "Allow"
},
{
"Action": [
"ssm:GetParameter"
],
"Resource": [
"arn:aws:ssm:*:*:parameter/todo-sample-app/auth-key"
],
"Effect": "Allow"
}
]
}
---
--- METRICS ---
- Files Extracted: 42
- Estimated Token Budget: ~96937 tokens
- Canonical Reference: https://codewiki.google/github.com/aws/chalice