{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## Creating the AWS Serverless Application Model (SAM) application\n", "\n", "### [Verify pre-requisites for AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html#serverless-sam-cli-install-prerequisites)\n", "SAM requires python 2.7 or 3.6 and pip to be installed to continue." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import boto3\n", "import os\n", "import sagemaker\n", "from sagemaker import get_execution_role\n", "import project_path # path to helper methods\n", "from lib import workshop\n", "\n", "role = get_execution_role()\n", "\n", "s3 = boto3.resource('s3')\n", "cfn = boto3.client('cloudformation')\n", "\n", "# use the region-specific sample data bucket\n", "region = boto3.Session().region_name\n", "stack_name = 'serverless-hello'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## [Create S3 Bucket](https://docs.aws.amazon.com/AmazonS3/latest/gsg/CreatingABucket.html)\n", "\n", "We will create an S3 bucket that will be used throughout the workshop for storing our data.\n", "\n", "[s3.create_bucket](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.create_bucket) boto3 documentation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bucket = workshop.create_bucket_name('sam-')\n", "s3.create_bucket(Bucket=bucket, CreateBucketConfiguration={'LocationConstraint': region})\n", "print(bucket)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Verify Python and pip versions\n", "\n", "Both Python and pip will be installed when using SageMaker managed notebooks." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!python --version\n", "!pip --version" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Installing AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)\n", "\n", "The primary distribution method for the AWS SAM CLI on Linux, Windows, and macOS is pip, a package manager for Python that provides an easy way to install, upgrade, and remove Python packages and their dependencies.\n", "\n", "Pre-requistites:\n", "* Docker\n", "* [AWS Command Line Interface (AWS CLI)](https://docs.aws.amazon.com/cli/latest/userguide/)\n", "* (Pip only) Python 2.7 or Python 3.6" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip install aws-sam-cli" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Verify the latest version of SAM is installed " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%bash\n", "export PATH=$PATH:/home/ec2-user/.local/bin\n", "sam --version\n", "sam init -r python3.6 -n serverless-todo" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!cat serverless-todo/README.md" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Load required dependencies locally to the application" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip install -r serverless-todo/hello_world/requirements.txt -t serverless-todo/hello_world/" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pygmentize serverless-todo/hello_world/app.py" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Validate template](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-validate-template.html)\n", "\n", "The `aws cloudformation validate-template` command is designed to check only the syntax of your template. It does not ensure that the property values that you have specified for a resource are valid for that resource. Nor does it determine the number of resources that will exist when the stack is created." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws cloudformation validate-template --template-body file://serverless-todo/template.yaml" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Package deployment](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-cli-package.html)\n", "\n", "\n", "For some resource properties that require an Amazon S3 location (a bucket name and filename), you can specify local references instead. For example, you might specify the S3 location of your AWS Lambda function's source code or an Amazon API Gateway REST API's OpenAPI (formerly Swagger) file. Instead of manually uploading the files to an S3 bucket and then adding the location to your template, you can specify local references, called local artifacts, in your template and then use the package command to quickly upload them. A local artifact is a path to a file or folder that the package command uploads to Amazon S3. For example, an artifact can be a local path to your AWS Lambda function's source code or an Amazon API Gateway REST API's OpenAPI file." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws cloudformation package \\\n", " --template-file serverless-todo/template.yaml \\\n", " --output-template-file serverless-todo/sam-template.yaml \\\n", " --s3-bucket $bucket \\\n", " --s3-prefix serverless" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [Deploy Application](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-cli-deploy.html)\n", "\n", "AWS CloudFormation requires you to use a change set to create a template that includes transforms. Instead of independently creating and then executing a change set, use the `aws cloudformation deploy` command. When you run this command, it creates a change set, executes the change set, and then terminates. This command reduces the numbers of required steps when you create or update a stack that includes transforms." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws cloudformation deploy \\\n", " --template-file serverless-todo/sam-template.yaml \\\n", " --stack-name serverless-hello \\\n", " --capabilities CAPABILITY_IAM" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### [View Output](https://docs.amazonaws.cn/en_us/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html)\n", "\n", "The `aws cloudformation describe-stacks` command provides information on your running stacks. You can use an option to filter results on a stack name. This command returns information about the stack, including the name, stack identifier, and status." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws cloudformation describe-stacks --stack-name $stack_name --region $region" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Get the API Endpoint created from the SAM deployment" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "stacks = cfn.describe_stacks(StackName=stack_name)\n", "stack = stacks[\"Stacks\"][0]\n", "\n", "for output in stack[\"Outputs\"]:\n", " if output[\"OutputKey\"] == 'HelloWorldApi':\n", " api_endpoint = output[\"OutputValue\"]\n", "# print('%s=%s (%s)' % (output[\"OutputKey\"], output[\"OutputValue\"], output[\"Description\"]))\n", " \n", "print(api_endpoint)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Call REST API\n", "\n", "After we create and deploy the project we will install [httpie](https://httpie.org/) which is a framework that consists of a single `http` command designed for interaction with HTTP servers, RESTful APIs, and web services. Replace the variable `{{api-gw-address}}` with the `HelloWorldApi` in `Outputs` above." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip install httpie" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('calling {0}'.format(api_endpoint))\n", "!http $api_endpoint" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create a TODO application from the Hello World example\n", "\n", "Now that you have learned how to create a serverless application with the Hello World example it's time for you to create a todo application from the example. There is sample code for a [todo](./todos) application.\n", "\n", "Steps to create\n", "* Modify the `template.yaml` to include the REST paths for GET/LIST/PUT/DELETE for the application and include the DynamoDB table in the template that will be used to store and retrieve the todos.\n", "* Create a Cognito user pool to provide authentication capabilties to your API.\n", "* Modify the `template.yaml` file to include a `CustomAuthorizer` for the PUT endpoint.\n", "* Run through example REST calls using `httpie`\n", "* Cleanup\n", "\n", "When you are done modifying the `template.yaml` we can go ahead an repackage and deploy the template to update the service. [CloudFormation Package](#package)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Create a todo" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "echo '{ \"text\": \"My First Todo\" }' | http POST https://{api-url}/dev/todos\n", "http https://{api-url}/dev/todos" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### List the todos available" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!http POST https://{api-url}/dev/todos" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now you can see how easy it is to create a microservice using Lambda but what if we wanted to only allow authenticated users to have the ability to create todos? We can create a Cognito user pool and use it as an authorizer to the API. Create a new Cognito user pool and hook it up to the create method of the service. \n", "\n", "Change the template.yml file to add the authorizer like below.\n", "\n", "```\n", " authorizer:\n", " type: COGNITO_USER_POOLS\n", " authorizerId:\n", " Ref: TodoApiGatewayAuthorizer\n", "```\n", "\n", "Let's redeploy the service and try and add a todo" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now package and deploy the updated service. [CloudFormation Package](#package)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!echo '{ \"text\": \"My First Todo\" }' | http POST https://{api-url}/dev/todos" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This should have returned an HTTP/1.1 401 Unauthorized because you did not send the appropriate Authorization header on the call. We will simulate a user logging into the application and passing the bearer token in the headers of the request. Running the 3 methods below will simulate creating a user, confirming the user, and generating the appropriate auth for the user to be able to create the todo. Grab the IdToken generated and add it to the headers like the command below.\n", "\n", "### Create user in Cognito" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "username = '{{username}}'\n", "password = '{{password}}'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws cognito-idp sign-up --region $region --client-id {cognito-client-id} --username $username --password $password\n", " " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%writefile auth.json\n", "\n", "{\n", " \"UserPoolId\": \"{{your-cognito-user-pool}}\",\n", " \"ClientId\": \"{{your-cognito-client-id}}\",\n", " \"AuthFlow\": \"ADMIN_NO_SRP_AUTH\",\n", " \"AuthParameters\": {\n", " \"USERNAME\": \"{{username}}\",\n", " \"PASSWORD\": \"{{passsword}\"\n", " }\n", "}\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Confirm the sign up of the user" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws cognito-idp admin-confirm-sign-up --region $region --user-pool-id {cognito-user-pool} --username {{username}}\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Initiate the authentication to generate the token" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws cognito-idp admin-initiate-auth --region $region --cli-input-json file://auth.json" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!echo '{ \"text\": \"My Authenticated Todo\" }' | http POST https://{api-url}/dev/todos Authorization:\"Bearer {your-idtoken}\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now you should have created a new todo after the authorizer approves the token.\n", "\n", "### Register a user\n", "\n", "> aws cognito-idp sign-up --region {your-aws-region} --client-id {your-client-id} --username admin@example.com --password {password}\n", "\n", "### Confirm user registration\n", "\n", "> aws cognito-idp admin-confirm-sign-up --region {your-aws-region} --user-pool-id {your-user-pool-id} --username admin@example.com\n", "\n", "### Authenticate (get tokens)\n", "\n", "> aws cognito-idp admin-initiate-auth --region {your-aws-region} --cli-input-json file://auth.json\n", "\n", "### Where auth.json is:\n", "\n", ">{\n", " \"UserPoolId\": \"{your-user-pool-id}\",\n", " \"ClientId\": \"{your-client-id}\",\n", " \"AuthFlow\": \"ADMIN_NO_SRP_AUTH\",\n", " \"AuthParameters\": {\n", " \"USERNAME\": \"admin@example.com\",\n", " \"PASSWORD\": \"{password}\"\n", " }\n", "}\n", "\n", "### You should get a response like this if everything is set up correctly:\n", "\n", ">{\n", " \"AuthenticationResult\": {\n", " \"ExpiresIn\": 3600,\n", " \"IdToken\": \"{your-idtoken}\",\n", " \"RefreshToken\": \"{your-refresh-token}\",\n", " \"TokenType\": \"Bearer\",\n", " \"AccessToken\": \"{your-access-token}\"\n", " },\n", " \"ChallengeParameters\": {}\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Cleanup" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!aws cloudformation delete-stack --stack-name $stack_name" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "workshop.delete_bucket_completely(bucket)" ] } ], "metadata": { "kernelspec": { "display_name": "conda_python3", "language": "python", "name": "conda_python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.5" } }, "nbformat": 4, "nbformat_minor": 2 }