├── .github ├── generate-toc └── workflows │ ├── e2e-test-deploy.yaml │ ├── help-action.yml │ └── slash-command-dispatcher.yaml ├── .gitignore ├── LICENSE ├── README.md ├── bentoctl_lambda ├── __init__.py ├── aws_lambda │ ├── README.md │ ├── app.py │ ├── aws-lambda-rie-x86 │ ├── bentoml_server_config.yaml │ ├── entry_script.sh │ └── template.j2 ├── create_deployable.py ├── generate.py ├── parameters.py ├── registry_utils.py ├── templates │ ├── cloudformation_default.yaml │ └── terraform_default.tf └── utils.py ├── operator_config.py ├── requirements.txt └── tests ├── classifier.py └── test_api.py /.github/generate-toc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This script was taken from https://github.com/ekalinin/github-markdown-toc 4 | # checkout the repo for how to use it 5 | # 6 | # Internal Working 7 | # Steps: 8 | # 9 | # 1. Download corresponding html file for some README.md: 10 | # curl -s $1 11 | # 12 | # 2. Discard rows where no substring 'user-content-' (github's markup): 13 | # awk '/user-content-/ { ... 14 | # 15 | # 3.1 Get last number in each row like ' ... sitemap.js.*<\/h/)+2, RLENGTH-5) 24 | # 25 | # 5. Find anchor and insert it inside "(...)": 26 | # substr($0, match($0, "href=\"[^\"]+?\" ")+6, RLENGTH-8) 27 | # 28 | 29 | gh_toc_version="0.8.0" 30 | 31 | gh_user_agent="gh-md-toc v$gh_toc_version" 32 | 33 | # 34 | # Download rendered into html README.md by its url. 35 | # 36 | # 37 | gh_toc_load() { 38 | local gh_url=$1 39 | 40 | if type curl &>/dev/null; then 41 | curl --user-agent "$gh_user_agent" -s "$gh_url" 42 | elif type wget &>/dev/null; then 43 | wget --user-agent="$gh_user_agent" -qO- "$gh_url" 44 | else 45 | echo "Please, install 'curl' or 'wget' and try again." 46 | exit 1 47 | fi 48 | } 49 | 50 | # 51 | # Converts local md file into html by GitHub 52 | # 53 | # -> curl -X POST --data '{"text": "Hello world github/linguist#1 **cool**, and #1!"}' https://api.github.com/markdown 54 | #

Hello world github/linguist#1 cool, and #1!

'" 55 | gh_toc_md2html() { 56 | local gh_file_md=$1 57 | URL=https://api.github.com/markdown/raw 58 | 59 | if [ ! -z "$GH_TOC_TOKEN" ]; then 60 | TOKEN=$GH_TOC_TOKEN 61 | else 62 | TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" 63 | if [ -f "$TOKEN_FILE" ]; then 64 | TOKEN="$(cat $TOKEN_FILE)" 65 | fi 66 | fi 67 | if [ ! -z "${TOKEN}" ]; then 68 | AUTHORIZATION="Authorization: token ${TOKEN}" 69 | fi 70 | 71 | # echo $URL 1>&2 72 | OUTPUT=$(curl -s \ 73 | --user-agent "$gh_user_agent" \ 74 | --data-binary @"$gh_file_md" \ 75 | -H "Content-Type:text/plain" \ 76 | -H "$AUTHORIZATION" \ 77 | "$URL") 78 | 79 | if [ "$?" != "0" ]; then 80 | echo "XXNetworkErrorXX" 81 | fi 82 | if [ "$(echo "${OUTPUT}" | awk '/API rate limit exceeded/')" != "" ]; then 83 | echo "XXRateLimitXX" 84 | else 85 | echo "${OUTPUT}" 86 | fi 87 | } 88 | 89 | 90 | # 91 | # Is passed string url 92 | # 93 | gh_is_url() { 94 | case $1 in 95 | https* | http*) 96 | echo "yes";; 97 | *) 98 | echo "no";; 99 | esac 100 | } 101 | 102 | # 103 | # TOC generator 104 | # 105 | gh_toc(){ 106 | local gh_src=$1 107 | local gh_src_copy=$1 108 | local gh_ttl_docs=$2 109 | local need_replace=$3 110 | local no_backup=$4 111 | local no_footer=$5 112 | 113 | if [ "$gh_src" = "" ]; then 114 | echo "Please, enter URL or local path for a README.md" 115 | exit 1 116 | fi 117 | 118 | 119 | # Show "TOC" string only if working with one document 120 | if [ "$gh_ttl_docs" = "1" ]; then 121 | 122 | echo "Table of Contents" 123 | echo "=================" 124 | echo "" 125 | gh_src_copy="" 126 | 127 | fi 128 | 129 | if [ "$(gh_is_url "$gh_src")" == "yes" ]; then 130 | gh_toc_load "$gh_src" | gh_toc_grab "$gh_src_copy" 131 | if [ "${PIPESTATUS[0]}" != "0" ]; then 132 | echo "Could not load remote document." 133 | echo "Please check your url or network connectivity" 134 | exit 1 135 | fi 136 | if [ "$need_replace" = "yes" ]; then 137 | echo 138 | echo "!! '$gh_src' is not a local file" 139 | echo "!! Can't insert the TOC into it." 140 | echo 141 | fi 142 | else 143 | local rawhtml=$(gh_toc_md2html "$gh_src") 144 | if [ "$rawhtml" == "XXNetworkErrorXX" ]; then 145 | echo "Parsing local markdown file requires access to github API" 146 | echo "Please make sure curl is installed and check your network connectivity" 147 | exit 1 148 | fi 149 | if [ "$rawhtml" == "XXRateLimitXX" ]; then 150 | echo "Parsing local markdown file requires access to github API" 151 | echo "Error: You exceeded the hourly limit. See: https://developer.github.com/v3/#rate-limiting" 152 | TOKEN_FILE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/token.txt" 153 | echo "or place GitHub auth token here: ${TOKEN_FILE}" 154 | exit 1 155 | fi 156 | local toc=`echo "$rawhtml" | gh_toc_grab "$gh_src_copy"` 157 | echo "$toc" 158 | if [ "$need_replace" = "yes" ]; then 159 | if grep -Fxq "" $gh_src && grep -Fxq "" $gh_src; then 160 | echo "Found markers" 161 | else 162 | echo "You don't have or in your file...exiting" 163 | exit 1 164 | fi 165 | local ts="<\!--ts-->" 166 | local te="<\!--te-->" 167 | local dt=`date +'%F_%H%M%S'` 168 | local ext=".orig.${dt}" 169 | local toc_path="${gh_src}.toc.${dt}" 170 | local toc_header="## Table of Contents" 171 | local toc_footer="" 172 | # http://fahdshariff.blogspot.ru/2012/12/sed-mutli-line-replacement-between-two.html 173 | # clear old TOC 174 | sed -i${ext} "/${ts}/,/${te}/{//!d;}" "$gh_src" 175 | 176 | # add header 177 | echo -e "\n${toc_header}\n" > "${toc_path}" 178 | 179 | # create toc file 180 | echo "${toc}" >> "$toc_path" 181 | 182 | # add footer 183 | if [ "${no_footer}" != "yes" ]; then 184 | echo -e "\n${toc_footer}\n" >> "$toc_path" 185 | fi 186 | 187 | # insert toc file 188 | if [[ "`uname`" == "Darwin" ]]; then 189 | sed -i "" "/${ts}/r ${toc_path}" "$gh_src" 190 | else 191 | sed -i "/${ts}/r ${toc_path}" "$gh_src" 192 | fi 193 | echo 194 | if [ "${no_backup}" = "yes" ]; then 195 | rm ${toc_path} ${gh_src}${ext} 196 | fi 197 | echo "!! TOC was added into: '$gh_src'" 198 | if [ -z "${no_backup}" ]; then 199 | echo "!! Origin version of the file: '${gh_src}${ext}'" 200 | echo "!! TOC added into a separate file: '${toc_path}'" 201 | fi 202 | echo 203 | fi 204 | fi 205 | } 206 | 207 | # 208 | # Grabber of the TOC from rendered html 209 | # 210 | # $1 - a source url of document. 211 | # It's need if TOC is generated for multiple documents. 212 | # 213 | gh_toc_grab() { 214 | common_awk_script=' 215 | modified_href = "" 216 | split(href, chars, "") 217 | for (i=1;i <= length(href); i++) { 218 | c = chars[i] 219 | res = "" 220 | if (c == "+") { 221 | res = " " 222 | } else { 223 | if (c == "%") { 224 | res = "\\x" 225 | } else { 226 | res = c "" 227 | } 228 | } 229 | modified_href = modified_href res 230 | } 231 | print sprintf("%*s", (level-1)*3, "") "* [" text "](" gh_url modified_href ")" 232 | ' 233 | if [ `uname -s` == "OS/390" ]; then 234 | grepcmd="pcregrep -o" 235 | echoargs="" 236 | awkscript='{ 237 | level = substr($0, length($0), 1) 238 | text = substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5) 239 | href = substr($0, match($0, "href=\"([^\"]+)?\"")+6, RLENGTH-7) 240 | '"$common_awk_script"' 241 | }' 242 | else 243 | grepcmd="grep -Eo" 244 | echoargs="-e" 245 | awkscript='{ 246 | level = substr($0, length($0), 1) 247 | text = substr($0, match($0, /a>.*<\/h/)+2, RLENGTH-5) 248 | href = substr($0, match($0, "href=\"[^\"]+?\"")+6, RLENGTH-7) 249 | '"$common_awk_script"' 250 | }' 251 | fi 252 | href_regex='href=\"[^\"]+?\"' 253 | 254 | # if closed is on the new line, then move it on the prev line 255 | # for example: 256 | # was: The command foo1 257 | # 258 | # became: The command foo1 259 | sed -e ':a' -e 'N' -e '$!ba' -e 's/\n<\/h/<\/h/g' | 260 | 261 | # find strings that corresponds to template 262 | $grepcmd '//g' | sed 's/<\/code>//g' | 266 | 267 | # remove g-emoji 268 | sed 's/]*[^<]*<\/g-emoji> //g' | 269 | 270 | # now all rows are like: 271 | # ... / placeholders" 300 | echo " $app_name - Create TOC for markdown from STDIN" 301 | echo " $app_name --help Show help" 302 | echo " $app_name --version Show version" 303 | return 304 | fi 305 | 306 | if [ "$1" = '--version' ]; then 307 | echo "$gh_toc_version" 308 | echo 309 | echo "os: `lsb_release -d | cut -f 2`" 310 | echo "kernel: `cat /proc/version`" 311 | echo "shell: `$SHELL --version`" 312 | echo 313 | for tool in curl wget grep awk sed; do 314 | printf "%-5s: " $tool 315 | echo `$tool --version | head -n 1` 316 | done 317 | return 318 | fi 319 | 320 | if [ "$1" = "-" ]; then 321 | if [ -z "$TMPDIR" ]; then 322 | TMPDIR="/tmp" 323 | elif [ -n "$TMPDIR" -a ! -d "$TMPDIR" ]; then 324 | mkdir -p "$TMPDIR" 325 | fi 326 | local gh_tmp_md 327 | if [ `uname -s` == "OS/390" ]; then 328 | local timestamp=$(date +%m%d%Y%H%M%S) 329 | gh_tmp_md="$TMPDIR/tmp.$timestamp" 330 | else 331 | gh_tmp_md=$(mktemp $TMPDIR/tmp.XXXXXX) 332 | fi 333 | while read input; do 334 | echo "$input" >> "$gh_tmp_md" 335 | done 336 | gh_toc_md2html "$gh_tmp_md" | gh_toc_grab "" 337 | return 338 | fi 339 | 340 | if [ "$1" = '--insert' ]; then 341 | need_replace="yes" 342 | shift 343 | fi 344 | 345 | if [ "$1" = '--no-backup' ]; then 346 | need_replace="yes" 347 | no_backup="yes" 348 | shift 349 | fi 350 | 351 | if [ "$1" = '--hide-footer' ]; then 352 | need_replace="yes" 353 | no_footer="yes" 354 | shift 355 | fi 356 | 357 | for md in "$@" 358 | do 359 | echo "" 360 | gh_toc "$md" "$#" "$need_replace" "$no_backup" "$no_footer" 361 | done 362 | 363 | echo "" 364 | echo "Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)" 365 | } 366 | 367 | # 368 | # Entry point 369 | # 370 | gh_toc_app "$@" 371 | 372 | -------------------------------------------------------------------------------- /.github/workflows/e2e-test-deploy.yaml: -------------------------------------------------------------------------------- 1 | # Run secret-dependent integration tests only after /ok-to-test approval 2 | on: 3 | pull_request: 4 | repository_dispatch: 5 | types: [test-e2e-deploy-command] 6 | 7 | name: e2e tests 8 | 9 | jobs: 10 | # Repo owner has commented / on a (fork-based) pull request 11 | test-e2e-deploy: 12 | runs-on: ubuntu-latest 13 | if: 14 | github.event_name == 'repository_dispatch' && 15 | github.event.client_payload.slash_command.command == 'test-e2e-deploy' && 16 | github.event.client_payload.slash_command.args.named.sha != '' && 17 | contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.args.named.sha) 18 | steps: 19 | 20 | # Check out merge commit 21 | - name: Fork based /test-e2e-deploy checkout 22 | uses: actions/checkout@v2 23 | with: 24 | ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' 25 | 26 | # 27 | 28 | - run: aws --version 29 | - name: Configure AWS Credentials 30 | uses: aws-actions/configure-aws-credentials@v1 31 | with: 32 | aws-access-key-id: ${{ secrets.AWS_KEY_ID }} 33 | aws-secret-access-key: ${{ secrets.AWS_SEC }} 34 | aws-region: 'us-west-1' 35 | - name: Setup python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: '3.8' 39 | - name: Install dependencies 40 | run: python -m pip install -r requirements.txt 41 | - name: Run Integration tests for deploy 42 | run: python tests/test_api.py 43 | 44 | # Update check run called "integration-fork" 45 | - uses: actions/github-script@v1 46 | id: update-check-run 47 | if: ${{ always() }} 48 | env: 49 | number: ${{ github.event.client_payload.pull_request.number }} 50 | job: ${{ github.job }} 51 | # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run 52 | conclusion: ${{ job.status }} 53 | with: 54 | github-token: ${{ secrets.GITHUB_TOKEN }} 55 | script: | 56 | const { data: pull } = await github.pulls.get({ 57 | ...context.repo, 58 | pull_number: process.env.number 59 | }); 60 | const ref = pull.head.sha; 61 | 62 | const { data: checks } = await github.checks.listForRef({ 63 | ...context.repo, 64 | ref 65 | }); 66 | 67 | const check = checks.check_runs.filter(c => c.name === process.env.job); 68 | 69 | const { data: result } = await github.checks.update({ 70 | ...context.repo, 71 | check_run_id: check[0].id, 72 | status: 'completed', 73 | conclusion: process.env.conclusion 74 | }); 75 | 76 | return result; 77 | -------------------------------------------------------------------------------- /.github/workflows/help-action.yml: -------------------------------------------------------------------------------- 1 | name: help action 2 | on: 3 | repository_dispatch: 4 | types: [help-command] 5 | 6 | jobs: 7 | display-help-info: 8 | runs-on: ubuntu-latest 9 | if: 10 | github.event_name == 'repository_dispatch' && 11 | github.event.client_payload.slash_command.command == 'help' 12 | steps: 13 | - name: Create comment 14 | uses: peter-evans/create-or-update-comment@v1 15 | with: 16 | issue-number: ${{ github.event.client_payload.github.payload.issue.number }} 17 | body: | 18 | Hello @${{ github.event.client_payload.github.actor }}! 19 | These are the list of commands available in this repo. 20 | 21 | | Command | Description | 22 | | ------- | ----------- | 23 | | /test-e2e-deploy sha=_"first 7 char of commit sha"_ | Runs the e2e test for this deployment tool. | 24 | -------------------------------------------------------------------------------- /.github/workflows/slash-command-dispatcher.yaml: -------------------------------------------------------------------------------- 1 | # If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event 2 | name: slash command dispatcher 3 | 4 | on: 5 | issue_comment: 6 | types: [created] 7 | 8 | jobs: 9 | test-e2e-deploy: 10 | runs-on: ubuntu-latest 11 | # Only run for PRs, not issue comments 12 | if: ${{ github.event.issue.pull_request }} 13 | steps: 14 | # Generate a GitHub App installation access token from an App ID and private key 15 | # To create a new GitHub App: 16 | # https://developer.github.com/apps/building-github-apps/creating-a-github-app/ 17 | # See app.yml for an example app manifest 18 | - name: Generate token 19 | id: generate_token 20 | uses: tibdex/github-app-token@v1 21 | with: 22 | app_id: ${{ secrets.DEPLOY_BOT_APP_ID }} 23 | private_key: ${{ secrets.DEPLOY_BOT_PRIVATE_KEY }} 24 | 25 | - name: Slash Command Dispatch 26 | uses: peter-evans/slash-command-dispatch@v2 27 | env: 28 | TOKEN: ${{ steps.generate_token.outputs.token }} 29 | with: 30 | token: ${{ env.TOKEN }} # GitHub App installation access token 31 | #token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work 32 | reaction-token: ${{ secrets.GITHUB_TOKEN }} 33 | issue-type: pull-request 34 | commands: test-e2e-deploy 35 | permission: write 36 | 37 | help-command: 38 | runs-on: ubuntu-latest 39 | steps: 40 | # Checkout is necessary here due to referencing a local action. 41 | # It's also necessary when using the 'config-from-file' option. 42 | # Otherwise, avoid using checkout to keep this workflow fast. 43 | - uses: actions/checkout@v2 44 | 45 | - name: Generate token 46 | id: generate_token 47 | uses: tibdex/github-app-token@v1 48 | with: 49 | app_id: ${{ secrets.DEPLOY_BOT_APP_ID }} 50 | private_key: ${{ secrets.DEPLOY_BOT_PRIVATE_KEY }} 51 | 52 | - name: slash command dispatch 53 | uses: peter-evans/slash-command-dispatch@v2 54 | env: 55 | TOKEN: ${{ steps.generate_token.outputs.token }} 56 | with: 57 | token: ${{ env.TOKEN }} 58 | commands: help 59 | permission: none 60 | issue-type: both 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Editor 132 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠️ BentoCTL project has been deprecated 2 | 3 | Plese see the latest BentoML documentation on OCI-container based deployment workflow: https://docs.bentoml.com/ 4 | 5 | ## Bentoctl AWS Lambda deployment 6 | 7 | Bentoctl is a CLI tool for deploying your machine-learning models to any cloud platforms and serving predictions via REST APIs. 8 | It built on top of [BentoML: the unified model serving framework](https://github.com/bentoml/bentoml), and makes it easy to bring any BentoML packaged model to production. 9 | 10 | This repo contains the Bentoctl AWS Lambda deployment operator. This operator defines the terraform configuration for deploying the Lambda function and how to build docker image that's compatible with AWS Lambda. 11 | 12 | 13 | > **Note:** This operator is compatible with BentoML version 1.0.0 and above. For older versions, please switch to the branch `pre-v1.0` and follow the instructions in the README.md. 14 | 15 | 16 | ## Table of Contents 17 | 18 | * [Prerequisites](#prerequisites) 19 | * [Quickstart with bentoctl](#quickstart-with-bentoctl) 20 | * [Configuration options](#configuration-options) 21 | 22 | 23 | 24 | 25 | 26 | ## Prerequisites 27 | 28 | 1. Bentoml - BentoML version 1.0 and greater. Please follow the [Installation guide](https://docs.bentoml.org/en/latest/quickstart.html#installation). 29 | 2. Terraform - [Terraform](https://www.terraform.io/) is a tool for building, configuring, and managing infrastructure. 30 | 3. AWS CLI installed and configured with an AWS account with permission to the Cloudformation, Lamba, API Gateway and ECR. Please follow the [Installation guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). 31 | 4. Docker - Install instruction: https://docs.docker.com/installll instruction: https://www.terraform.io/downloads.html 32 | 5. A built Bento project. For this guide, we will use the Iris classifier bento from the [BentoML quickstart guide](https://docs.bentoml.org/en/latest/quickstart.html#quickstart). 33 | 34 | ## Quickstart with bentoctl 35 | 36 | Bentoctl is a CLI tool that you can use to deploy bentos to Lambda. It helps in configuring and managing your deployments super easy. 37 | 38 | 1. Install bentoctl via pip 39 | ``` 40 | $ pip install bentoctl 41 | ``` 42 | 43 | 2. Install AWS Lambda operator 44 | 45 | Bentoctl will install the official AWS Lambda operator and its dependencies. 46 | 47 | ``` 48 | $ bentoctl operator install aws-lambda 49 | ``` 50 | 51 | 3. Initialize deployment with bentoctl 52 | 53 | Follow the interactive guide to initialize deployment project. 54 | 55 | ```bash 56 | $ bentoctl init 57 | 58 | Bentoctl Interactive Deployment Config Builder 59 | 60 | Welcome! You are now in interactive mode. 61 | 62 | This mode will help you setup the deployment_config.yaml file required for 63 | deployment. Fill out the appropriate values for the fields. 64 | 65 | (deployment config will be saved to: ./deployment_config.yaml) 66 | 67 | api_version: v1 68 | name: demo 69 | operator: aws-lambda 70 | template: terraform 71 | spec: 72 | region: us-west-1 73 | timeout: 10 74 | memory_size: 512 75 | filename for deployment_config [deployment_config.yaml]: 76 | deployment config file exists! Should I override? [Y/n]: 77 | deployment config generated to: deployment_config.yaml 78 | ✨ generated template files. 79 | - bentoctl.tfvars 80 | - main.tf 81 | ``` 82 | 83 | 4. Build and push AWS Lambda comptable docker image to registry 84 | 85 | Bentoctl will build and push the Lambda compatible docker image to the AWS ECR repository. 86 | 87 | ```bash 88 | bentoctl build -b iris_classifier:latest -f deployment_config.yaml 89 | 90 | Step 1/20 : FROM bentoml/bento-server:1.0.0a7-python3.7-debian-runtime 91 | ---> dde7b88477b1 92 | Step 2/20 : ARG UID=1034 93 | ---> Running in b8f4ae1d8b08 94 | ---> e6c313c8d9ea 95 | Step 3/20 : ARG GID=1034 96 | .... 97 | Step 20/20 : ENTRYPOINT [ "/opt/conda/bin/python", "-m", "awslambdaric" ] 98 | ---> Running in 4e56057f3b18 99 | ---> dca82bca9034 100 | Successfully built dca82bca9034 101 | Successfully tagged aws-lambda-iris_classifier:btzv5wfv665trhcu 102 | 🔨 Image build! 103 | The push refers to repository [192023623294.dkr.ecr.us-west-1.amazonaws.com/quickstart] 104 | btzv5wfv665trhcu: digest: sha256:ffcd120f7629122cf5cd95664e4fd28e9a50e799be7bb23f0b5b03f14ca5c672 size: 3253 105 | 32096534b881: Pushed 106 | f709d8f0f57d: Pushed 107 | 7d30486f5c78: Pushed 108 | ... 109 | c1065d45b872: Pushed 110 | 🚀 Image pushed! 111 | ✨ generated template files. 112 | - bentoctl.tfvars 113 | The push refers to repository [192023623294.dkr.ecr.us-west-1.amazonaws.com/quickstart] 114 | ``` 115 | 116 | 5. Apply Deployment with Terraform 117 | 118 | 1. Initialize terraform project 119 | ```bash 120 | terraform init 121 | ``` 122 | 123 | 2. Apply terraform project to create Lambda deployment 124 | 125 | ```bash 126 | terraform apply -var-file=bentoctl.tfvars -auto-approve 127 | 128 | aws_iam_role.lambda_exec: Creating... 129 | aws_apigatewayv2_api.lambda: Creating... 130 | aws_apigatewayv2_api.lambda: Creation complete after 1s [id=ka8h2p2yfh] 131 | aws_cloudwatch_log_group.api_gw: Creating... 132 | aws_cloudwatch_log_group.api_gw: Creation complete after 0s [id=/aws/api_gw/quickstart-gw] 133 | aws_apigatewayv2_stage.lambda: Creating... 134 | aws_iam_role.lambda_exec: Creation complete after 3s [id=quickstart-iam] 135 | aws_iam_role_policy_attachment.lambda_policy: Creating... 136 | aws_lambda_function.fn: Creating... 137 | aws_apigatewayv2_stage.lambda: Creation complete after 2s [id=$default] 138 | aws_iam_role_policy_attachment.lambda_policy: Creation complete after 1s [id=quickstart-iam-20220414203448384500000001] 139 | aws_lambda_function.fn: Still creating... [10s elapsed] 140 | aws_lambda_function.fn: Still creating... [20s elapsed] 141 | aws_lambda_function.fn: Still creating... [30s elapsed] 142 | aws_lambda_function.fn: Still creating... [40s elapsed] 143 | aws_lambda_function.fn: Creation complete after 41s [id=quickstart-function] 144 | aws_lambda_permission.api_gw: Creating... 145 | aws_cloudwatch_log_group.lg: Creating... 146 | aws_apigatewayv2_integration.lambda: Creating... 147 | aws_lambda_permission.api_gw: Creation complete after 0s [id=AllowExecutionFromAPIGateway] 148 | aws_cloudwatch_log_group.lg: Creation complete after 0s [id=/aws/lambda/quickstart-function] 149 | aws_apigatewayv2_integration.lambda: Creation complete after 1s [id=8gumjws] 150 | aws_apigatewayv2_route.root: Creating... 151 | aws_apigatewayv2_route.services: Creating... 152 | aws_apigatewayv2_route.root: Creation complete after 0s [id=jjp5f23] 153 | aws_apigatewayv2_route.services: Creation complete after 0s [id=8n57a1d] 154 | 155 | Apply complete! Resources: 11 added, 0 changed, 0 destroyed. 156 | 157 | Outputs: 158 | 159 | base_url = "https://ka8h2p2yfh.execute-api.us-west-1.amazonaws.com/" 160 | function_name = "quickstart-function" 161 | image_tag = "192023623294.dkr.ecr.us-west-1.amazonaws.com/quickstart:btzv5wfv665trhcu" 162 | ``` 163 | 164 | 6. Test deployed endpoint 165 | 166 | The `iris_classifier` uses the `/classify` endpoint for receiving requests so the full URL for the classifier will be in the form `{EndpointUrl}/classify` 167 | 168 | ```bash 169 | URL=$(terraform output -json | jq -r .base_url.value)classify 170 | curl -i \ 171 | --header "Content-Type: application/json" \ 172 | --request POST \ 173 | --data '[5.1, 3.5, 1.4, 0.2]' \ 174 | $URL 175 | 176 | HTTP/2 200 177 | date: Thu, 14 Apr 2022 23:02:45 GMT 178 | content-type: application/json 179 | content-length: 1 180 | apigw-requestid: Ql8zbicdSK4EM5g= 181 | 182 | 0% 183 | ``` 184 | 185 | 7. Delete deployment 186 | Use the `bentoctl destroy` command to remove the registry and the deployment 187 | 188 | ```bash 189 | bentoctl destroy -f deployment_config.yaml 190 | ``` 191 | ## Configuration options 192 | 193 | * `region`: AWS region for Lambda deployment 194 | * `timeout`: Timeout per request 195 | * `memory_size`: The memory for your function, set a value between 128 MB and 10,240 MB in 1-MB increments 196 | -------------------------------------------------------------------------------- /bentoctl_lambda/__init__.py: -------------------------------------------------------------------------------- 1 | from .create_deployable import create_deployable 2 | from .generate import generate 3 | from .registry_utils import create_repository, delete_repository 4 | 5 | __all__ = ["create_deployable", "generate", "create_repository", "delete_repository"] 6 | -------------------------------------------------------------------------------- /bentoctl_lambda/aws_lambda/README.md: -------------------------------------------------------------------------------- 1 | AWS Lambda supports deployment via containers of upto 10gb and that is what we 2 | are using for deploying bentos into lambda. The docs can be accessed 3 | [here](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html). 4 | 5 | In order for AWS to run the container in lambda they require the following 6 | - Defining a handler: this is the python code that will handle the `event` and 7 | `context` dict that is passed when ever the lambda function is invoked. Our 8 | handler is defined in ./app.py and it uses 9 | [Mangum](https://github.com/jordaneremieff/mangum) to handle the incoming 10 | requrest. 11 | - setup aws-lambda-ric: this is the runtime-interface-client that AWS has 12 | developed to interface with the Runtime API 13 | (https://github.com/aws/aws-lambda-python-runtime-interface-client). The 14 | ./entry_script.sh handles invoking and loading the handler with the RIC. 15 | 16 | ## Terraform setup 17 | 18 | AWS lambda is setup with an HTTP API infront to act as public endpoint for the 19 | lambda function. Any request that comes is passed onto the lambda function. The 20 | request is handled by mangum which transforms it into a ASGI request that can be 21 | processed by bentoml's ASGI app. 22 | 23 | All the logs are in cloudwatch and IAM policies for every infra is created by 24 | the terraform template. 25 | 26 | ## Dev setup 27 | 28 | Right now running it locally is not possible. There is a lambda Runtime Emulator 29 | that loads any handler and emulates the AWS interface but it is still work in 30 | progress. 31 | -------------------------------------------------------------------------------- /bentoctl_lambda/aws_lambda/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from bentoml import load 4 | from mangum import Mangum 5 | 6 | API_GATEWAY_STAGE = os.environ.get("API_GATEWAY_STAGE", None) 7 | 8 | print("Loading from dir...") 9 | bento_service = load("./", standalone_load=True) 10 | 11 | print("bento service", bento_service) 12 | 13 | mangum_app = Mangum(bento_service.asgi_app, api_gateway_base_path=API_GATEWAY_STAGE) 14 | -------------------------------------------------------------------------------- /bentoctl_lambda/aws_lambda/aws-lambda-rie-x86: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bentoml/aws-lambda-deploy/80ebada800e6db5af66bf38d077ebd59c004f89e/bentoctl_lambda/aws_lambda/aws-lambda-rie-x86 -------------------------------------------------------------------------------- /bentoctl_lambda/aws_lambda/bentoml_server_config.yaml: -------------------------------------------------------------------------------- 1 | api_server: 2 | metrics: 3 | enabled: False 4 | -------------------------------------------------------------------------------- /bentoctl_lambda/aws_lambda/entry_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then 3 | exec /usr/local/bin/aws-lambda-rie /usr/local/bin/python -m awslambdaric $@ 4 | else 5 | exec /usr/local/bin/python -m awslambdaric $@ 6 | fi 7 | -------------------------------------------------------------------------------- /bentoctl_lambda/aws_lambda/template.j2: -------------------------------------------------------------------------------- 1 | {% extends bento_base_template %} 2 | {% set bento__home = "/tmp" %} 3 | {% block SETUP_BENTO_ENTRYPOINT %} 4 | EXPOSE 3000 5 | ENV BENTOML_CONFIG={{bento__path}}/bentoml_config.yaml 6 | 7 | RUN {{ common.mount_cache("/root/.cache/pip") }} pip install awslambdaric==2.0.8 mangum==0.12.3 8 | 9 | USER root 10 | ADD ./aws-lambda-rie /usr/local/bin/aws-lambda-rie 11 | RUN chmod +x /usr/local/bin/aws-lambda-rie 12 | RUN chmod +x {{bento__path}}/env/docker/entry_script.sh 13 | ENTRYPOINT ["{{bento__path}}/env/docker/entry_script.sh"] 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /bentoctl_lambda/create_deployable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import shutil 5 | from pathlib import Path 6 | from sys import version_info 7 | from typing import Any 8 | 9 | from attr import asdict 10 | 11 | if version_info >= (3, 8): 12 | from shutil import copytree 13 | else: 14 | from backports.shutil_copytree import copytree 15 | 16 | from bentoml._internal.bento.bento import BentoInfo 17 | from bentoml._internal.bento.build_config import DockerOptions 18 | from bentoml._internal.bento.gen import generate_dockerfile 19 | 20 | LAMBDA_DIR = Path(os.path.dirname(__file__), "aws_lambda") 21 | TEMPLATE_PATH = LAMBDA_DIR.joinpath("template.j2") 22 | APP_PATH = LAMBDA_DIR.joinpath("app.py") 23 | ENTRY_SCRIPT = LAMBDA_DIR.joinpath("entry_script.sh") 24 | AWS_LAMBDA_RIE = LAMBDA_DIR.joinpath("aws-lambda-rie-x86") 25 | BENTOML_CONFIG_FILE = LAMBDA_DIR.joinpath("bentoml_server_config.yaml") 26 | 27 | 28 | def create_deployable( 29 | bento_path: str, 30 | destination_dir: str, 31 | bento_metadata: dict[str, Any], 32 | overwrite_deployable: bool, 33 | ) -> str: 34 | """ 35 | The deployable is the bento along with all the modifications (if any) 36 | requried to deploy to the cloud service. 37 | 38 | Parameters 39 | ---------- 40 | bento_path: str 41 | Path to the bento from the bento store. 42 | destination_dir: str 43 | directory to create the deployable into. 44 | bento_metadata: dict 45 | metadata about the bento. 46 | 47 | Returns 48 | ------- 49 | docker_context_path : str 50 | path to the docker context. 51 | """ 52 | 53 | deployable_path = Path(destination_dir) 54 | copytree(bento_path, deployable_path, dirs_exist_ok=True) 55 | 56 | bento_metafile = Path(bento_path, "bento.yaml") 57 | with bento_metafile.open("r", encoding="utf-8") as metafile: 58 | info = BentoInfo.from_yaml_file(metafile) 59 | 60 | options = asdict(info.docker) 61 | options["dockerfile_template"] = TEMPLATE_PATH 62 | 63 | dockerfile_path = deployable_path.joinpath("env", "docker", "Dockerfile") 64 | with dockerfile_path.open("w", encoding="utf-8") as dockerfile: 65 | dockerfile_generated = generate_dockerfile( 66 | DockerOptions(**options).with_defaults(), 67 | str(deployable_path), 68 | use_conda=not info.conda.is_empty(), 69 | ) 70 | dockerfile.write(dockerfile_generated) 71 | 72 | # copy over app.py file 73 | shutil.copy(str(APP_PATH), os.path.join(deployable_path, "app.py")) 74 | 75 | # the entry_script.sh file that will be the entrypoint 76 | shutil.copy( 77 | str(ENTRY_SCRIPT), 78 | os.path.join(deployable_path, "env", "docker", "entry_script.sh"), 79 | ) 80 | 81 | # aws-lambda runtime-interface-emulator - check docs for more info 82 | shutil.copy( 83 | str(AWS_LAMBDA_RIE), 84 | os.path.join(deployable_path, "aws-lambda-rie"), 85 | ) 86 | 87 | # bentoml_config_file to dissable /metrics 88 | shutil.copy( 89 | str(BENTOML_CONFIG_FILE), 90 | os.path.join(deployable_path, "bentoml_config.yaml"), 91 | ) 92 | 93 | return str(deployable_path) 94 | -------------------------------------------------------------------------------- /bentoctl_lambda/generate.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | 4 | from bentoctl.exceptions import TemplateExists, TemplateTypeNotDefined 5 | 6 | from bentoctl_lambda.parameters import DeploymentValues 7 | from bentoctl_lambda.utils import get_metadata 8 | 9 | 10 | def generate(name, spec, template_type, destination_dir, values_only=True): 11 | """ 12 | generates the template corresponding to the template_type. 13 | 14 | Parameters 15 | ---------- 16 | name : str 17 | deployment name to be used by the template. This name will be used 18 | to create the resource names. 19 | spec : dict 20 | The properties of the deployment (specifications) passed from the 21 | deployment_config's `spec` section. 22 | template_type: str 23 | The type of template that is to be generated by the operator. The 24 | available ones are [terraform, cloudformation] 25 | 26 | Returns 27 | ------- 28 | generated_path : str 29 | The path for the generated template. 30 | """ 31 | if template_type == "terraform": 32 | template_file_name = "terraform_default.tf" 33 | generated_template_file_name = "main.tf" 34 | generated_params_file_name = "bentoctl.tfvars" 35 | else: 36 | raise TemplateTypeNotDefined(template_type) 37 | 38 | generated_files = [generated_params_file_name] 39 | generated_template_file_path = os.path.join( 40 | destination_dir, generated_template_file_name 41 | ) 42 | if not values_only: 43 | if os.path.exists(generated_template_file_path): 44 | raise TemplateExists(generated_template_file_path) 45 | shutil.copyfile( 46 | os.path.join(os.path.dirname(__file__), f"templates/{template_file_name}"), 47 | generated_template_file_path, 48 | ) 49 | generated_files.append(generated_template_file_name) 50 | 51 | # generate params file 52 | params = DeploymentValues(name, spec, template_type) 53 | params.to_params_file(os.path.join(destination_dir, generated_params_file_name)) 54 | 55 | return generated_files 56 | -------------------------------------------------------------------------------- /bentoctl_lambda/parameters.py: -------------------------------------------------------------------------------- 1 | from collections import UserDict 2 | 3 | DEPLOYMENT_PARAMS_WARNING = """# This file is maintained automatically by 4 | # "bentoctl generate" and "bentoctl build" commands. 5 | # Manual edits may be lost the next time these commands are run. 6 | 7 | """ 8 | 9 | 10 | def parse_image_tag(image_tag: str): 11 | registry_url, tag = image_tag.split("/") 12 | repository, version = tag.split(":") 13 | 14 | return registry_url, repository, version 15 | 16 | 17 | class DeploymentValues(UserDict): 18 | def __init__(self, name, spec, template_type): 19 | if "image_tag" in spec: 20 | _, image_repository, image_version = parse_image_tag(spec["image_tag"]) 21 | spec["image_repository"] = image_repository 22 | spec["image_version"] = image_version 23 | 24 | super().__init__({"deployment_name": name, **spec}) 25 | self.template_type = template_type 26 | 27 | def to_params_file(self, file_path): 28 | if self.template_type == "terraform": 29 | self.generate_terraform_tfvars_file(file_path) 30 | 31 | @classmethod 32 | def from_params_file(cls, file_path): 33 | pass 34 | 35 | def generate_terraform_tfvars_file(self, file_path): 36 | params = [] 37 | for param_name, param_value in self.items(): 38 | params.append(f'{param_name} = "{param_value}"') 39 | 40 | with open(file_path, "w") as params_file: 41 | params_file.write(DEPLOYMENT_PARAMS_WARNING) 42 | params_file.write("\n".join(params)) 43 | params_file.write("\n") 44 | -------------------------------------------------------------------------------- /bentoctl_lambda/registry_utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import boto3 4 | 5 | 6 | def get_ecr_login_info(region, repository_id): 7 | ecr_client = boto3.client("ecr", region) 8 | token = ecr_client.get_authorization_token(registryIds=[repository_id]) 9 | username, password = ( 10 | base64.b64decode(token["authorizationData"][0]["authorizationToken"]) 11 | .decode("utf-8") 12 | .split(":") 13 | ) 14 | registry_url = token["authorizationData"][0]["proxyEndpoint"] 15 | 16 | return registry_url, username, password 17 | 18 | 19 | def get_repository(ecr_client, repository_name): 20 | result = ecr_client.describe_repositories(repositoryNames=[repository_name]) 21 | repository_id = result["repositories"][0]["registryId"] 22 | repository_uri = result["repositories"][0]["repositoryUri"] 23 | 24 | return repository_id, repository_uri 25 | 26 | 27 | def create_ecr_repository_if_not_exists(region, repository_name): 28 | ecr_client = boto3.client("ecr", region) 29 | try: 30 | repository_id, repository_uri = get_repository(ecr_client, repository_name) 31 | except ecr_client.exceptions.RepositoryNotFoundException: 32 | result = ecr_client.create_repository(repositoryName=repository_name) 33 | repository_id = result["repository"]["registryId"] 34 | repository_uri = result["repository"]["repositoryUri"] 35 | return repository_id, repository_uri 36 | 37 | 38 | def create_repository(repository_name, operator_spec): 39 | """ 40 | Create the ECR registry and return the info. 41 | """ 42 | repo_id, _ = create_ecr_repository_if_not_exists( 43 | operator_spec.get("region"), repository_name 44 | ) 45 | registry_url, username, password = get_ecr_login_info( 46 | operator_spec["region"], repo_id 47 | ) 48 | repository_url = f"{registry_url}/{repository_name}" 49 | 50 | return repository_url, username, password 51 | 52 | 53 | def delete_repository(repository_name, operator_spec): 54 | """ 55 | Destroy the ECR registry created. 56 | """ 57 | ecr_client = boto3.client("ecr", operator_spec.get("region")) 58 | try: 59 | get_repository(ecr_client, repository_name) 60 | ecr_client.delete_repository(repositoryName=repository_name, force=True) 61 | except ecr_client.exceptions.RepositoryNotFoundException: 62 | print(f"Repository {repository_name} not found. Skipping registry cleanup") 63 | -------------------------------------------------------------------------------- /bentoctl_lambda/templates/cloudformation_default.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Parameters: 3 | deploymentName: 4 | Type: String 5 | repositoryName: 6 | Type: String 7 | repositoryTag: 8 | Type: String 9 | memorySize: 10 | Type: Number 11 | Default: 514 12 | timeout: 13 | Type: Number 14 | Default: 20 15 | Globals: 16 | Api: 17 | Auth: 18 | AddDefaultAuthorizerToCorsPreflight: false 19 | ApiKeyRequired: false 20 | DefaultAuthorizer: NONE 21 | BinaryMediaTypes: 22 | - '*~1*' 23 | Cors: '''*''' 24 | Function: 25 | MemorySize: !Ref memorySize 26 | Timeout: !Ref timeout 27 | Resources: 28 | pandas: 29 | Properties: 30 | Environment: 31 | Variables: 32 | API_GATEWAY_STAGE: '' 33 | Events: 34 | Api: 35 | Properties: 36 | Method: any 37 | Path: /{proxy+} 38 | Type: HttpApi 39 | FunctionName: !Sub ${deploymentName}-fn 40 | ImageConfig: 41 | Command: 42 | - app.mangum_app 43 | ImageUri: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${repositoryName}:${repositoryTag}' 44 | PackageType: Image 45 | Type: AWS::Serverless::Function 46 | Transform: AWS::Serverless-2016-10-31 47 | Outputs: 48 | EndpointUrl: 49 | Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" 50 | Description: URL for endpoint 51 | -------------------------------------------------------------------------------- /bentoctl_lambda/templates/terraform_default.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = "~> 4.0.0" 6 | } 7 | } 8 | 9 | required_version = "~> 1.0" 10 | } 11 | 12 | provider "aws" { 13 | region = var.region 14 | } 15 | 16 | ################################################################################ 17 | # Input variable definitions 18 | ################################################################################ 19 | 20 | variable "deployment_name" { 21 | type = string 22 | } 23 | 24 | variable "image_tag" { 25 | type = string 26 | } 27 | 28 | variable "image_repository" { 29 | type = string 30 | } 31 | 32 | variable "image_version" { 33 | type = string 34 | } 35 | 36 | variable "region" { 37 | description = "AWS region for all resources." 38 | 39 | type = string 40 | default = "ap-south-1" 41 | } 42 | 43 | variable "timeout" { 44 | description = "Timout for the Lambda Function." 45 | type = number 46 | default = 300 47 | } 48 | 49 | variable "memory_size" { 50 | description = "Memory allocated to lambda function." 51 | type = number 52 | default = 256 53 | } 54 | 55 | ################################################################################ 56 | # Resource definitions 57 | ################################################################################ 58 | 59 | data "aws_ecr_repository" "service" { 60 | name = var.image_repository 61 | } 62 | 63 | data "aws_ecr_image" "service_image" { 64 | repository_name = data.aws_ecr_repository.service.name 65 | image_tag = var.image_version 66 | } 67 | 68 | resource "aws_lambda_function" "fn" { 69 | function_name = "${var.deployment_name}-function" 70 | role = aws_iam_role.lambda_exec.arn 71 | 72 | timeout = var.timeout 73 | memory_size = var.memory_size 74 | image_uri = "${data.aws_ecr_repository.service.repository_url}@${data.aws_ecr_image.service_image.id}" 75 | package_type = "Image" 76 | 77 | image_config { 78 | command = [ 79 | "app.mangum_app", 80 | ] 81 | entry_point = [] 82 | } 83 | } 84 | 85 | resource "aws_cloudwatch_log_group" "lg" { 86 | name = "/aws/lambda/${aws_lambda_function.fn.function_name}" 87 | 88 | retention_in_days = 30 89 | } 90 | 91 | resource "aws_iam_role" "lambda_exec" { 92 | name = "${var.deployment_name}-iam" 93 | 94 | assume_role_policy = jsonencode({ 95 | Version = "2012-10-17" 96 | Statement = [{ 97 | Action = "sts:AssumeRole" 98 | Effect = "Allow" 99 | Sid = "" 100 | Principal = { 101 | Service = "lambda.amazonaws.com" 102 | } 103 | } 104 | ] 105 | }) 106 | } 107 | 108 | resource "aws_iam_role_policy_attachment" "lambda_policy" { 109 | role = aws_iam_role.lambda_exec.name 110 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 111 | } 112 | 113 | resource "aws_apigatewayv2_api" "lambda" { 114 | name = "${var.deployment_name}-gw" 115 | protocol_type = "HTTP" 116 | } 117 | 118 | resource "aws_apigatewayv2_stage" "lambda" { 119 | api_id = aws_apigatewayv2_api.lambda.id 120 | 121 | name = "$default" 122 | auto_deploy = true 123 | 124 | access_log_settings { 125 | destination_arn = aws_cloudwatch_log_group.api_gw.arn 126 | 127 | format = jsonencode({ 128 | requestId = "$context.requestId" 129 | sourceIp = "$context.identity.sourceIp" 130 | requestTime = "$context.requestTime" 131 | protocol = "$context.protocol" 132 | httpMethod = "$context.httpMethod" 133 | resourcePath = "$context.resourcePath" 134 | routeKey = "$context.routeKey" 135 | status = "$context.status" 136 | responseLength = "$context.responseLength" 137 | integrationErrorMessage = "$context.integrationErrorMessage" 138 | } 139 | ) 140 | } 141 | } 142 | 143 | resource "aws_apigatewayv2_integration" "lambda" { 144 | api_id = aws_apigatewayv2_api.lambda.id 145 | 146 | integration_uri = aws_lambda_function.fn.invoke_arn 147 | integration_type = "AWS_PROXY" 148 | integration_method = "POST" 149 | } 150 | 151 | resource "aws_apigatewayv2_route" "root" { 152 | api_id = aws_apigatewayv2_api.lambda.id 153 | 154 | route_key = "ANY /" 155 | target = "integrations/${aws_apigatewayv2_integration.lambda.id}" 156 | } 157 | 158 | resource "aws_apigatewayv2_route" "services" { 159 | api_id = aws_apigatewayv2_api.lambda.id 160 | 161 | route_key = "ANY /{proxy+}" 162 | target = "integrations/${aws_apigatewayv2_integration.lambda.id}" 163 | } 164 | 165 | resource "aws_cloudwatch_log_group" "api_gw" { 166 | name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}" 167 | retention_in_days = 30 168 | } 169 | 170 | resource "aws_lambda_permission" "api_gw" { 171 | statement_id = "AllowExecutionFromAPIGateway" 172 | action = "lambda:InvokeFunction" 173 | function_name = aws_lambda_function.fn.function_name 174 | principal = "apigateway.amazonaws.com" 175 | source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*" 176 | } 177 | 178 | ################################################################################ 179 | # Output value definitions 180 | ################################################################################ 181 | 182 | output "function_name" { 183 | description = "Name of the Lambda function." 184 | value = aws_lambda_function.fn.function_name 185 | } 186 | 187 | output "image_tag" { 188 | description = "The Image tag that is used for creating the function" 189 | value = var.image_tag 190 | } 191 | output "endpoint" { 192 | description = "Base URL for API Gateway stage." 193 | value = aws_apigatewayv2_stage.lambda.invoke_url 194 | } 195 | -------------------------------------------------------------------------------- /bentoctl_lambda/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import boto3 4 | import fs 5 | from bentoml.bentos import Bento 6 | 7 | 8 | def get_metadata(path: str): 9 | metadata = {} 10 | 11 | bento = Bento.from_fs(fs.open_fs(path)) 12 | metadata["tag"] = bento.tag 13 | metadata["bentoml_version"] = ".".join(bento.info.bentoml_version.split(".")[:3]) 14 | 15 | python_version_txt_path = "env/python/version.txt" 16 | python_version_txt_path = os.path.join(path, python_version_txt_path) 17 | with open(python_version_txt_path, "r") as f: 18 | python_version = f.read() 19 | metadata["python_version"] = ".".join(python_version.split(".")[:2]) 20 | 21 | return metadata 22 | -------------------------------------------------------------------------------- /operator_config.py: -------------------------------------------------------------------------------- 1 | OPERATOR_SCHEMA = { 2 | "region": { 3 | "required": True, 4 | "type": "string", 5 | "default": "us-west-1", 6 | "help_message": "AWS region for Lambda deployment", 7 | }, 8 | "timeout": { 9 | "required": False, 10 | "type": "integer", 11 | "coerce": int, 12 | "default": 10, 13 | "help_message": "Timeout per request", 14 | }, 15 | "memory_size": { 16 | "required": False, 17 | "type": "integer", 18 | "coerce": int, 19 | "default": 512, 20 | "help_message": "The memory for your function, set a value between 128 MB and 10,240 MB in 1-MB increments", 21 | }, 22 | } 23 | 24 | OPERATOR_NAME = "aws-lambda" 25 | 26 | OPERATOR_MODULE = "bentoctl_lambda" 27 | 28 | OPERATOR_DEFAULT_TEMPLATE = "terraform" 29 | 30 | OPERATOR_AVAILABLE_TEMPLATES = ["terraform"] 31 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3 2 | backports.shutil_copytree -------------------------------------------------------------------------------- /tests/classifier.py: -------------------------------------------------------------------------------- 1 | # iris_classifier.py 2 | from bentoml import BentoService, api, env 3 | from bentoml.adapters import DataframeInput, FileInput, JsonInput 4 | from bentoml.types import FileLike, JsonSerializable 5 | 6 | 7 | @env(infer_pip_packages=True) 8 | class TestService(BentoService): 9 | @api(input=DataframeInput(), batch=True) 10 | def dfapi(self, df): 11 | print(df) 12 | return df 13 | 14 | @api(input=JsonInput(), batch=False) 15 | def jsonapi(self, json: JsonSerializable): 16 | print(json) 17 | return json 18 | 19 | @api(input=FileInput(), batch=False) 20 | def fileapi(self, file_stream: FileLike): 21 | print(file_stream) 22 | if file_stream.bytes_ is not None: 23 | return file_stream.bytes_ 24 | else: 25 | return file_stream._stream.read() 26 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | import tempfile 5 | import time 6 | 7 | import requests 8 | from classifier import TestService 9 | from pandas import DataFrame 10 | 11 | sys.path.append("./") 12 | from delete import delete 13 | from deploy import deploy 14 | from describe import describe 15 | 16 | 17 | class Setup: 18 | def __init__(self): 19 | """ 20 | Setup the deployment on the deployment choosen 21 | """ 22 | self.deployment_name = "lambda_bento_deploy_test" 23 | self.dirpath = tempfile.mkdtemp() 24 | print("temp dir {} created!".format(self.dirpath)) 25 | self.saved_dir = os.path.join(self.dirpath, "saved_dir") 26 | 27 | # make config file 28 | config = """ 29 | { 30 | "region": "us-west-1", 31 | "timeout": 60, 32 | "memory_size": 1024 33 | } 34 | """ 35 | self.config_file = os.path.join(self.dirpath, "config.json") 36 | with open(self.config_file, "w") as f: 37 | f.write(config) 38 | 39 | # make bento service 40 | os.mkdir(self.saved_dir) 41 | test_service = TestService() 42 | # test_service.pack() 43 | test_service.save_to_dir(self.saved_dir) 44 | 45 | def make_deployment(self): 46 | deploy(self.saved_dir, self.deployment_name, self.config_file) 47 | info_json = describe(self.deployment_name, self.config_file) 48 | url = info_json["EndpointUrl"] + "/{}" 49 | 50 | # ping /healthz to check if deployment is up 51 | # attempt = 0 52 | # while attempt < 5: 53 | # time.sleep(10) 54 | # if urllib.request.urlopen(url.format("healthz")).status == 200: 55 | # break 56 | time.sleep(20) 57 | return url 58 | 59 | def teardown(self): 60 | delete(self.deployment_name, self.config_file) 61 | shutil.rmtree(self.dirpath) 62 | print("Removed {}!".format(self.dirpath)) 63 | 64 | 65 | def test_json(url): 66 | """ 67 | GIVEN the api is deployed 68 | WHEN a valid json is given 69 | THEN accepts the binary_data and returns it 70 | """ 71 | headers = {"content-type": "application/json"} 72 | input_json = "[[1, 2, 3, 4]]" 73 | resp = requests.post(url, data=input_json, headers=headers) 74 | assert resp.ok 75 | assert resp.content == bytearray(input_json, "ascii") 76 | 77 | 78 | def test_df(url): 79 | """ 80 | GIVEN the api is deployed 81 | WHEN a dataframe is passed, as json or csv 82 | THEN accepts the binary_data and returns it 83 | """ 84 | input_array = [[1, 2, 3, 4]] 85 | 86 | # request as json 87 | resp = requests.post(url, json=input_array) 88 | assert resp.ok 89 | assert DataFrame(resp.json()).to_json() == DataFrame(input_array).to_json() 90 | 91 | # request as csv 92 | headers = {"content-type": "text/csv"} 93 | csv = DataFrame(input_array).to_csv(index=False) 94 | resp = requests.post(url.format("dfapi"), data=csv, headers=headers) 95 | assert resp.ok 96 | assert DataFrame(resp.json()).to_json() == DataFrame(input_array).to_json() 97 | 98 | 99 | def test_files(url): 100 | """ 101 | GIVEN the api is deployed 102 | WHEN a file is passed either as raw bytes with any content-type or as mulitpart/form 103 | THEN it accepts the binary_data and returns it 104 | """ 105 | binary_data = b"test" 106 | 107 | # request with raw data 108 | headers = {"content-type": "image/jpeg"} 109 | resp = requests.post(url, data=binary_data, headers=headers) 110 | assert resp.ok 111 | assert resp.content == b'"test"' 112 | 113 | # request mulitpart/form-data 114 | file = {"audio": ("test", binary_data)} 115 | resp = requests.post(url.format("fileapi"), files=file) 116 | assert resp.ok 117 | assert resp.content == b'"test"' 118 | 119 | 120 | if __name__ == "__main__": 121 | 122 | setup = Setup() 123 | failed = False 124 | try: 125 | url = setup.make_deployment() 126 | except Exception as e: 127 | print("Setup failed") 128 | raise e 129 | else: 130 | # setup successful! 131 | print("Setup successful") 132 | 133 | # list of tests to perform 134 | TESTS = [(test_json, "jsonapi"), (test_df, "dfapi")] 135 | 136 | for test_func, endpoint in TESTS: 137 | try: 138 | print("Testing endpoint /{}...".format(endpoint), end="") 139 | test_func(url.format(endpoint)) 140 | print("\033[92m passed! \033[0m") 141 | except Exception as e: 142 | print("\033[91m failed! \033[0m") 143 | print("\nTest at endpoint /{} failded: ".format(endpoint), e) 144 | failed = True 145 | finally: 146 | setup.teardown() 147 | 148 | if failed: 149 | sys.exit(1) 150 | else: 151 | sys.exit(0) 152 | --------------------------------------------------------------------------------