Azure Storage に保存したブロブのバックアップ

仮想マシンのイメージなんかを Azure Storage にバックアップする記事は探せばすぐ見つかるけど、 Azure Storage に保存しているブロブを別のどこかにバックアップする記事は見つけることができなかったので、 Microsoft.WindowsAzure.Storage を使ってバックアップ用のプログラムを書いてみた。 バックアップ用のストレージアカウントを用意しておいて、 ストレージアカウントをまたいでコンテナの中身をコピーする愚直な方法。

using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using System;
using System.Threading.Tasks;

namespace AzureStorageBackup
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 3)
            {
                Console.WriteLine("AzureStorageBackup <srcConnectionString> <destConnectionString> <containerName>");
                return;
            }

            MainAsync(args).GetAwaiter().GetResult();
        }

        static async Task MainAsync(string[] args)
        {
            var backuper = new Backuper(args[0], args[1], args[2]);
            await backuper.BackupAsync();
            await backuper.EraseAsync();
        }
    }

    class Backuper
    {
        CloudStorageAccount SrcAccount { get; }

        CloudStorageAccount DestAccount { get; }

        CloudBlobClient SrcBlobClient { get; }

        CloudBlobClient DestBlobClient { get; }

        string ContainerName { get; }

        public Backuper(string srcConnectionString, string destConnectionString, string containerName)
        {
            SrcAccount = CloudStorageAccount.Parse(srcConnectionString);
            DestAccount = CloudStorageAccount.Parse(destConnectionString);
            ContainerName = containerName;
            SrcBlobClient = SrcAccount.CreateCloudBlobClient();
            DestBlobClient = DestAccount.CreateCloudBlobClient();
        }

        public async Task BackupAsync()
        {
            // コピー元のコンテナが存在しなければ何もせずに即終了
            var srcContainer = SrcBlobClient.GetContainerReference(ContainerName);
            if (await srcContainer.ExistsAsync() == false)
            {
                return;
            }

            // コピー先のストレージアカウントがコピー元のコンテナにアクセスできるようにするために、
            // SAS を発行する
            var blobToken = srcContainer.GetSharedAccessSignature(new SharedAccessBlobPolicy()
            {
                Permissions = SharedAccessBlobPermissions.Read,
                SharedAccessStartTime = DateTimeOffset.UtcNow,
                SharedAccessExpiryTime = DateTimeOffset.UtcNow.AddDays(1),
            });

            // コピー先のコンテナを作成
            var destContainer = DestBlobClient.GetContainerReference(ContainerName + DateTime.UtcNow.ToString("yyyyMMddhhmmss"));
            await destContainer.CreateIfNotExistsAsync();

            // すべてのブロブをコピーする
            await BackupContainerAsync(srcContainer, destContainer, blobToken);
        }

        async Task BackupContainerAsync(CloudBlobContainer srcContainer, CloudBlobContainer destContainer, string blobToken)
        {
            var continuationToken = new BlobContinuationToken();
            while (continuationToken != null)
            {
                // ページングされているので、続きを取得するには
                // ContinuationToken を指定する必要がある
                var response = await srcContainer.ListBlobsSegmentedAsync(continuationToken);
                continuationToken = response.ContinuationToken;

                foreach (var item in response.Results)
                {
                    CloudBlob destBlob;
                    if (item is CloudPageBlob pageBlob)
                    {
                        destBlob = destContainer.GetPageBlobReference(pageBlob.Name);
                    }
                    else if (item is CloudBlockBlob blockBlob)
                    {
                        destBlob = destContainer.GetBlockBlobReference(blockBlob.Name);
                    }
                    else if (item is CloudBlobDirectory directory)
                    {
                        await BackupDirectoryAsync(directory, destContainer, blobToken);
                        continue;
                    }
                    else
                    {
                        continue;
                    }
                    await destBlob.StartCopyAsync(new Uri(item.Uri.AbsoluteUri + blobToken));
                    Console.WriteLine($"COPY {item.Uri}");
                }
            }
        }

        async Task BackupDirectoryAsync(CloudBlobDirectory srcDirectory, CloudBlobContainer destContainer, string blobToken)
        {
            var continuationToken = new BlobContinuationToken();
            while (continuationToken != null)
            {
                // ページングされているので、続きを取得するには
                // ContinuationToken を指定する必要がある
                var response = await srcDirectory.ListBlobsSegmentedAsync(continuationToken);
                continuationToken = response.ContinuationToken;

                foreach (var item in response.Results)
                {
                    CloudBlob destBlob;
                    if (item is CloudPageBlob pageBlob)
                    {
                        destBlob = destContainer.GetPageBlobReference(pageBlob.Name);
                    }
                    else if (item is CloudBlockBlob blockBlob)
                    {
                        destBlob = destContainer.GetBlockBlobReference(blockBlob.Name);
                    }
                    else if (item is CloudBlobDirectory directory)
                    {
                        await BackupDirectoryAsync(directory, destContainer, blobToken);
                        continue;
                    }
                    else
                    {
                        continue;
                    }
                    await destBlob.StartCopyAsync(new Uri(item.Uri.AbsoluteUri + blobToken));
                    Console.WriteLine($"COPY {item.Uri}");
                }
            }
        }

        public async Task EraseAsync()
        {
            var aWeekAgo = DateTimeOffset.UtcNow.AddDays(-7);
            var continuationToken = new BlobContinuationToken();
            while (continuationToken != null)
            {
                var response = await DestBlobClient.ListContainersSegmentedAsync(
                    $"{ContainerName}",
                    continuationToken);
                continuationToken = response.ContinuationToken;

                foreach (var container in response.Results)
                {
                    // 7日前までのバックアップを保持する
                    // 7日より前は削除
                    if (container.Properties.LastModified < aWeekAgo)
                    {
                        await container.DeleteIfExistsAsync();
                    }
                }
            }
        }
    }
}

当たり前だけど、コンテナ内のブロブが増えれば増えるほど時間がかかる。 Microsoft.WindowsAzure.Storage は内部で REST API を呼び出しているみたいだから、 ただでさえ速いとは言い難いのに。 並列化するか、他のアプローチを考えないといけないかもな。