I have a simple flask application. And I need to run it on Cloud Run with enabled option "Manage authorized users with Cloud IAM."
app.py
from flask import Flask
api_app = Flask(__name__)
endpoints.py
from app import api_app
@api_app.route("/create", methods=["POST"])
def api_create():
# logic
main.py
from app import api_app
from endpoints import *
if __name__ == "__main__":
api_app.run(host="0.0.0.0", port=8080)
And it works if i run it locally. It also works well if run in docker. When I upload the application image to Cloud Run, there are no deployment errors. But when I try to call the endpoint I get an error even though everything is working fine locally.
Request example:
import urllib
import google.auth.transport.requests
import google.oauth2.id_token
import requests
import os
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "test.json"
audience="https://test-service-*****.a.run.app"
req = urllib.request.Request(audience)
auth_req = google.auth.transport.requests.Request()
token = google.oauth2.id_token.fetch_id_token(auth_req, audience)
response = requests.post(f"{audience}/create", data={
"text": "cool text"
}, headers={
"Authorization": f"Bearer {token}"
})
Response:
<!doctype html>
<html lang=en>
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The browser (or proxy) sent a request that this server could not understand.</p>
In Cloud Run logs I have only one warning log
POST 400 820b 3ms python-requests/2.26.0 https://test-service-*****.a.run.app/create
Dockerfile:
FROM python:3.8-slim-buster
WORKDIR /alpha
ENV PYTHONUNBUFFERED True
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD exec gunicorn --bind :8080 --workers 1 --threads 8 --timeout 0 main:api_app
Why can't I reach the application endpoint in Cloud Run?
CodePudding user response:
So I did some testing, and the only thing I did was remove the port binding from the Dockerfile CMD exec gunicorn
and from the main.py
. Note that the dundermain thingy is not needed as gunicorn takes care of that.
After that it worked as expected.
Note that I did not set it up as a private endpoint as I was to lazy to jump through the hoops for that, and the response code you get back is not a 403.
Note that I also made sure that /create
returned something. I'm going to assume that you did the same.
EDIT
I've made the CR endpoint private, and created a serviceaccount with the role "Cloud Run Invoker", created a JSON key and saved it as test.json
I've also added some logic to print out the token, and to decode the first part. Your output should look similar to mine.
EDIT 2
In the end it ended up Flask not being able to deal with the incoming data. There's a very nice answer here which explains how to get the data from all the different ways it can get POSTed
FROM python:3.8-slim-buster
WORKDIR /alpha
ENV PYTHONUNBUFFERED True
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD exec gunicorn --workers 1 --threads 8 --timeout 0 main:api_app
endpoints.py
from app import api_app
from flask import request
@api_app.route("/create", methods=["POST"])
def api_create():
data = request.data
return f"{data}"
main.py
from app import api_app
from endpoints import *
perform_request.py
import urllib
import requests
import google.auth.transport.requests
import google.oauth2.id_token
import os
import base64
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "test.json"
target_uri = "https://pasta*****.a.run.app"
req = urllib.request.Request(target_uri)
auth_req = google.auth.transport.requests.Request()
token = google.oauth2.id_token.fetch_id_token(auth_req, target_uri)
print(token, "\n")
print(base64.b64decode(f"""{token.split(".")[0]}==""".encode("utf-8")), "\n")
response = requests.post(
f"{target_uri}/create",
data={"text": "cool text"},
headers={"Authorization": f"Bearer {token}"},
)
print(response.text)
output
eyJhbGciOiJSUzI1NiIsIm[SNIPPED]
b'{"alg":"RS256","kid":"713fd68c9[SNIPPED]","typ":"JWT"}'
b'text=cool text'