タケユー・ウェブ日報

Ruby on Rails や Flutter といったWeb・モバイルアプリ技術を武器にお客様のビジネス立ち上げを支援する、タケユー・ウェブ株式会社の技術ブログです。

S3からGoogle Driveに同期する(Lambda Ruby + CDK)

やりたかったこと

  • S3にアップロードされたアイテムをGoogleDriveの共有フォルダに同期する
  • S3のオブジェクト作成イベントでLambdaを実行して処理
  • 対象のバケットやLambda関数などはCDKで作成する

この記事に書いたこと

  • Lambda Ruby で gem を使う方法
  • AWS SDK for Ruby で SecretsManager の秘密情報を取得する方法
  • AWS SDK for Ruby で S3 からダウンロードする方法
  • Google API Client for Ruby で GoogleDrive にアップロードする方法
  • S3 + EventNotification + Lambda でアップロードされたオブジェクトの情報を取得する方法
  • CDK で S3 + EventNotification + Lambda を構成する方法

GoogleDriveにアクセスするための情報を用意する

  • サービスアカウントキー(JSON
  • 共有フォルダのID

こちらの記事が参考になります。

qiita.com

SecretsManager

秘密情報はAmazon SecretsManagerに入れておき、Lambda関数内で取り出して使うことにします。

サービスアカウントキー(JSON
$ aws secretsmanager create-secret --name "GoogleDriveServiceAccountKey" --secret-string file://serviceaccount-1234567890234-123456789012.json
共有フォルダのID
$ aws secretsmanager create-secret --name "GoogleDriveDirectoryID" --secret-string 1AMGFIquotsMPeGz1glPoLS39sB6GBy5j
Lambda関数

コード

次のフォルダ構成で作成します。

  • functions
    • sync_to_google_drive
      • index.rb
      • Gemfile
Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'aws-sdk'
gem 'google-api-client'
index.rb
require "json"
require "aws-sdk-secretsmanager"
require "aws-sdk-s3"
require 'googleauth'
require 'google/apis/drive_v3'

def handler(event:, context:)
  puts "event: #{event.inspect}"
  puts "context: #{context.inspect}"

  secretsmanager = Aws::SecretsManager::Client.new
  service_account_key_json = secretsmanager.get_secret_value(secret_id: "GoogleDriveServiceAccountKey").secret_string
  google_drive_directory_id = secretsmanager.get_secret_value(secret_id: "GoogleDriveDirectoryID").secret_string

  bucket_name = event['Records'][0]['s3']['bucket']['name']
  key = event['Records'][0]['s3']['object']['key']
  local_file = File.join('/tmp', File.basename(key))
  s3 = Aws::S3::Client.new
  s3_obj = s3.get_object(
    response_target: local_file,
    bucket:bucket_name,
    key: key
  )
  puts "s3_obj: #{s3_obj.inspect}"

  authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
    json_key_io: StringIO.new(service_account_key_json),
    scope: 'https://www.googleapis.com/auth/drive'
  )
  authorizer.fetch_access_token!

  drive = Google::Apis::DriveV3::DriveService.new
  drive.authorization = authorizer

  file_object = {
    name: key,
    parents: [google_drive_directory_id],
    modifiedTime: s3_obj.last_modified
  }
  drive.create_file(
    file_object,
    upload_source: local_file
  )

  { statusCode: 200, body: JSON.dump({ok: true}) }
rescue => e
  message = "#{e.class.name} (#{e.message})"
  puts message

  { statusCode: 501, body: JSON.dump({ok: false, error: message}) }
end

bundle install

gemを vendor/bundle 以下にインストールして、本体と一緒にアップロードできるようにしておきます。

$ cd functions/sync_to_google_drive
$ bundle install --path vendor/bundle

CDK

抜粋

import * as iam from "@aws-cdk/aws-iam";
import * as lambda from "@aws-cdk/aws-lambda";
import * as s3 from "@aws-cdk/aws-s3";
import * as s3n from "@aws-cdk/aws-s3-notifications";

const myBucket = new s3.Bucket(this, "myBucket", {
  bucketName: 'my-bucket',
  blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
  versioned: true,
  removalPolicy: cdk.RemovalPolicy.RETAIN,
});
const syncToGoogleDriveFunctionRole = new iam.Role(
  this,
  "syncToGoogleDriveFunctionRole",
  {
    assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
    managedPolicies: [
      iam.ManagedPolicy.fromAwsManagedPolicyName("AWSLambdaExecute"),
    ],
  }
);
syncToGoogleDriveFunctionRole.attachInlinePolicy(
  new iam.Policy(this, "syncToGoogleDriveFunctionRolePolicy", {
    statements: [
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ["secretsmanager:GetSecretValue"],
        resources: ["*"],
      }),
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ["s3:GetObject"],
        resources: [
          myBucket.arnForObjects("*")
        ],
      }),
    ],
  })
);
const syncToGoogleDriveFunction = new lambda.Function(this, "syncToGoogleDriveFunction", {
  runtime: lambda.Runtime.RUBY_2_5,
  handler: "index.handler",
  code: new lambda.AssetCode("./functions/sync_to_google_drive"),
  role: syncToGoogleDriveFunctionRole,
  environment: {},
  timeout: cdk.Duration.seconds(900),
});
myBucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(syncToGoogleDriveFunction));