Obsidian のバックアップを diffzip で強化

Summary

Obsidian を利用する環境を整えているが、同期の仕組みを構築し終わったのでバックアップも設定しておく。
いくつも方法はあるが、今回は vrtmrz/obsidian-livesync の作者の vrtmrz/diffzip を利用することにした。これにより起動時に別途用意した S3 へ差分バックアップを転送するようにしておくことで端末間同期のやり直しやいざという時の切り戻しができるようにしておく。

S3 バケットの準備

AWS を利用する MinIO などでセルフホストすることが可能だが、外部からのアクセス経路やデータの防護策として最適な方法を取ることが難しく NAS に格納して一緒にデータが吹き飛ぶなど事象が発生すると立ち直れないため今回は Amazon S3 を利用する。
数十 GB の容量なので月額も安く、基本はアップロードのみのため問題ないだろう。

Topic
当初は Cloudflare R2 を利用する予定だったが、 Bucket に絞った権限が作成出来なかったため見送った。

方法はいくつかあるが、私は rain を試してみたかったため AWS CloudFormation で用意した。

  • スタック名にランダム文字列を付ける
  • IAM Group を作る
  • IAM user を作る
  • S3 を作成
    • バケットポリシーを作成して http は拒否
    • ライフサイクルを設定する
      • マルチパートアップロードの残骸は3日後に消す
      • 最新以外のバージョンは45日後に削除
obsidian-diffzip.yml
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
AWSTemplateFormatVersion: '2010-09-09'
Description: 'DiffZip Backup Infrastructure for Obsidian Plugin - Fixed Version'

Parameters:
  BucketBaseName:
    Type: String
    Description: 'Base name for the S3 bucket (6 random characters will be appended)'
    Default: 'obsidian-diffzip'
    AllowedPattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$'
    ConstraintDescription: 'Bucket name must be lowercase, start and end with alphanumeric characters'
    MinLength: 3
    MaxLength: 57

  Region:
    Type: String
    Description: 'AWS Region for deployment'
    Default: 'ap-northeast-1'
    AllowedValues:
      - ap-northeast-1  # Tokyo
      - us-east-1       # N. Virginia
      - us-west-2       # Oregon

Resources:
  # Generate random suffix for bucket name
  RandomSuffixLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  RandomSuffixFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-random-suffix-generator'
      Runtime: python3.9
      Handler: index.handler
      Role: !GetAtt RandomSuffixLambdaRole.Arn
      Code:
        ZipFile: |
          import json
          import random
          import string
          import cfnresponse

          def handler(event, context):
              try:
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
                      return

                  # Generate 6 character random suffix
                  chars = string.ascii_lowercase + string.digits
                  suffix = ''.join(random.choice(chars) for _ in range(6))

                  response_data = {'RandomSuffix': suffix}
                  cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
              except Exception as e:
                  print(f"Error: {str(e)}")
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})          

  RandomSuffix:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !GetAtt RandomSuffixFunction.Arn

  # S3 Bucket for DiffZip backups
  DiffZipBackupBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${BucketBaseName}-${RandomSuffix.RandomSuffix}'
      CorsConfiguration:
        CorsRules:
          - AllowedHeaders:
              - amz-sdk-invocation-id
              - amz-sdk-request
              - authorization
              - content-type
              - x-amz-content-sha256
              - x-amz-date
              - x-amz-user-agent
            AllowedMethods:
              - GET
              - PUT
              - HEAD
            AllowedOrigins:
              - app://obsidian.md
              - capacitor://localhost
              - http://localhost:*
            MaxAge: 3600

      # Block all public access
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      # Enable versioning
      VersioningConfiguration:
        Status: Enabled
      # Lifecycle configuration
      LifecycleConfiguration:
        Rules:
          # Clean up incomplete multipart uploads after 3 days
          - Id: CleanupIncompleteMultipartUploads
            Status: Enabled
            AbortIncompleteMultipartUpload:
              DaysAfterInitiation: 3
          # Delete non-current versions after 45 days
          - Id: DeleteNonCurrentVersions
            Status: Enabled
            NoncurrentVersionExpiration:
              NoncurrentDays: 45
      # Encryption configuration
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      Tags:
        - Key: Purpose
          Value: DiffZipBackup
        - Key: Application
          Value: Obsidian

  # IAM Group for DiffZip backup operations
  DiffZipBackupGroup:
    Type: AWS::IAM::Group
    Properties:
      GroupName: !Sub '${AWS::StackName}-group-${RandomSuffix.RandomSuffix}'
      Policies:
        - PolicyName: DiffZipBackupPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # Allow read/write operations on backup objects
              - Sid: DiffZipBackupOperations
                Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                Resource:
                  - !Sub '${DiffZipBackupBucket.Arn}/*'
              # Allow bucket-level operations
              - Sid: DiffZipBackupBucketAccess
                Effect: Allow
                Action:
                  - s3:ListBucket
                Resource:
                  - !Sub '${DiffZipBackupBucket.Arn}'

  # IAM User for DiffZip backup access
  DiffZipBackupUser:
    Type: AWS::IAM::User
    Properties:
      UserName: !Sub '${AWS::StackName}-user-${RandomSuffix.RandomSuffix}'
      Groups:
        - !Ref DiffZipBackupGroup
      Tags:
        - Key: Purpose
          Value: DiffZipBackup

  # S3 Bucket Policy for additional security
  DiffZipBackupBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref DiffZipBackupBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          # Deny insecure connections
          - Sid: DenyInsecureConnections
            Effect: Deny
            Principal: '*'
            Action: 's3:*'
            Resource:
              - !Sub '${DiffZipBackupBucket.Arn}/*'
              - !Sub '${DiffZipBackupBucket.Arn}'
            Condition:
              Bool:
                'aws:SecureTransport': 'false'

Outputs:
  BucketRegion:
    Description: 'Region of the created S3 bucket'
    Value: !Ref AWS::Region
    Export:
      Name: !Sub '${AWS::StackName}-BucketRegion'

  S3Endpoint:
    Description: 'S3 endpoint for the region'
    Value: !Sub 'https://${DiffZipBackupBucket}.s3.${AWS::Region}.amazonaws.com'
    Export:
      Name: !Sub '${AWS::StackName}-S3Endpoint'

  IAMUserName:
    Description: 'Name of the created IAM user'
    Value: !Ref DiffZipBackupUser
    Export:
      Name: !Sub '${AWS::StackName}-IAMUserName'

  NextSteps:
    Description: 'Next steps to complete the setup'
    Value: !Sub |

      Create access key for user ${DiffZipBackupUser} using: aws iam create-access-key --user-name ${DiffZipBackupUser}

1
2
3
4
5
> export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
> export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
> export AWS_DEFAULT_REGION=ap-northeast-1

> ./rain deploy obsidian-diffzip.yml

構築後下記に情報が出るためこれを Obsidian のプラグイン画面に入力する

1
2
3
4
5
6
7
8
Deploying template 'obsidian-diffzip.yml' as stack 'obsidian-diffzip' in ap-northeast-1.
Stack obsidian-diffzip: UPDATE_COMPLETE
  Outputs:
    NextSteps: Create access key for user obsidian-diffzip-user-b5ecge using: aws iam create-access-key --user-name obsidian-diffzip-user-b5ecge
    BucketRegion: ap-northeast-1
    S3Endpoint: https://obsidian-diffzip-b5ecge.s3.ap-northeast-1.amazonaws.com
    IAMUserName: obsidian-diffzip-user-b5ecge
Successfully updated obsidian-diffzip

AccessKey, SecretKey は NextSteps 記載のコマンドで作成できる

  • Include hidden folders はお好みで設定する
  • Bucket は本来は S3 Bucket 名を設定するが今回は CORS 設定の都合で Endpoint にBucket名を含めているので変更しない
  • Test ボタンを押せば接続テストができる

設定

これで Obsidian 起動時にバックアップが自動で作成され S3 に転送されるようになった。

最終更新 2025-06-22 17:47 UTC
Hugo で構築されています。
テーマ StackJimmy によって設計されています。