Securing Cloud Run services tutorial
Stay organized with collections
Save and categorize content based on your preferences.
This tutorial walks through how to create a secure two-service application
running on Cloud Run. This application is a
Markdown editor which
includes a public "frontend" service which anyone can use to compose markdown
text, and a private "backend" service which renders Markdown text to HTML.
Diagram showing the request flow from the frontend 'editor' to the backend 'renderer'.
The "Renderer" backend is a private service. This allows guaranteeing a text
transformation standard across an organization without tracking changes across
libraries in multiple languages.
To generate a cost estimate based on your projected usage,
use the pricing calculator.
New Google Cloud users might be eligible for a free trial.
Before you begin
Sign in to your Google Cloud account. If you're new to
Google Cloud,
create an account to evaluate how our products perform in
real-world scenarios. New customers also get 300ドル in free credits to
run, test, and deploy workloads.
In the Google Cloud console, on the project selector page,
select or create a Google Cloud project.
Roles required to select or create a project
Select a project: Selecting a project doesn't require a specific
IAM role—you can select any project that you've been
granted a role on.
Create a project: To create a project, you need the Project Creator role
(roles/resourcemanager.projectCreator), which contains the
resourcemanager.projects.create permission. Learn how to grant
roles.
In the Google Cloud console, on the project selector page,
select or create a Google Cloud project.
Roles required to select or create a project
Select a project: Selecting a project doesn't require a specific
IAM role—you can select any project that you've been
granted a role on.
Create a project: To create a project, you need the Project Creator role
(roles/resourcemanager.projectCreator), which contains the
resourcemanager.projects.create permission. Learn how to grant
roles.
To enable APIs, you need the Service Usage Admin IAM
role (roles/serviceusage.serviceUsageAdmin), which
contains the serviceusage.services.enable permission. Learn how to grant
roles.
To configure gcloud with defaults for your Cloud Run service:
Set your default project:
gcloudconfigsetprojectPROJECT_ID
Replace PROJECT_ID with the name of the project you created for
this tutorial.
Configure gcloud for your chosen region:
gcloudconfigsetrun/regionREGION
Replace REGION with the supported Cloud Run
region
of your choice.
Cloud Run locations
Cloud Run is regional, which means the infrastructure that
runs your Cloud Run services is located in a specific region and is
managed by Google to be redundantly available across
all the zones within that region.
Meeting your latency, availability, or durability requirements are primary
factors for selecting the region where your Cloud Run services are run.
You can generally select the region nearest to your users but you should consider
the location of the other Google Cloud
products that are used by your Cloud Run service.
Using Google Cloud products together across multiple locations can affect
your service's latency as well as cost.
Change to the directory that contains the Cloud Run sample
code:
Node.js
cdnodejs-docs-samples/run/markdown-preview/
Python
cdpython-docs-samples/run/markdown-preview/
Go
cdgolang-samples/run/markdown-preview/
Java
cdjava-docs-samples/run/markdown-preview/
C#
cddotnet-docs-samples/run/markdown-preview/
Reviewing the private Markdown rendering service
From the perspective of the frontend there is a simple API specification for
the Markdown service:
One endpoint at /
Expects POST requests
The body of the POST request is Markdown text
You may want to review all of the code for any security concerns or just to
learn more about it by exploring the ./renderer/ directory. Note that the
tutorial does not explain the Markdown transformation code.
Shipping the private Markdown rendering service
To ship your code, build with Cloud Build, upload to
Artifact Registry, and deploy to Cloud Run:
Where PROJECT_ID is your Google Cloud project ID, and renderer is the
name you want to give your service.
Upon success, you will see a SUCCESS message containing the ID, creation
time, and image name. The image is stored in Artifact Registry and can be
reused if desired.
Where PROJECT_ID is your Google Cloud project ID, and renderer is the
name you want to give your service.
Upon success, you will see a SUCCESS message containing the ID, creation
time, and image name. The image is stored in Artifact Registry and can be
reused if desired.
Where PROJECT_ID is your Google Cloud project ID, and renderer is the
name you want to give your service.
Upon success, you will see a SUCCESS message containing the ID, creation
time, and image name. The image is stored in Artifact Registry and can be
re-used if desired.
Java
This sample uses Jib to build
Docker images using common Java tools. Jib optimizes container builds without
the need for a Dockerfile or having Docker
installed. Learn more about building Java containers with Jib.
Where PROJECT_ID is your Google Cloud project ID, and renderer is the
name you want to give your service.
Upon success, you will see a SUCCESS message containing the ID, creation
time, and image name. The image is stored in Artifact Registry and can be
re-used if desired.
Deploy as a private service with restricted access.
Cloud Run provides out-of-the-box access control
and service identity features. Access control provides an
authentication layer that restricts users and other services from invoking the
service. Service identity allows restricting your service from accessing other
Google Cloud resources by creating a dedicated service account with limited
permissions.
Create a service account to serve as the "compute identity" of the render
service. By default this has no privileges other than project membership.
resource"google_cloud_run_v2_service""renderer"{name="renderer"location="us-central1"deletion_protection=false # set to "true" in productiontemplate{containers{ # Replace with the URL of your Secure Services > Renderer image. # gcr.io/<PROJECT_ID>/rendererimage="us-docker.pkg.dev/cloudrun/container/hello"}service_account=google_service_account.renderer.email}}
Trying out the private Markdown rendering service
Private services cannot be directly loaded by a web browser. Instead, use curl
or a similar HTTP request CLI tool that allows injecting an Authorization header.
To send some bold text to the service and see it convert the markdown asterisks to
HTML <strong> tags:
Get the URL from the deployment output.
Use gcloud to derive a special development-only identity token for authentication:
TOKEN=$(gcloudauthprint-identity-token)
Create a curl request that passes the raw Markdown text as a URL-escaped query string parameter:
Replace SERVICE_URL with the URL provided after deploying the
Markdown rendering service.
The response should be an HTML snippet:
<strong>Hello Bold Text</strong>
Reviewing the integration between editor and rendering services
The editor service provides a simple text-entry UI and a space to see the HTML
preview. Before continuing, review the code retrieved earlier by opening the
./editor/ directory.
Next, explore the following few sections of code that securely
integrates the two services.
Node.js
The render.js module creates authenticated requests to the private renderer
service. It uses the Google Cloud metadata server in the
Cloud Run environment to create an identity token and add
it to the HTTP request as part of an Authorization header.
const{GoogleAuth}=require('google-auth-library');constgot=require('got');constauth=newGoogleAuth();letclient,serviceUrl;// renderRequest creates a new HTTP request with IAM ID Token credential.// This token is automatically handled by private Cloud Run (fully managed) and Cloud Functions.constrenderRequest=asyncmarkdown=>{if(!process.env.EDITOR_UPSTREAM_RENDER_URL)throwError('EDITOR_UPSTREAM_RENDER_URL needs to be set.');serviceUrl=process.env.EDITOR_UPSTREAM_RENDER_URL;// Build the request to the Renderer receiving service.constserviceRequestOptions={method:'POST',headers:{'Content-Type':'text/plain',},body:markdown,timeout:3000,};try{// Create a Google Auth client with the Renderer service url as the target audience.if(!client)client=awaitauth.getIdTokenClient(serviceUrl);// Fetch the client request headers and add them to the service request headers.// The client request headers include an ID token that authenticates the request.constclientHeaders=awaitclient.getRequestHeaders();serviceRequestOptions.headers['Authorization']=clientHeaders['Authorization'];}catch(err){throwError('could not create an identity token: '+err.message);}try{// serviceResponse converts the Markdown plaintext to HTML.constserviceResponse=awaitgot(serviceUrl,serviceRequestOptions);returnserviceResponse.body;}catch(err){throwError('request to rendering service failed: '+err.message);}};
Parse the markdown from JSON and send it to the Renderer service to be
transformed into HTML.
The new_request method creates authenticated requests to private services.
It uses the Google Cloud metadata server in the Cloud Run
environment to create an identity token and add it to the HTTP request
as part of an Authorization header.
In other environments, new_request requests an identity token from Google's
servers by authenticating with
Application Default Credentials.
importosimporturllibimportgoogle.auth.transport.requestsimportgoogle.oauth2.id_tokendefnew_request(data):"""Creates a new HTTP request with IAM ID Token credential. This token is automatically handled by private Cloud Run and Cloud Functions. Args: data: data for the authenticated request Returns: The response from the HTTP request """url=os.environ.get("EDITOR_UPSTREAM_RENDER_URL")ifnoturl:raiseException("EDITOR_UPSTREAM_RENDER_URL missing")req=urllib.request.Request(url,data=data.encode())auth_req=google.auth.transport.requests.Request()target_audience=urlid_token=google.oauth2.id_token.fetch_id_token(auth_req,target_audience)req.add_header("Authorization",f"Bearer {id_token}")response=urllib.request.urlopen(req)returnresponse.read()
Parse the markdown from JSON and send it to the Renderer service to be
transformed into HTML.
@app.route("/render",methods=["POST"])defrender_handler():"""Parse the markdown from JSON and send it to the Renderer service to be transformed into HTML. """body=request.get_json(silent=True)ifnotbody:return"Error rendering markdown: Invalid JSON",400data=body["data"]try:parsed_markdown=render.new_request(data)returnparsed_markdown,200exceptExceptionaserr:returnf"Error rendering markdown: {err}",500
Go
RenderService creates authenticated requests to private services. It uses the Google Cloud metadata server in the Cloud Run environment to
create an identity token and add it to the HTTP request as part of an
Authorization header.
In other environments, RenderService requests an identity token from Google's
servers by authenticating with
Application Default Credentials.
import("bytes""context""fmt""io""net/http""time""golang.org/x/oauth2""google.golang.org/api/idtoken")// RenderService represents our upstream render service.typeRenderServicestruct{// URL is the render service address.URLstring// tokenSource provides an identity token for requests to the Render Service.tokenSourceoauth2.TokenSource}// NewRequest creates a new HTTP request to the Render service.// If authentication is enabled, an Identity Token is created and added.func(s*RenderService)NewRequest(methodstring)(*http.Request,error){req,err:=http.NewRequest(method,s.URL,nil)iferr!=nil{returnnil,fmt.Errorf("http.NewRequest: %w",err)}ctx,cancel:=context.WithTimeout(context.Background(),30*time.Second)defercancel()// Create a TokenSource if none exists.ifs.tokenSource==nil{s.tokenSource,err=idtoken.NewTokenSource(ctx,s.URL)iferr!=nil{returnnil,fmt.Errorf("idtoken.NewTokenSource: %w",err)}}// Retrieve an identity token. Will reuse tokens until refresh needed.token,err:=s.tokenSource.Token()iferr!=nil{returnnil,fmt.Errorf("TokenSource.Token: %w",err)}token.SetAuthHeader(req)returnreq,nil}
The request is sent to the Renderer service after adding the markdown
text to be transformed into HTML. Response errors are handled to differentiate
communication problems from rendering functionality.
varrenderClient=&http.Client{Timeout:30*time.Second}// Render converts the Markdown plaintext to HTML.func(s*RenderService)Render(in[]byte)([]byte,error){req,err:=s.NewRequest(http.MethodPost)iferr!=nil{returnnil,fmt.Errorf("RenderService.NewRequest: %w",err)}req.Body=io.NopCloser(bytes.NewReader(in))deferreq.Body.Close()resp,err:=renderClient.Do(req)iferr!=nil{returnnil,fmt.Errorf("http.Client.Do: %w",err)}out,err:=io.ReadAll(resp.Body)iferr!=nil{returnnil,fmt.Errorf("ioutil.ReadAll: %w",err)}ifresp.StatusCode!=http.StatusOK{returnout,fmt.Errorf("http.Client.Do: %s (%d): request not OK",http.StatusText(resp.StatusCode),resp.StatusCode)}returnout,nil}
Java
makeAuthenticatedRequest creates authenticated requests to private
services. It uses the Google Cloud metadata server in the
Cloud Run environment to create an identity token and add it to the
HTTP request as part of an Authorization header.
In other environments, makeAuthenticatedRequest requests an identity token
from Google's servers by authenticating with
Application Default Credentials.
// makeAuthenticatedRequest creates a new HTTP request authenticated by a JSON Web Tokens (JWT)// retrievd from Application Default Credentials.publicStringmakeAuthenticatedRequest(Stringurl,Stringmarkdown){Stringhtml="";try{// Retrieve Application Default CredentialsGoogleCredentialscredentials=GoogleCredentials.getApplicationDefault();IdTokenCredentialstokenCredentials=IdTokenCredentials.newBuilder().setIdTokenProvider((IdTokenProvider)credentials).setTargetAudience(url).build();// Create an ID tokenStringtoken=tokenCredentials.refreshAccessToken().getTokenValue();// Instantiate HTTP requestMediaTypecontentType=MediaType.get("text/plain; charset=utf-8");okhttp3.RequestBodybody=okhttp3.RequestBody.create(markdown,contentType);Requestrequest=newRequest.Builder().url(url).addHeader("Authorization","Bearer "+token).post(body).build();Responseresponse=ok.newCall(request).execute();html=response.body().string();}catch(IOExceptione){logger.error("Unable to get rendered data",e);}returnhtml;}
Parse the markdown from JSON and send it to the Renderer service to be
transformed into HTML.
// '/render' expects a JSON body payload with a 'data' property holding plain text// for rendering.@PostMapping(value="/render",consumes="application/json")publicStringrender(@RequestBodyDatadata){Stringmarkdown=data.getData();Stringurl=System.getenv("EDITOR_UPSTREAM_RENDER_URL");if(url==null){Stringmsg="No configuration for upstream render service: "+"add EDITOR_UPSTREAM_RENDER_URL environment variable";logger.error(msg);thrownewIllegalStateException(msg);}Stringhtml=makeAuthenticatedRequest(url,markdown);returnhtml;}
C#
GetAuthenticatedPostResponse creates authenticated requests to private
services. It uses the Google Cloud metadata server in the
Cloud Run environment to create an identity token and add it to the
HTTP request as part of an Authorization header.
In other environments, GetAuthenticatedPostResponse requests an identity token
from Google's servers by authenticating with
Application Default Credentials.
privateasyncTask<string>GetAuthenticatedPostResponse(stringurl,stringpostBody){// Get the OIDC access token from the service account via Application Default CredentialsGoogleCredentialcredential=awaitGoogleCredential.GetApplicationDefaultAsync();OidcTokentoken=awaitcredential.GetOidcTokenAsync(OidcTokenOptions.FromTargetAudience(url));stringaccessToken=awaittoken.GetAccessTokenAsync();// Create request to the upstream service with the generated OAuth access token in the Authorization headervarupstreamRequest=newHttpRequestMessage(HttpMethod.Post,url);upstreamRequest.Headers.Authorization=newAuthenticationHeaderValue("Bearer",accessToken);upstreamRequest.Content=newStringContent(postBody);varupstreamResponse=await_httpClient.SendAsync(upstreamRequest);upstreamResponse.EnsureSuccessStatusCode();returnawaitupstreamResponse.Content.ReadAsStringAsync();}
Parse the markdown from JSON and send it to the Renderer service to be
transformed into HTML.
Where PROJECT_ID is your Google Cloud project ID, and editor is the
name you want to give your service.
Upon success, you will see a SUCCESS message containing the ID, creation
time, and image name. The image is stored in Container Registry and can be
re-used if desired.
Where PROJECT_ID is your Google Cloud project ID, and editor is the
name you want to give your service.
Upon success, you will see a SUCCESS message containing the ID, creation
time, and image name. The image is stored in Artifact Registry and can be
re-used if desired.
Where PROJECT_ID is your Google Cloud project ID, and editor is the
name you want to give your service.
Upon success, you will see a SUCCESS message containing the ID, creation
time, and image name. The image is stored in Artifact Registry and can be
re-used if desired.
Java
This sample uses Jib to build
Docker images using common Java tools. Jib optimizes container builds without
the need for a Dockerfile or having Docker
installed. Learn more about building Java containers with Jib.
Where PROJECT_ID is your Google Cloud project ID, and editor is the
name you want to give your service.
Upon success, you will see a SUCCESS message containing the ID, creation
time, and image name. The image is stored in Artifact Registry and can be
re-used if desired.
Deploy as a private service with special access to the rendering service.
Create a service account to serve as the "compute identity" of the private
service. By default this has no privileges other than project membership.
resource"google_service_account""editor"{account_id="editor-identity"display_name="Service identity of the Editor (Frontend) service."}
The Editor service does not need to interact with anything else in Google Cloud other than the Markdown rendering service.
Grant access to the editor-identity compute identity to invoke the Markdown
rendering service. Any service which uses this as a compute identity will
have this privilege.
Because this is given the invoker role in the context of the render service,
the render service is the only private Cloud Run service the editor
can invoke.
Deploy with the editor-identity service account and allow public,
unauthenticated access.
resource"google_cloud_run_v2_service""editor"{name="editor"location="us-central1"deletion_protection=false # set to "true" in productiontemplate{containers{ # Replace with the URL of your Secure Services > Editor image. # gcr.io/<PROJECT_ID>/editorimage="us-docker.pkg.dev/cloudrun/container/hello"env{name="EDITOR_UPSTREAM_RENDER_URL"value=google_cloud_run_v2_service.renderer.uri}}service_account=google_service_account.editor.email}}
There are three HTTP requests involved in rendering markdown using these services.
Diagram showing the request flow from the user to the editor, editor to get a token from Metadata server, editor to make request to render service, render service to return HTML to editor.
The frontend service with the editor-identity invokes the
render service. Both editor-identity and
renderer-identity have limited permissions so any security
exploit or code injection has limited access to other Google Cloud resources.
Trying it out
To try out the complete two-service application:
Navigate your browser to the URL provided by the deployment step above.
Try editing the Markdown text on the left and click the button to see it
preview on the right.
It should look like this:
Screenshot of the Markdown Editor User Interface
If you choose to continue developing these services, remember that they have
restricted Identity and Access Management (IAM) access to the rest of Google Cloud and
will need to be given additional IAM roles to access many other
services.
[[["Easy to understand","easyToUnderstand","thumb-up"],["Solved my problem","solvedMyProblem","thumb-up"],["Other","otherUp","thumb-up"]],[["Hard to understand","hardToUnderstand","thumb-down"],["Incorrect information or sample code","incorrectInformationOrSampleCode","thumb-down"],["Missing the information/samples I need","missingTheInformationSamplesINeed","thumb-down"],["Other","otherDown","thumb-down"]],["Last updated 2025年11月24日 UTC."],[],[]]