Today, I had the pleasure of trying to configure active storage with azure beyond its normal very restricted capability. This took a lot of debugging so I felt the need to write about it so that I remember how to do this in the future.

However, I have put in a pull request (https://github.com/rails/rails/pull/35096) into the activestorage project as the change that I have made is both simple and consistent with the Amazon S3 activestorage service which is built in to active storage.

The Problem

Normally, in the active storage documentation (https://edgeguides.rubyonrails.org/active_storage_overview.html#microsoft-azure-storage-service), it tells you to add the following into config/storage.yml.

azure:
service: AzureStorage
storage_account_name: ""
storage_access_key: ""
container: ""

Now that works absolutely fine – no need for any changes. But if, like me, you like to run your systems locally for development and you don’t use windows – so you can’t use Microsoft’s emulator – then you can use projects such as ‘Azurite’ (https://www.npmjs.com/package/azurite) which is an azure storage emulator written in NodeJS.

But, how can you re configure activestorage to use this ? it always goes to the microsoft endpoints as you would expect.

If you then were to dig in to the ‘azure-storage’ gem (https://github.com/azure/azure-storage-ruby) – you would find client options such as ‘storage_blob_host’ and ‘use_path_style_uri’.

If you have used active storage before with Amazon S3 for example, you will know that you can add these extra options directly into your config/storage.yml – but not with the azure adapter – you will get the following error :

ArgumentError:
unknown keywords: use_path_style_uri, storage_blob_host

The Cause Of The Problem

Take a look inside https://github.com/rails/rails/blob/v5.2.2/activestorage/lib/active_storage/service/azure_storage_service.rb#L13 and you will see the following code

def initialize(storage_account_name:, storage_access_key:, container🙂
@client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
@signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
@blobs = client.blob_client
@container = container
end

As you can see, the initialize method only accepts storage_account_name, storage_access_key and container – nothing else. This method is called with the options that come from the storage.yml file – so it is such a shame that it prevents you from passing extra options to the client.

My pull request will negate the need for any of these changes in your application, but my guess is that as you are reading this, you have googled for an answer and found this – so my PR either isn’t in or you are using active storage 5.2.2 (part of rails 5.2.2) or below – which has this restriction.

The Fix(es)

Personally, I hate “monkey patching” – but in this case, it is the neatest solution – and I feel better about it because I have put in a fix into activestorage as a PR and as it brings the azure service in line with the Amazon S3 and probably Google services within active storage – then it is likely to be accepted.

But, for those of you who dont want to monkey patch, I also started off with a change to your application.rb which achieves the same thing, but in a slightly uglier way.

Using Rails Initializers

Add the following code to config/initializers/active_storage.rb

require 'active_storage/service/azure_storage_service'
module ActiveStorage
class Service::AzureStorageService < Service
def initialize(storage_account_name:, storage_access_key:, container:, **client_options)
@client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **client_options)
@signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
@blobs = client.blob_client
@container = container
end
end
end

As mentioned, it is a monkey patch – and the code inside the initialize method comes from version 5.2.2. I often add a check into any monkey patch that I decide to let into my project – so that as I upgrade in the future, it raises an error if the version is not 5.2.2 – this forces me to check to see if the monkey patch is still required. However, in this case, I chose to not show this as that is personal preference and not really part of the solution – but you should consider something that makes sure you check this code as you upgrade as the ‘original’ initialize method might have changed.

Using Your application.rb

Add the following code to config/application.rb

initializer "reconfigure.active_storage", after: 'active_storage.services' do
next unless
ENV.keys.any? {|k| k.start_with?('AZURE_STORAGE')}
client = ActiveStorage::Blob.service.client
options = client.options.slice(:storage_account_name, :storage_access_key)
options.merge! <add your extra options in here - probably pulling from environment variables or something>
client.reset!(options)
client.blob_client.storage_service_host[:primary] = client.storage_blob_host
client.blob_client.storage_service_host[:secondary] = client.storage_blob_host true
end

This solution is uglier, but does not involve any monkey patching. However, your extra configuration cannot be in the config/storage.yml file – it must be in the code above – or via environment variables etc… which is a pain. I prefer having all the config in one place. So, I chose the monkey patch for my project – it just felt neater.


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *