Skip to content

Commit

Permalink
New post
Browse files Browse the repository at this point in the history
  • Loading branch information
valeriangalliat committed May 6, 2024
1 parent 4937787 commit 8a95651
Show file tree
Hide file tree
Showing 4 changed files with 339 additions and 185 deletions.
218 changes: 218 additions & 0 deletions 2024/05/google-chrome-cloud-functions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Using Google Chrome instead of Chromium in Google Cloud Functions</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>document.documentElement.classList.add(localStorage.theme || ((matchMedia && matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'))</script>
<link rel="stylesheet" href="/css/normalize.css">
<link rel="stylesheet" href="/css/github-20220617.css">
<link rel="stylesheet" href="/css/main-20230317.css">
<link rel="icon" href="/favicon.ico" data-emoji="🏕️">
<link rel="alternate" href="/feed.xml" type="application/atom+xml" title="CodeJam">
<meta property="og:title" content="Using Google Chrome instead of Chromium in Google Cloud Functions">
<meta property="og:description" content="When using Puppeteer, Playwright and similar, you need to have Chrome installed. When you’re running on AWS Lambda or Google Cloud Functions, it can get tricky.">
<meta property="og:image" content="https://www.codejam.info/img/profile.jpg">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@valeriangalliat">
<meta name="twitter:creator" content="@valeriangalliat">
</head>
<body class="post">
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/val.html">About</a></li>
<li><a href="https://photography.codejam.info/">Photography</a></li>
<li class="pull"><button title="Toggle theme" class="change-color-theme">🌝</button></li>
</ul>
</nav>
<header>
<div class="header-inner">
<div class="content">
<h1>Using Google Chrome instead of Chromium in Google Cloud Functions</h1>
<p class="date">May 5, 2024</p>
</div>
</div>
</header>
<div class="content">
<p>When using Puppeteer, Playwright and similar, you need to have Chrome
installed. When you’re running on AWS Lambda or Google Cloud Functions,
it can get tricky.</p>
<p>Google Cloud Functions <em>used to</em> bundle Chromium in their base images,
but it’s been a few years it’s no longer the case. That’s where packages
like <a href="https://github.com/alixaxel/chrome-aws-lambda"><code>chrome-aws-lambda</code></a>
come in handy, by bundling Chromium directly inside a npm package, and
exposing a function that extracts the Chromium binary and returns the
path:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> chromium = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;chrome-aws-lambda&#x27;</span>)

<span class="hljs-keyword">const</span> path = <span class="hljs-keyword">await</span> chromium.<span class="hljs-property">executablePath</span>
</code></pre>
<div class="note">
<p><strong>Note:</strong> unnecessary pedantic detail: the above code doesn’t look like a function,
but <a href="https://github.com/alixaxel/chrome-aws-lambda/blob/f9d5a9ff0282ef8e172a29d6d077efc468ca3c76/source/index.ts#L147">it is, in fact</a>,
a getter function that returns a promise. 😄</p>
</div>
<p>However that’s Chromium, and you may have reasons to want Google Chrome
instead (mainly, proprietary codecs).</p>
<h2 id="a-totally-unrelated-note-about-aws-lambda" tabindex="-1"><a class="header-anchor" href="#a-totally-unrelated-note-about-aws-lambda"><span>A totally unrelated note about AWS Lambda</span></a></h2>
<p>This article is about Google Cloud Functions, but if you’re on AWS
Lambda, the above option is your best bet. Because of the Lambda total
size limit of 250 MB (all layers combined), it’s really hard to get a
binary of Chrome that fits in there.</p>
<p>That’s why <code>chrome-aws-lambda</code> uses <a href="https://github.com/alixaxel/lambdafs">LambdaFS</a>
under the hood, to aggressively compress the Chrome installation with
Brotli and make it fit in that limited space.</p>
<p>But again with that build, you won’t have proprietary codecs. I tried to
trim down a Chrome Linux build and compress it with the same technique
but never managed to make it fit on AWS Lambda. Recent Chrome versions
are just too big.</p>
<p>There’s another option, which is to compile Chromium yourself with
proprietary codecs. I never found any prebuilt binaries of Chromium that
include proprietary codecs (maybe because of license issues
redistributing them 🙃) so you’re on your own here.</p>
<p><a href="https://www.remotion.dev/">Remotion</a> successfully does that for
<a href="https://www.remotion.dev/docs/lambda">Remotion Lambda</a>.
Here’s <a href="https://github.com/remotion-dev/chrome-build-instructions">their instructions</a>
to compile Chromium with proprietary codecs for Lambda.</p>
<p>Fair warning: it gets hairy, fast.</p>
<h2 id="back-to-google-cloud-functions" tabindex="-1"><a class="header-anchor" href="#back-to-google-cloud-functions"><span>Back to Google Cloud Functions</span></a></h2>
<p>Google Cloud Functions is more generous as for bundle size, so we don’t
need to resort to those tricks, and we can include a complete,
uncompressed, Google Chrome installation.</p>
<p>Google publishes <a href="https://googlechromelabs.github.io/chrome-for-testing/">Chrome for Testing</a>,
builds <a href="https://developer.chrome.com/blog/chrome-for-testing">specifically made</a>
for headless usage.</p>
<p>We can just download the latest build from there as part of the
<code>gcp-build</code> script in our <code>package.json</code>.</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
<span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">&quot;gcp-build&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;curl -s -O &#x27;https://storage.googleapis.com/chrome-for-testing-public/124.0.6367.91/linux64/chrome-linux64.zip&#x27; &amp;&amp; unzip chrome-linux64.zip &amp;&amp; rm chrome-linux64.zip&quot;</span>
<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<div class="note">
<p><strong>Note:</strong> the <code>gcp-build</code> script allows you to <a href="https://cloud.google.com/appengine/docs/standard/nodejs/running-custom-build-step">run a custom build step</a>
in Google Cloud Build, which is what Cloud Functions (both 1st and 2nd
gen, as well as Cloud Run and App Engine) use to build your function
image.</p>
<p>It would work just fine with a <code>postinstall</code> script as well, but
<code>gcp-build</code> makes sure you run it only on Google Cloud Build, which is
probably desirable in this particular case.</p>
</div>
<p>You will then have the Chrome binary in <code>chrome-linux64/chrome</code>, that
you can pass to the tool of your choice.</p>
<h2 id="with-puppeteer" tabindex="-1"><a class="header-anchor" href="#with-puppeteer"><span>With Puppeteer</span></a></h2>
<p>Courtesy of <a href="https://medium.com/@jackklpan/run-puppeteer-in-google-cloud-functions-v2-b18a353e609b">this post</a>,
with Puppeteer, you don’t need to download Chrome manually, since it
provides a nifty script to do just that.</p>
<p>Actually, Puppeteer’s <a href="https://github.com/puppeteer/puppeteer/blob/f23646b3526aa87145c17b22e9967ec8f77d82d2/packages/puppeteer/package.json#L41"><code>postinstall</code> script</a>
automatically downloads the latest version of Chrome for Testing for
your platform.</p>
<p>The caveat is that this script by default installs it to
<code>~/.cache/puppeteer</code>, which in the case of Google Cloud Build, is not
gonna be preserved in the final image. So we need to instruct Puppeteer
to install Chrome in a directory that Cloud Build will keep.</p>
<p>This can be done with the following <code>.puppeteerrc.js</code>:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = {
<span class="hljs-attr">cacheDirectory</span>: <span class="hljs-string">`<span class="hljs-subst">${__dirname}</span>/.cache/puppeteer`</span>
}
</code></pre>
<p>But even then, there’s another caveat. Puppeteer’s <code>postinstall</code> script
will only run after it gets installed. However, because of build
caching, you will get in a state where <code>node_modules</code> is restored, with
Puppeteer already installed (so <code>postinstall</code> will <em>not</em> run), but the
<code>.cache/puppeteer</code> directory will also <em>not</em> be restored.</p>
<p>To mitigate that, we need to make sure to install Chrome systematically.
Again we can leverage the <code>gcp-build</code> for that:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
<span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">&quot;gcp-build&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;npx puppeteer browsers install chrome&quot;</span>
<span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<div class="note">
<p><strong>Note:</strong> you could call Puppeteer’s <code>postinstall</code> script directly by
doing <code>node node_modules/puppeteer/install.mjs</code> instead, but I found the
above command cleaner.</p>
</div>
<p>The good thing is that this script knows to not re-download Chrome if
it’s already found in the cache directory, so when the <code>postinstall</code>
script <em>does</em> run, the extra <code>gcp-build</code> command will be a no-op.</p>
<section class="post-footer">
<h3>Want to leave a comment?</h3>
<p>
Start a conversation on <a href="https://twitter.com/valeriangalliat">Twitter</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
</p>
</section>
</div>
<footer>
<div class="footer-inner">
<figure>
<a href="/val.html">
<img alt="The author (Val) in front of a mountain" src="/img/profile.jpg">
</a>
</figure>
<div class="footer-content">
<p>
Made with 🧡 by <a href="/val.html">Val</a>.
Generated from a <abbr title="Pull requests welcome!"><a href="https://github.com/valeriangalliat/blog/tree/master/2024/05/google-chrome-cloud-functions.md">public repository</a></abbr>.
</p>
<ul class="social">
<li>
<a href="https://twitter.com/valeriangalliat">
<svg width="16" height="16" viewBox="0 0 16 16">
<title>Twitter</title>
<use href="/img/icons/407-twitter.svg#icon"></use>
</svg>
</a>
</li>
<li>
<a href="https://github.com/valeriangalliat">
<svg width="16" height="16" viewBox="0 0 16 16">
<title>GitHub</title>
<use href="/img/icons/433-github.svg#icon"></use>
</svg>
</a>
</li>
<li>
<a href="https://www.instagram.com/funkyval_/">
<svg width="16" height="16" viewBox="0 0 16 16">
<title>Instagram</title>
<use href="/img/icons/403-instagram.svg#icon"></use>
</svg>
</a>
</li>
<li>
<a href="https://www.youtube.com/@FunkyVal">
<svg width="16" height="16" viewBox="0 0 16 16">
<title>YouTube</title>
<use href="/img/icons/414-youtube.svg#icon"></use>
</svg>
</a>
</li>
<li>
<a href="https://ko-fi.com/funkyval">
<svg width="16" height="16" viewBox="0 0 16 16">
<title>Buy me a coffee!</title>
<use href="/img/icons/219-heart.svg#icon"></use>
</svg>
</a>
</li>
<li>
<a href="/feed.xml">
<svg width="16" height="16" viewBox="0 0 16 16">
<title>RSS</title>
<use href="/img/icons/412-rss.svg#icon"></use>
</svg>
</a>
</li>
</ul>
</div>
</div>
</footer>
<script src="/js/emojicon.js"></script>
<script src="/js/main-20230317.js"></script>
</body>
</html>
Loading

0 comments on commit 8a95651

Please sign in to comment.