The needs

This post describes how I’ve configured a Cloud Storage bucket to host a static assets (shared) for various websites across my domains. The aim is to have something like https://assets.prokop.dev to offer from single URL all assets, I might ever need to handle my WEB stuff.

Using Google Cloud Storage

The following is everything what was required to create fully operational, CDN powered, backed by object bucket asset distribution facility.

A new Google Cloud Project

I headed to Google Cloud Console and created a new project called “Web Static Content”.

I had to enable billing for this, even though I expect to get all handled within Free Tier allowance.

Cloud Storage Bucket

Navigate to Cloud Storage section of Google Cloud Console.

I have created bucked named assets.prokop.dev.

Naming bucket

I have selected South Carolina as single region for my data.

Select location

“Standard” for storage class is the most appropriate option for our use case.

Storage class

I’ve disabled “Enforce public access prevention on this bucket”. Access control is uniform as we want to allow access to all files.

Access Control

And I’ve chosen not to use any retention of data.

Storage class

Then just clicked “Create”.

As my Google account that I use for Google Cloud is managed by Google Workspace and the domain prokop.dev seems to be account secondary domain, Google did not prompt for domain validation.

Note that Cloud Storage Always Free quotas apply to usage in US-WEST1, US-CENTRAL1, and US-EAST1 regions. So, to get first 5 GB per month free, just create bucket in one of above regions.

Uploading and sharing some files

Finally, I’ve upload few files.

Files in Bucket

Then navigate to Permission tab and click “Grant Access”. You will need to add In the New principals field, enter allUsers.

In the Select a role drop down, select the Cloud Storage sub-menu, and click the Storage Object Viewer option.

Viewer Role

Confirm that you want to make access to bucket public:

Public Access

Check that everything works by trying the public URL to your data: https://storage.googleapis.com/assets.prokop.dev/index.html

Configure CDN

$ curl http://c.storage.googleapis.com/style.css -v -H "host: assets.prokop.dev"

> GET /style.css HTTP/1.1
> Host: assets.prokop.dev
> User-Agent: curl/7.83.0
> Accept: */*
>

< HTTP/1.1 200 OK
< X-GUploader-UploadID: ADPycdtmn2NkzHCjlqVAEthSS0a-2be67QuIStVgAcTcODbEFIaeKtsffXmBmNogIKkXoiuoxVsKWqc167c1I8n5gsGHqw
< x-goog-generation: 1692656828987613
< x-goog-metageneration: 1
< x-goog-stored-content-encoding: identity
< x-goog-stored-content-length: 3652
< x-goog-hash: crc32c=SfZ7qg==
< x-goog-hash: md5=gKrqkiiDXLVw+Fntq/B5Ng==
< x-goog-storage-class: STANDARD
< Accept-Ranges: bytes
< Content-Length: 3652
< Server: UploadServer
< Date: Mon, 21 Aug 2023 22:48:02 GMT
< Expires: Mon, 21 Aug 2023 23:48:02 GMT
< Cache-Control: public, max-age=3600
< Last-Modified: Mon, 21 Aug 2023 22:27:08 GMT
< ETag: "80aaea9228835cb570f859edabf07936"
< Content-Type: text/css
< Age: 2815

Add CNAME record c.storage.googleapis.com

Viewer Role

After:

$ curl https://assets.prokop.dev/style.css -v > /dev/null

> GET /style.css HTTP/2
> Host: assets.prokop.dev
> user-agent: curl/7.83.0
> accept: */*
>

< HTTP/2 200
< date: Mon, 21 Aug 2023 23:39:04 GMT
< content-type: text/css
< content-length: 3652
< x-guploader-uploadid: ADPycdtmn2NkzHCjlqVAEthSS0a-2be67QuIStVgAcTcODbEFIaeKtsffXmBmNogIKkXoiuoxVsKWqc167c1I8n5gsGHqw
< x-goog-generation: 1692656828987613
< x-goog-metageneration: 1
< x-goog-stored-content-encoding: identity
< x-goog-stored-content-length: 3652
< x-goog-hash: crc32c=SfZ7qg==
< x-goog-hash: md5=gKrqkiiDXLVw+Fntq/B5Ng==
< x-goog-storage-class: STANDARD
< expires: Mon, 21 Aug 2023 23:48:02 GMT
< cache-control: public, max-age=14400
< last-modified: Mon, 21 Aug 2023 22:27:08 GMT
< etag: "80aaea9228835cb570f859edabf07936"
< cf-cache-status: MISS
< accept-ranges: bytes
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=Q9AGp2BU4qhhXjSNqET%2BODPdIam5YRgYZob%2F8FWJ6EE3HyHKKHLJwGGqS%2FmKBrgYYm5OMCtoJZPOBVdQXkvrR2Uc0XR0eskxDRNFLNc%2BtPR7BO7%2FCz%2F6kcHXSsCsRGQKHWx1ZQ%3D%3D"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< strict-transport-security: max-age=31536000; includeSubDomains; preload
< x-content-type-options: nosniff
< server: cloudflare
< cf-ray: 7fa6b69a1a980763-MAN
< alt-svc: h3=":443"; ma=86400

Add Cors support

When trying to consume files on web page in another domain, the following error is shown in console (and browser reject to render resource).

Access to script at 'https://assets.prokop.dev/js/bootstrap/5.3.3/js/bootstrap.bundle.min.js' from origin 'https://fizjoterapia.uk' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
pre-appointment:198 
        
GET https://assets.prokop.dev/js/bootstrap/5.3.3/js/bootstrap.bundle.min.js net::ERR_FAILED 200 (OK)

Access to CSS stylesheet at 'https://assets.prokop.dev/js/bootstrap/5.3.3/css/bootstrap.min.css' from origin 'https://fizjoterapia.uk' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
        
GET https://assets.prokop.dev/js/bootstrap/5.3.3/css/bootstrap.min.css net::ERR_FAILED 200 (OK)

This can be fixed by configuring CORS on Google Storage Bucket. First the following file needs to be created.

[
    {
      "origin": [
        "https://fizjoterapia.uk",
        "https://www.srihash.org"
      ],
      "method": ["GET"],
      "responseHeader": ["Content-Type"],
      "maxAgeSeconds": 3600
    }
]

Then run: gcloud storage buckets update gs://assets.prokop.dev --cors-file=cors.config. And test.

curl https://assets.prokop.dev/js/bootstrap/5.3.3/css/bootstrap.css -v -H "Origin: https://fizjoterapia.uk" > /dev/null
> GET /js/bootstrap/5.3.3/css/bootstrap.css HTTP/2
> Host: assets.prokop.dev
> user-agent: curl/7.83.0
> accept: */*
> origin: https://fizjoterapia.uk
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
< HTTP/2 200
< date: Thu, 21 Mar 2024 23:30:43 GMT
< content-type: text/css
< content-length: 281046
< x-guploader-uploadid: ABPtcPrSlOwsjOUDBShOqZ2s2kX-i6cFnh_6GhisB1JVokDuDHrFSc9-To3vTmCE5Cb_DRz-FCnUT1m2oA
< expires: Fri, 22 Mar 2024 00:12:28 GMT
< cache-control: public, max-age=14400
< last-modified: Thu, 21 Mar 2024 20:20:11 GMT
< etag: "1162850e40492183d0df775907004258"
< x-goog-generation: 1711052411059453
< x-goog-metageneration: 1
< x-goog-stored-content-encoding: identity
< x-goog-stored-content-length: 281046
< x-goog-hash: crc32c=FS109g==
< x-goog-hash: md5=EWKFDkBJIYPQ33dZBwBCWA==
< x-goog-storage-class: STANDARD
< access-control-allow-origin: https://fizjoterapia.uk
< access-control-expose-headers: Content-Length, Content-Type, Date, Server, Transfer-Encoding, X-GUploader-UploadID, X-Google-Trace
< vary: Origin
< cf-cache-status: HIT
< age: 1095
< accept-ranges: bytes
< report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=XDPyKRJiQHCb%2FRbFCxhXxI6KJWML4MT30TaDCJC2f%2FqbMmpZW2JLrgZfiv612WcTCYqKfr7IU0Hw0o%2FI4pLNtZTHsTXIYcecbCDGxG48d8FZxCD9GTjaefhvu4LGlnWbu8gXXw%3D%3D"}],"group":"cf-nel","max_age":604800}
< nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
< strict-transport-security: max-age=31536000; includeSubDomains; preload
< x-content-type-options: nosniff
< server: cloudflare
< cf-ray: 8681bb40ecbe76c9-LHR
< alt-svc: h3=":443"; ma=86400

Resources and references