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
.
I have selected South Carolina as single region for my data.
“Standard” for storage class is the most appropriate option for our use case.
I’ve disabled “Enforce public access prevention on this bucket”. Access control is uniform as we want to allow access to all files.
And I’ve chosen not to use any retention of data.
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.
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.
Confirm that you want to make access to bucket public:
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
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