One place for hosting & domains

      Platform

      From 0 to 3 Million+ Deployments: Scaling App Platform on Kubernetes


      Video

      About the Talk

      Get a better understanding of how your builds run on DigitalOcean App Platform, and how to scale efficiently when your user base expands.

      App Platform, a new DigitalOcean managed-service offering, was released in October 2020 and has already been deployed over 3 million times. This immense growth didn’t come without its challenges around infrastructure, observability, and release velocity.

      Kamal and Nick share techniques and strategies that have helped DigitalOcean overcome these problem areas and the benefits received from each solution.

      About the Presenters

      Kamal Nasser
      Kamal Nasser is a Senior Software Engineer at DigitalOcean. When not automating and playing with modern software and technologies, you’ll likely find him penning early 17th century calligraphy.

      Nick Tate
      Nicholas Tate is a Tech Lead on the App Platform team at DigitalOcean. He has worked on the DigitalOcean Managed Kubernetes team and before that, on multi-cloud Kubernetes at Containership. He loves everything and anything related to cloud native infrastructure and enjoys playing guitar.



      Source link

      How To Build a Python REST API with Fauna and Deploy it to DigitalOcean App Platform


      Introduction

      Many developers don’t have the time or experience to set up and manage infrastructure for their applications. To keep up with deadlines and reduce costs, developers need to find solutions that allow them to deploy their apps to the cloud as quickly and efficiently as possible to focus on writing the code and delivering new features to their customers. Together, DigitalOcean’s App Platform and Fauna provide that ability.

      DigitalOcean App Platform is a Platform-as-a-Service (PaaS) that abstracts the infrastructure that runs your apps. It also lets you deploy applications by pushing your code to a Git branch.

      Fauna is a powerful data layer for applications of any size. As you’ll see in this tutorial, with Fauna, you can get a database up and running quickly without having to worry about the database operations.

      Together, these two solutions let you focus on your application instead of managing your infrastructure.

      In this tutorial, you’ll integrate Fauna with Python by writing a minimal REST API using the Flask framework. You’ll then deploy the API to DigitalOcean’s App Platform from a Git repository.The API will consist of:

      • A public /signup POST endpoint for creating users in the Users collection.
      • A public /login POST endpoint for authenticating with the documents in the Users collection.
      • A private /things GET endpoint for fetching a list of Fauna documents from the Things collection.

      The finished Python project is available at this Github repository.

      Prerequisites

      Before starting this tutorial, you will need:

      Step 1 — Setting Up the Fauna Database

      In the first step, you will configure a Fauna database and create the collections for the API. Fauna is a document-based database rather than a traditional table-based relational database. Fauna stores your data in documents and collections, which are groups of documents.

      To create the collections, you will execute queries using FQL, Fauna’s native query language. FQL is an expressive and powerful query language that gives you access to the full power of Fauna.

      To get started, log into Fauna’s dashboard. After logging in, click on the Create Database button at the top.

      In the New Database form, use PYTHON_API for the database name:

      Creating a Fauna database

      Leave Pre-populate with demo data unchecked. Press the Save button.

      After creating the database, you will see the home section for your database:

      Fauna database home

      You’re now going to create two collections:

      • The Users collection that will store documents with authentication information.
      • The Things collection to store some mock data to test your API.

      To create these collections, you’ll execute some FQL queries in the dashboard’s shell. Access the shell from the main dashboard menu on the left:

      Fauna dashboard shell

      Write the following FQL query in the bottom panel of the shell to create a collection called Things by using the CreateCollection function:

      CreateCollection({name: "Things"})
      

      Press the RUN QUERY button. You will get a result similar to this in the shell’s top panel:

      {
        ref: Collection("Things"),
        ts: 1614805457170000,
        history_days: 30,
        name: "Things"
      }
      

      The result shows four fields:

      • ref is a reference to the collection itself.
      • ts is the timestamp of its creation in microseconds.
      • history_days is how long Fauna will retain changes on documents’ changes.
      • name is the collection name.

      Next, create the Users collection with the following query:

      CreateCollection({name: "Users"})
      

      Now that both collections are in place, you will create your first document.

      Documents in Fauna are somewhat similar to JSON objects. Documents can store strings, numbers, and arrays, but they can also use Fauna data types. A common Fauna type is Ref, which represents a reference to a document in a collection.

      The Create function creates a new document into the specified collection. Run the following query to create a document in the Things collection with two fields:

      Create(
        Collection("Things"),
        {
          data: {
            name: "Banana",
            color: "Yellow"
          }
        }
      )
      

      After running that query, Fauna will return the created document:

      {
        ref: Ref(Collection("Things"), "292079274901373446"),
        ts: 1614807352895000,
        data: {
          name: "Banana",
          color: "Yellow"
        }
      }
      

      The result shows the following fields:

      • ref of type Ref is a reference to this document in the Things collection with the ID 292079274901373446. Do note that your document will have a different ID.
      • ts is the timestamp of its creation in microseconds.
      • data is the actual content of the document.

      This result looks similar to the result you got when you created a collection. That’s because all entities in Fauna (collections, indexes, roles, etc) are actually stored as documents.

      To read documents, use the Get function which accepts a reference of a document. Run the Get query using the reference for your document:

      Get(Ref(Collection("Things"), "292079274901373446"))
      

      The result is the full document:

      {
        ref: Ref(Collection("Things"), "292079274901373446"),
        ts: 1614807352895000,
        data: {
          name: "Banana",
          color: "Yellow"
        }
      }
      

      To get all references for documents stored in a collection, use the Documents function with the Paginate function:

      Paginate(Documents(Collection("Things")))
      

      This query returns a page with an array of references:

      {
        data: [Ref(Collection("Things"), "292079274901373446")]
      }
      

      To get actual documents instead of references, iterate over the references using Map. Then use a Lambda (an anonymous function) to iterate over the array of references and Get each reference:

      Map(
        Paginate(Documents(Collection("Things"))),
        Lambda("ref", Get(Var("ref")))
      )
      

      The result is an array containing full documents:

      {
        data: [
          {
            ref: Ref(Collection("Things"), "292079274901373446"),
            ts: 1614807352895000,
            data: {
              name: "Banana",
              color: "Yellow"
            }
          }
        ]
      }
      

      You’re now going to create the Users_by_username index. You typically use indexes in Fauna to catalog, filter, and sort data, but you can also use them for other purposes like enforcing unique constraints.

      The Users_by_username index will find users by their username, and also enforce a unique constraint to prevent two documents from having the same username.

      Execute this code in the shell to create the index:

      CreateIndex({
        name: "Users_by_username",
        source: Collection("Users"),
        terms: [{ field: ["data", "username"] }],
        unique: true
      })
      

      The CreateIndex function will create an index with the configured settings:

      • name is the name of the index.
      • source is the collection (or collections) the index will index data from.
      • terms is the search/filter terms you’ll pass to this index when using it to find documents.
      • unique means that the indexed values will be unique. In this example, the username property of the documents in the Users collection will be enforced as unique.

      To test the index, create a new document inside the Users collection by running the following code in the Fauna shell:

      Create(
        Collection("Users"),
        {
          data: {
            username: "sammy"
          }
        }
      )
      

      You’ll see a result like the following:

      {
        ref: Ref(Collection("Users"), "292085174927098368"),
        ts: 1614812979580000,
        data: {
          username: "sammy"
        }
      }
      

      Now try to create a document with the same username value:

      Create(
        Collection("Users"),
        {
          data: {
            username: "sammy"
          }
        }
      )
      

      You’ll receive an error now:

      Error: [
        {
          "position": [
            "create"
          ],
          "code": "instance not unique",
          "description": "document is not unique."
        }
      ]
      

      Now that the index is in place, you can query it and fetch a single document. Run this code in the shell to fetch the sammy user using the index:

      Get(
        Match(
          Index("Users_by_username"),
          "sammy"
        )
      )
      

      Here’s how it works:

      • Index returns a reference to the Users_by_username index.
      • Match returns a reference to the matched document (the one that has a username with the value of sammy).
      • Get takes the reference returned by Match, and fetches the actual document.

      The result of this query will be:

      {
        ref: Ref(Collection("Users"), "292085174927098368"),
        ts: 1614812979580000,
        data: {
          username: "sammy"
        }
      }
      

      Delete this testing document by passing its reference to the Delete function:

      Delete(Ref(Collection("Users"), "292085174927098368"))
      

      Next you’ll configure security settings for Fauna so you can connect to it from your code.

      Step 2 — Configuring a Server Key and Authorization Rules

      In this step you’ll create a server key that your Python application will use to communicate with Fauna. Then you’ll configure access permissions.

      To create a key, go to the Security section of the Fauna dashboard by using the main menu on the left. Once there:

      1. Press the New Key button.
      2. Select the Server role.
      3. Press Save.

      Creating a Fauna key

      After saving, the dashboard will show you the key’s secret. Save the secret somewhere safe and never commit it to your Git repository.

      Warning: The Server role is omnipotent and anyone with this secret would have full access to your database. As its name implies, this is the role typically used by trusted server applications, although it is also possible to create a key with a custom role with limited privileges. When you create production applications, you’ll want to make a more restrictive role.

      By default, everything in Fauna is private, so you’re now going to create a new role to allow the logged-in users to read documents from the Things collection.

      In the Security section of the dashboard, go to Roles, and create a new custom role with the name User.

      In the Collections dropdown, add the Things collection and press the Read permission so that it shows a green check mark:

      Configuring the permissions of a Fauna role

      Before saving the role, go to the Membership tab and add the Users collection to the role:

      Configuring the memerbship of a Fauna role

      You can now save your User custom role by pressingthe Save button.

      Now any logged-in user from a document in the Users collection will be able to read any document from the Things collection.

      With authentication and authorization in place, let’s now create the Python API that will talk to Fauna.

      Step 3 — Building the Python Application

      In this step you will build a small REST API using the Flask framework, and you’ll write FQL queries in Python, connecting to your Fauna database using the the Fauna driver.

      To get started, create a project folder and access it from your terminal.

      First install Flask with:

      Then install the Fauna Python driver with:

      In your project folder, create the file main.py and add the following code to the file, which adds the necessary imports, the FAUNA_SECRET environment variable, and the basic configuration of the Flask application:

      main.py

      import os
      FAUNA_SECRET = os.environ.get('FAUNA_SECRET')
      
      import flask
      from flask import request
      
      import faunadb
      from faunadb import query as q
      from faunadb.client import FaunaClient
      
      app = flask.Flask(__name__)
      app.config["DEBUG"] = True
      

      The FAUNA_SECRET environment variable will carry the server secret you created earlier. To be able to run this application, locally or in the cloud, this variable needs to be injected. Without it, the application won’t be able to connect to Fauna. You’ll provide this environment variable when you launch the app.

      Now add the the /signup route to the main.py file. This will create new documents in the Users collection:

      main.py

      @app.route('/signup', methods=['POST'])
      def signup():
      
          body = request.json
          client = FaunaClient(secret=FAUNA_SECRET)
      
          try:
              result = client.query(
                  q.create(
                      q.collection("Users"),
                      {
                          "data": {
                              "username": body["username"]
                          },
                          "credentials": {
                              "password": body["password"]
                          }
                      }
                  )
              )
      
              return {
                  "userId": result['ref'].id()
              }
      
          except faunadb.errors.BadRequest as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 409
      

      Note that the Fauna client is being instantiated on every request using the server secret:

      main.py

      ...
      client = FaunaClient(secret=FAUNA_SECRET)
      ...
      

      Once users are logged in, the API will execute queries on behalf of each user using different secrets, which is why it makes sense to instantiate the client on every request.

      Unlike other databases, the Fauna client does not maintain a persistent connection. From the outside world, Fauna behaves like an API; every query is a single HTTP request.

      After the client is ready, the FQL query executes, which creates a new document in the Users collection. Each Fauna driver translates idiomatic syntax to FQL statements. In this route, you added this query:

      main.py

      ...
      q.create(
          q.collection("Users"),
          {
              "data": {
                  "user": json["user"]
              },
              "credentials": {
                  "password": json["password"]
              }
          }
      )
      ...
      

      This is what this query would look like in native FQL:

      Create(
          Collection("Users"),
          {
              "data": {
                  "user": "sammy"
              },
              "credentials": {
                  "password": "secretpassword"
              }
          }
      )
      

      In addition to the document data, you’re adding a credentials configuration with the user’s password. This part of the document is completely private. You will never be able to read a document’s credentials afterwards. When using Fauna’s authentication system, it’s not possible to expose users’ passwords by mistake.

      Finally, if there’s already a user with the same username, a faunadb.errors.BadRequest exception will be raised, and a 409 response with the error information will be returned to the client.

      Next, add the /login route in the main.py file to authenticate the user and password. This follows a similar pattern as the previous example; you execute a query using the Fauna connection and if the authentication fails, you raise a faunadb.errors.BadRequest exception and return a a 401 response with the error information. Add this code to main.py:

      main.py

      @app.route('/login', methods=['POST'])
      def login():
      
          body = request.json
          client = FaunaClient(secret=FAUNA_SECRET)
      
          try:
              result = client.query(
                  q.login(
                      q.match(
                          q.index("Users_by_username"),
                          body["username"]
                      ),
                      {"password": body["password"]}
                  )
              )
      
              return {
                  "secret": result['secret']
              }
      
          except faunadb.errors.BadRequest as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 401
      

      This is the FQL query used to authenticate users with Fauna:

      main.py

      q.login(
          q.match(
              q.index("Users_by_username"),
              body["username"]
          ),
          {"password": body["password"]}
      )
      

      This is what this query would look like in native FQL:

      Login(
          Match(
              Index("Users_by_username"),
              "sammy"
          ),
          {"password": "secretpassword"}
      )
      

      Match returns a reference to a document using the Users_by_username index that we created previously.

      If the provided password matches the referenced document, Login will create a new token and return a dictionary with the following keys:

      • ref with a reference to the token for the new document.
      • ts with the timestamp of the transaction.
      • instance with a reference to the document that was used to do the authentication.
      • secret with the token’s secret that will be used to make further queries to Fauna.

      If you run that FQL query into your Fauna dashboard’s shell you will see something similar to this:

      {
        ref: Ref(Ref("tokens"), "292001047221633538"),
        ts: 1614732749110000,
        instance: Ref(Collection("Users"), "291901454585692675"),
        secret: "fnEEDWVnxbACAgQNBIxMIAIIKq1E5xvPPdGwQ_zUFH4F5Dl0neg"
      }
      

      Depending on the security requirements of the project, you have to decide how to handle the token’s secret. If this API was meant to be consumed by browsers, you might return the secret inside a secure cookie or an encrypted JSON Web Token (JWT). Or you might store it as session data somewhere else, like a Redis instance. For the purpose of this demo, you return it in the body of the HTTP response:

      Finally, add this bit of code to main.py, which will start the Flask application:

      main.py

      app.run(host=os.getenv('IP', '0.0.0.0'), port=int(os.getenv('PORT', 8080)))
      

      It’s important to specify the 0.0.0.0 address. Once deployed to the cloud, this application will run in a Docker container. It won’t be able to receive requests from remote clients if it is running on 127.0.0.1, which is the default address for Flask applications.

      This is the complete main.py file so far:

      main.py

      import os
      FAUNA_SECRET = os.environ.get('FAUNA_SECRET')
      
      import flask
      from flask import request
      
      import faunadb
      from faunadb import query as q
      from faunadb.client import FaunaClient
      
      app = flask.Flask(__name__)
      app.config["DEBUG"] = True
      
      @app.route('/signup', methods=['POST'])
      def signup():
      
          body = request.json
          client = FaunaClient(secret=FAUNA_SECRET)
      
          try:
              result = client.query(
                  q.create(
                      q.collection("Users"),
                      {
                          "data": {
                              "username": body["username"]
                          },
                          "credentials": {
                              "password": body["password"]
                          }
                      }
                  )
              )
      
              return {
                  "userId": result['ref'].id()
              }
      
          except faunadb.errors.BadRequest as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 409
      
      @app.route('/login', methods=['POST'])
      def login():
      
          body = request.json
          client = FaunaClient(secret=FAUNA_SECRET)
      
          try:
              result = client.query(
                  q.login(
                      q.match(
                          q.index("Users_by_username"),
                          body["username"]
                      ),
                      {"password": body["password"]}
                  )
              )
      
              return {
                  "secret": result['secret']
              }
      
          except faunadb.errors.BadRequest as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 401
      
      app.run(host=os.getenv('IP', '0.0.0.0'), port=int(os.getenv('PORT', 8080)))
      

      Save the file.

      To launch this server locally from your terminal, use the following command with the FAUNA_SECRET environment variable with the secret you obtained when creating the server key:

      • FAUNA_SECRET=your_fauna_server_secret python main.py

      After triggering that command, Flask will show a warning informing you it is running with a development WSGI server. This is fine for the purpose of this demo so you can safely ignore this warning.

      Test your API by making HTTP requests using the curl command. Open a new terminal window and run the following command:

      Create a user with the following command:

      • curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/signup

      You’ll see the following response, indicating a successful user creation:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 37
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:00:47 GMT
      
      {
        "userId": "292092166117786112"
      }
      

      Now authenticate that user with this command:

      • curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/login

      You’ll get this successful response:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 70
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:01:19 GMT
      
      {
        "secret": "fnEEDbhO3jACAAQNBIxMIAIIOlDxujk-VJShnnhkZkCUPKIHxbc"
      }
      

      Close the terminal window where you ran your curl commands and switch back to the terminal where your Python server is running. Stop your server by pressing CTRL+C.

      Now that the application is working, we’re going to add a private endpoint that requires users to be authenticated.

      Step 4 — Adding a Private Endpoint

      In this step, you’ll add a private endpoint to the API, which will require the user to be authenticated first.

      First, create a new route in the main.py file. This route will respond to the /things endpoint. Place it above the line that starts the server with the app.run() method:

      main.py

      @app.route('/things', methods=['GET'])
      def things():
      

      Next, in the /things route, instantiate the Fauna client:

      main.py

          userSecret = request.headers.get('fauna-user-secret')
          client = FaunaClient(secret=userSecret)
      

      Instead of using the server secret, this route is using the user’s secret from the fauna-user-secret HTTP header which is used to instantiate the Fauna client. By using the users’ secrets instead of the server secret, FQL queries will now be subject to the authorization rules we’ve configured previously in the dashboard.

      Then add this try block to the route to execute the query:

      main.py

          try:
              result = client.query(
                  q.map_(
                      q.lambda_("ref", q.get(q.var("ref"))),
                      q.paginate(q.documents(q.collection("Things")))
                  )
              )
      
              things = map(
                  lambda doc: {
                      "id": doc["ref"].id(),
                      "name": doc["data"]["name"],
                      "color": doc["data"]["color"]
                  },
                  result["data"]
              )
      
              return {
                  "things": list(things)
              }
      

      This executes an FQL query and parses the Fauna response into a serializable type that is then returned as a JSON string in the body of the HTTP response.

      Finally, add this except block to the route:

      main.py

          except faunadb.errors.Unauthorized as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 401
      

      If the request doesn’t contain a valid secret, a faunadb.errors.Unauthorized exception will be raised and a 401 response with the error information will be returned.

      This is the full code for the /things route:

      main.py

      @app.route('/things', methods=['GET'])
      def things():
      
          userSecret = request.headers.get('fauna-user-secret')
          client = FaunaClient(secret=userSecret)
      
          try:
              result = client.query(
                  q.map_(
                      q.lambda_("ref", q.get(q.var("ref"))),
                      q.paginate(q.documents(q.collection("Things")))
                  )
              )
      
              things = map(
                  lambda doc: {
                      "id": doc["ref"].id(),
                      "name": doc["data"]["name"],
                      "color": doc["data"]["color"]
                  },
                  result["data"]
              )
      
              return {
                  "things": list(things)
              }
      
          except faunadb.errors.Unauthorized as exception:
              error = exception.errors[0]
              return {
                  "code": error.code,
                  "description": error.description
              }, 401
      

      Save the file and run your server again:

      • FAUNA_SECRET=your_fauna_server_secret python main.py

      To test this endpoint, first obtain a secret by authenticating with valid credentials. Open a new terminal window and execute this curl command:

      • curl -i -d '{"username":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST http://0.0.0.0:8080/login

      This command returns a succesful response, although the value for secret will be different:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 70
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:01:19 GMT
      
      {
        "secret": "fnEEDb...."
      }
      

      Now hen do a GET request to /things using the secret:

      curl -i -H 'fauna-user-secret: fnEEDb...' -X GET http://0.0.0.0:8080/things
      

      You’ll get another successful response:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 118
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:14:49 GMT
      
      {
        "things": [
          {
            "color": "Yellow",
            "id": "292079274901373446",
            "name": "Banana"
          }
        ]
      }
      

      Close your terminal window where you ran the curl commands. Return to your window where your server is running and stop the server with CTRL+C.

      Now that you have a working app, you’re ready to deploy it.

      Step 4 — Deploying to DigitalOcean

      In the final step of this tutorial, you will create an app on App Platform and deploy it from a GitHub repository.

      Before pushing the project to a Git repository, be sure to run the following command in the project’s folder:

      • pip freeze > requirements.txt

      This will create a requirements.txt file with the list of dependencies that need to be installed once the application is deployed.

      Now initialize your project directory as a Git repository:

      Now execute the following command to add files to your repository:

      This adds all the files in the current directory.

      With the files added, make your initial commit:

      • git commit -m "Initial version of the site"

      Your files will commit.

      Open your browser and navigate to GitHub, log in with your profile, and create a new repository called sharkopedia. Create an empty repository without a README or license file.

      Once you’ve created the repository, return to the command line to push your local files to GitHub.

      First, add GitHub as a remote repository:

      • git remote add origin https://github.com/your_username/sharkopedia

      Next, rename the default branch main, to match what GitHub expects:

      Finally, push your main branch to GitHub’s main branch:

      Your files will transfer. You’re now ready to deploy your app.

      Note: To be able to create an app on App Platform, you’ll first need to add a payment method to your DigitalOcean account.

      The application will run on a container which costs $5 per month, although only a few cents will be needed to test it out. Don’t forget to delete the application once you’re done or you’ll continue to be charged.

      Go to the Apps section of the DigitalOcean dashboard, and click on Launch Your App:

      Select the source for deployment. You will need to authorize DigitalOcean to read your Github repositories. Once you’ve authorized access, select the repository with your Python project and the branch that contains the version of the app you want to deploy:

      Selecting a repository and branch

      At this point, App Platform will determine that your project uses Python and will let you configure some application options:

      Configuring the app options

      Set the following options

      • Ensure the Type is Web Service.
      • Create aFAUNA_SECRET environment variable with your server secret.
      • Set the Run Command to python main.py.
      • Set the HTTP Port to 8080.

      Next, enter a name for your app and select a deploy region:

      Configuring the name of the app and deploy region

      Next, choose the Basic plan and Basic Size that costs $5 per month:

      Selecting the app plan

      After that, scroll down and click on Launch Your App.

      Once you’ve finished configuring the app, a container will be created and deployed with your application. This first-time initialization will take a couple of minutes, but subsequent deploys will be much faster.

      In the app’s dashboard you’ll see a green check mark to indicate the deploy process has finished successfully:

      App is running

      You will now be able to execute HTTP requests to the provided app domain. Execute the following command in your terminal, substituting your_app_name with your actual app name, to return a new secret for the sammy user:

      • curl -i -d '{"user":"sammy", "password": "secretpassword"}' -H 'Content-Type: application/json' -X POST https://your_app_name.ondigitalocean.app/login

      You’ll receive a response similar to the following:

      HTTP/1.0 200 OK
      Content-Type: application/json
      Content-Length: 70
      Server: Werkzeug/1.0.1 Python/3.9.2
      Date: Thu, 04 Mar 2021 01:01:19 GMT
      
      {
        "secret": "fnAADbhO3jACEEQNBIxMIAOOIlDxujk-VJShnnhkZkCUPKIskdjfh"
      }
      

      Your application is now up and running on Digital Ocean.

      Conclusion

      In this tutorial you created a Python REST API using Fauna as the data layer, and you deployed it to DigitalOcean App Platform.

      To keep learning about Fauna and dive deeper into FQL, check out the Fauna Documentation.



      Source link

      How To Deploy a Pre-Trained Question and Answer TensorFlow.js Model on App Platform


      The author selected Code 2040 to receive a donation as part of the Write for DOnations program.

      Introduction

      As the field of machine learning (ML) grows, so does the list of environments for using this technology. One of these environments is the web browser, and in recent years, there has been a surge in data-related frameworks targeting web-based machine learning models. An example of these frameworks is TensorFlow.js, TensorFlow’s JavaScript counterpart library for training, executing, and deploying machine learning models in the browser. In an attempt to make TensorFlow.js accessible to developers with limited or no ML experience, the library comes with several pre-trained models that work out of the box.

      A pre-trained machine learning model is ready-to-use machine learning you don’t have to train. TensorFlow.js includes 14 that suit a variety of use cases. For example, there is an image classifying model for identifying common objects and a body segmentation model for identifying body parts. The principal convenience of these models is that, as the name states, you don’t have to train them. Instead, you load them in your application. Besides their ease of use and pre-trained nature, these are curated models—they are accurate and fast, sometimes trained by the algorithms’ authors and optimized for the web browser.

      In this tutorial, you will create a web application that serves a Question and Answer (QnA) pre-trained model using TensorFlow.js. The model you will deploy is a Bidirectional Encoder Representations from Transformers (BERT) model that uses a passage and a question as the input, and tries to answer the question from the passage.

      You will deploy the app in DigitalOcean’s App Platform, a managed solution for building, deploying, and scaling applications within a few clicks from sources such as GitHub. The app you will create consists of a static page with two input fields, one for the passage and one for the question. In the end, you will have a written and deployed—from GitHub—a Question and Answer application using one of TensorFlow.js’ pre-trained models and DigitalOcean’s App Platform.

      Prerequisites

      To complete this tutorial, you will need:

      Step 1 — Creating the App’s Interface and Importing the Required Libraries

      In this step, you will write the app’s HTML code, which will define its interface and import the libraries for the app. The first of these libraries is TensorFlow.js, and instead of installing the package locally, you will load it from a content delivery network or CDN. A CDN is a network of servers spanning multiple locations that store content they provide to the internet. This content includes JavaScript files, such as the TensorFlow.js library, and loading it from a CDN saves you from packing them in your application. Similarly, you will import the library containing the Question and Answer model. In the next step, you will write the app’s JavaScript, which uses the model to answer a given question.

      In this tutorial, you will structure an app that will look like this:

      The app

      The app has five major elements:

      • A button that loads a pre-defined passage you can use to test the app.
      • An input text field for a passage (if you choose to write or copy your own).
      • An input text field for the question.
      • A button that triggers the prediction that answers the question.
      • An area to display the model’s output beneath the Answer! button (currently blank white space).

      Start by creating a new directory named tfjs-qna-do at your preferred location. In this directory, using a text editor of your choice, create a new HTML file named index.html and paste in the following code:

      index.html

      <!DOCTYPE html>
      <html lang="en-US">
      
      <head>
          <meta charset="utf-8" />
          <!-- Load TensorFlow.js -->
          <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
          <!-- Load the QnA model -->
          <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/qna"></script>
          <link href="./style.css" rel="stylesheet">
      </head>
      
      <body>
          <div class="main-centered-container">
              <h1>TensorFlow.js Pre-trained "QnA" BERT model</h1>
              <h3 class="header-border">Introduction</h3>
              <p>This application hosts TensorFlow.js' pre-trained Question and Answer model, and it attempts to answer the question
              using a given passage. To use it, write a passage (any!) in the input area below and a question. Then, click the
              "answer!" button to answer the question using the given passage as a reference. You could also click the test
              button to load a pre-defined input text.</p>
      
              <h4>Try the test passage!</h4>
              <div id='test-buttons'></div>
      
              <div>
                  <h4>Enter the model's input passage here</h4>
                  <textarea id='input-text' rows="20" cols="100" placeholder="Write the input text..."></textarea>
              </div>
      
              <div>
                  <h4>Enter the question to ask</h4>
                  <textarea id='question' rows="5" cols="100" placeholder="Write the input text..."></textarea>
              </div>
              <h4>Click to answer the question</h4>
              <div id="answer-button"></div>
              <h4>The model's answers</h4>
              <div id='answer'></div>
      
              <script src="./index.js"></script>
      
      
          </div>
      </body>
      
      </html>
      

      Here’s how that HTML breaks down:

      • The initial <html> tag has the <head> tag that’s used for defining metadata, styles, and loading scripts. Its first element is <meta>, and here you will set the page’s charset encoding to utf-8. After it, there are two <script> tags for loading both TensorFlow.js and the Question and Answer model from a CDN.
      • Following the two <script> tags, there’s a <link> tag that loads a CSS file (which you will create next).
      • Next, there’s the HTML’s <body>—the document’s content. Inside of it, there’s a <div> tag of class main-centered-container containing the page’s elements. The first is a <h1> header with the application title and a smaller <h3> header, followed by a brief introduction explaining how it works.
      • Under the introduction, there’s a <h4> header and a <div> where you will append buttons that populate the passage input text field with sample text.
      • Then, there are the app’s input fields: one for the passage (what you want the model to read) and one for the question (what you want the model to answer). If you wish to resize them, change the rows and cols attributes.
      • After the text fields, there’s a <div> with id button where you will later append a button that, upon clicking, reads the text fields’ text and uses them as an input to the model.
      • Last, there is a <div> with id answer that’s used to display the model’s output and a <script> tag to include the JavaScript code you will write in the following section.

      Next, you’ll add CSS to the project. In the same directory where you added the index.html file, create a new file named style.css and add the following code:

      style.css

      body {
        margin: 50px 0;
        padding: 0;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial;
      }
      
      button {
        margin: 10px 10px;
        font-size: 100%;
      }
      
      p {
        line-height: 1.8;
      }
      
      .main-centered-container {
        padding: 60px;
        display: flex;
        flex-direction: column;
        margin: 0 auto;
        max-width: 960px;
      }
      
      .header-border {
        border: solid #d0d0d0;
        border-width: 0 0 1px;
        padding: 0 0 5px;
      }
      

      This CSS adds style to three HTML elements: body, buttons, and paragraphs (<p>). To body, it adds a margin, padding, and changes the default font. To button, it adds margin and increases the font size. To paragraph p, it modifies the line-height attribute. The CSS has a class main-centered-container that centers the content and another one, header-border, that adds a solid line to an element.

      In this step, you wrote the app’s HTML file. You imported the TensorFlow.js library and QnA model, and defined the elements you will use later to add the passage and the question you want to answer. In the following step, you will write the JavaScript code that reads the elements and triggers the predictions.

      Step 2 — Predicting with the Pre-Trained Model

      In this section, you will implement the web app’s JavaScript code that reads the app’s input fields, makes a prediction, and writes the predicted answers in HTML. You will do it in one function that’s triggered when you click the “answer” button. Once clicked, it will reference both input fields to get their values, use them as inputs to the model, and write its output in the <div> with id output you defined in Step 1. Then, you will run the app locally to test it before adding it to GitHub.

      In the project’s directory tfjs-qna-do/, create a new file named index.js, and declare the following variables:

      index.js

      let model;
      
      // The text field containing the input text
      let inputText;
      
      // The text field containing the question
      let questionText;
      
      // The div where we will write the model's answer
      let answersOutput;
      

      The first of the variables, model, is where you will store the QnA model; inputText and questionText are references to the input and question text fields; answersOutput is a reference to the output <div>.

      In addition to these variables, you will need a constant for storing the sample text you will use to test the app. We will use the Wikipedia article on DigitalOcean as a sample passage. Based on this passage, you could ask the model questions like “Where is DigitalOcean headquartered?” and hopefully, it will output “New York”.

      Copy this block into your index.js file:

      index.js

      // Sample passage from Wikipedia.
      const doText = `DigitalOcean, Inc. is an American cloud infrastructure provider[2] headquartered in New York City with data centers worldwide.[3] 
      DigitalOcean provides developers cloud services that help to deploy and scale applications that run simultaneously on multiple computers.
      DigitalOcean also runs Hacktoberfest which is a month-long celebration (October 1-31) of open source software run in partnership with GitHub and Twilio.
      `;
      

      Now, you will define the app’s functions, starting with createButton(). This function creates a button and appends it to an HTML element:

      index.js

      function createButton(innerText, id, listener, selector, disabled = false) {
        const btn = document.createElement('BUTTON');
        btn.innerText = innerText;
        btn.id = id;
        btn.disabled = disabled;
      
        btn.addEventListener('click', listener);
        document.querySelector(selector).appendChild(btn);
      }
      

      createButton() is a function that creates the app’s two buttons; since all the buttons work similarly, this function avoids repeating code. The function has five parameters:

      • innerText: the button’s text.
      • id: the button’s id.
      • listener: a callback function that’s executed when the user clicks the button.
      • selector: the <div> element where you will append the button.
      • disabled: a boolean value to disable or enable the button. This parameter’s default value is false.

      createButton() starts by creating an instance of a button and assigns it to the variable btn. Then, it sets the button’s innerText, id, and disabled attributes. The button uses a click event listener that executes a callback function whenever you click it. The function’s last line appends the button to the <div> element specified in the selector parameter.

      Next, you will create a new function named setupButtons() that calls createButton() two times to create the app’s buttons. Begin by creating the app’s Answer! button:

      index.js

      function setupButtons() {
        // Button to predict
        createButton('Answer!', 'answer-btn',
          () => {
            model.findAnswers(questionText.value, inputText.value).then((answers) => {
              // Write the answers to the output div as an unordered list.
              // It uses map create a new list of the answers while adding the list tags.
              // Then, we use join to concatenate the answers as an array with a line break
              // between answers.
              const answersList = answers.map((answer) => `<li>${answer.text} (confidence: ${answer.score})</li>`)
                .join('<br>');
      
              answersOutput.innerHTML = `<ul>${answersList}</ul>`;
            }).catch((e) => console.log(e));
          }, '#answer-button', true);
      }
      

      The first button the function creates is the one that triggers the predictions. Its first two arguments, innerText and id, are the button’s text and the identifier you want to assign to the button. The third argument is the listener’s callback (explained below). The fourth argument, selector, is the id of the <div> where you want to add the button (#answer-button), and the fifth argument, disabled, is set to true, which disables the button (to avoid predicting before the app loads the model).

      The listener’s callback is a function that’s executed once you click the Answer! button. Once clicked, the callback function first calls the pre-trained model’s function findAnswers(). (For more about the findAnswers() function, see the product documentation). findAnswers() uses as arguments the input passage (read from questionText) and the question (read from inputText). It returns the model’s output in an array that looks as follows:

      Model's output

      [ { "text": "New York City", "score": 19.08431625366211, "startIndex": 84, "endIndex": 97 }, { "text": "in New York City", "score": 8.737937569618225, "startIndex": 81, "endIndex": 97 }, { "text": "New York", "score": 7.998648166656494, "startIndex": 84, "endIndex": 92 }, { "text": "York City", "score": 7.5290607213974, "startIndex": 88, "endIndex": 97 }, { "text": "headquartered in New York City", "score": 6.888534069061279, "startIndex": 67, "endIndex": 97 } ]

      Each of the array’s elements is an object of four attributes:

      • text: the answer.
      • score: the model confidence level.
      • startIndex: the index of the passage’s first character that answers the question.
      • endIndex: the index of the answer’s last characters.

      Instead of displaying the output as the model returns it, the callback uses a map() function that creates a new array containing the model’s answers and scores while adding the list <li> HTML tags. Then, it joins the array’s elements with a <br> (line break) between answers and assigns the result to the answer <div> to display them as a list.

      Next, add a second call to createButton(). Add the highlighted portion to the setupButtons function beneath the first createButton:

      index.js

      function setupButtons() {
        // Button to predict
        createButton('Answer!', 'answer-btn',
          () => {
            model.findAnswers(questionText.value, inputText.value).then((answers) => {
              // Write the answers to the output div as an unordered list.
              // It uses map create a new list of the answers while adding the list tags.
              // Then, we use join to concatenate the answers as an array with a line break
              // between answers.
              const answersList = answers.map((answer) => `<li>${answer.text} (confidence: ${answer.score})</li>`)
                .join('<br>');
      
              answersOutput.innerHTML = `<ul>${answersList}</ul>`;
            }).catch((e) => console.log(e));
          }, '#answer-button', true);
      
        createButton('DigitalOcean', 'test-case-do-btn',
          () => {
           document.getElementById('input-text').value = doText;
          }, '#test-buttons', false);
      }
      

      This new call appends to the test-buttons <div> the button that loads the DigitalOcean sample text you defined earlier in the variable doText. The function’s first argument is the label for the button (DigitalOcean), the second argument is the id, the third is the listener’s callback that writes in the passage input text area the value of the doText variable, the fourth is the selector, and the last is a false value (to avoid disabling the button).

      Next, you will create a function, named init(), that calls the other functions:

      index.js

      async function init() {
        setupButtons();
        answersOutput = document.getElementById('answer');
        inputText = document.getElementById('input-text');
        questionText = document.getElementById('question');
      
        model = await qna.load();
        document.getElementById('answer-btn').disabled = false;
      }
      

      init() starts by calling setupButtons(). Then, it assigns some of the app’s HTML elements to the variables you defined at the top of the script. Next, it loads the QnA model and changes to false the disabled attribute of the answer (answer-btn) button.

      Last, call init():

      index.js

      init();
      

      You have finished the app. To test it, open your web browser and write the project’s directory absolute path with /index.html appended to it in the address bar. You could also open your file manager application (such as Finder on Mac) and click on “index.html” to open the web application. In this case, the address would look like your_filepath/tfjs-qna-do/index.html.

      To find the absolute path, go to the terminal and execute the following command from the project’s directory:

      Its output will look like this:

      Output

      /Users/your_filepath/tfjs-qna-do

      After launching the app, you will need to wait a few seconds while it downloads and loads the model. You will know it’s ready when the app enables the Answer! button. Then, you could either use the test passage button (DigitalOcean) or write your own passage, and then input a question.

      To test the app, click the “DigitalOcean” button. In the passage input area, you will see the sample text about DigitalOcean. In the question input area, write “Where is DigitalOcean headquartered?” From the passage, we can see the answer is “New York”. But will the model say the same? Click Answer! to find out.

      The output should look similar to this:

      Model's answers to "where is DO headquartered?"

      Correct! Although there are slight differences, four of the five answers say that DigitalOcean’s headquarters are in New York City.

      In this step, you completed and tested the web application. The JavaScript code you wrote reads the input passage and question and uses it as an input to the QnA model to answer the question. Next, you will add the code to GitHub.

      Step 3 — Pushing the App to GitHub

      In this section, you will add the web application to a GitHub repository. Later, you will connect the repository to DigitalOcean’s App Platform and deploy the app.

      Start by logging in to GitHub. From its main page, click the green button under your name or the plus sign on the screen’s upper right corner to create a new repository.

      Click to create a repository

      Click to create a repository

      Either choice takes you to the Create a new repository screen. In the Repository name field (after your username), name the repository tfjs-qna-do. Select its privacy setting, and click Create repository to create it.

      Create the repository

      Open a terminal and go to the project’s directory tfjs-qna-do/. There, execute the following command to create a new local Git repository:

      Next, stage the files you want to track with Git, which in this case, is everything in the directory:

      And commit them:

      • git commit -m "initial version of the app"

      Rename the repository’s principal branch to main:

      Then link your local repository with the remote one on GitHub:

      • git remote add origin git@github.com:your-github-username/tfjs-qna-do.git

      Last, push the local codebase to the remote repository:

      It will ask you to enter your GitHub credentials if this is the first time you are pushing code to it.

      Once you have pushed the code, return to the GitHub repository and refresh the page. You should see your code.

      The code in the repo

      In this step, you have pushed your web app’s code to a remote GitHub repository. Next, you will link this repository to your DigitalOcean account and deploy the app.

      Step 4 — Deploying the Web Application in DigitalOcean App Platform

      In this last section, you will deploy the Question and Answer app to DigitalOcean’s App Platform) from the GitHub repository created in Step 3.

      Start by logging into your DigitalOcean account. Once logged in, click the Create green button in the upper right corner and then on Apps; this takes you to the Create new app screen:

      Create a new DO App

      Here, choose GitHub as the source where the project resides.

      Choose the source

      If you haven’t linked your DigitalOcean account with GitHub, it will ask you to authorize DigitalOcean to access your GitHub. After doing so, select the repository you want to link: tfjs-qna-do.

      Link the repo

      Back on the App Platform window, select the app’s repository (tfjs-qna-do), the branch from where it will deploy the app (main), and check the Autodeploy code changes box; this option ensures that the application gets re-deployed every time you push code to the main branch.

      Choose the source (cont.)

      In the next screen, Configure your app, use the default configuration values. As an additional step, if you wish to change the web app’s HTTP requests route, from, for example, the default https://example.ondigitalocean.app/tfjs-qna-do, to https://example.ondigitalocean.app/my-custom-route, click on Edit under HTTP Routes and write my-custom-route in the ROUTES input.

      Configure the app

      Next, you will name the site. Again, you could leave the default tfjs-qna-do name or replace it with another one.

      Name the site

      Clicking Next will take you to the last screen, Finalize and launch, where you will select the app’s pricing tier. Since the app is a static webpage, App Platform will automatically select the free Starter plan. Last, click the Launch Starter App button to build and deploy the application.

      Finalize and launch

      While the app is deployed, you will see a screen similar to this:

      Deploying app

      And once deployed, you will see:

      Deployed

      To access the app, click on the app’s URL to access your app, which is now deployed in the cloud.

      Deployed app

      Conclusion

      As the field of machine learning expands, so do its use cases alongside the environments and platforms it reaches – including the web browser.

      In this tutorial, you have built and deployed a web application that uses a TensorFlow.js pre-trained model. Your Question and Answer web app takes as input a passage along with a question and uses a pre-trained BERT model to answer the question according to the passage. After developing the app, you linked your GitHub account with DigitalOcean and deployed the application in App Platform without needing additional code.

      As future work, you could trigger the app’s automatic deployment by adding a change to it and pushing it to GitHub. You could also add a new test passage and format the answer’s output as a table to improve its readability.

      For more information about TensorFlow.js, refer to its official documentation.



      Source link