RDS for Oracleのダンプ取得〜S3アップロードをシェルスクリプトで実装してみた

RDS for Oracleのダンプ取得〜S3アップロードをシェルスクリプトで実装してみた

RDS for Oracleのダンプファイルを取得してS3にアップロードする方法を、実装例(シェルスクリプト)とともに紹介します。
2026.07.29

はじめに

こんにちは、コンサルティング部の阿部です。

私は普段、インフラ案件のNon-Tech寄りな課題への支援(プロジェクト推進等)に従事しており、「自ら手を動かして設計構築をする」ような機会はあまり多くありません。

そんな中、Amazon RDS for Oracle で動かすデータベースについて、「ダンプファイルを定期的に取得して、S3に保管したい」という要件を検討する機会がありました。AWS公式ドキュメントのOracle Data Pump を使用したインポートを読む限りは実現できそうだったのですが、実際に動作するのか気になるところですし、せっかくの機会なので実際に動かして確かめてみました。

この記事では、「Oracleのダンプを取得してS3に上げられるか」を確認することを第一目的として、EC2上のシェルバッチとして組んで動かしてみたのでご紹介します。

本記事のゴール・前提

  • 必要な事前設定を揃えたうえで、実際にRDSからS3へダンプをアップロードできること
  • 完璧な本番用シェルを作ることは目的にしない(スクリプトはあくまで検証用なので、残課題ありでOK)
  • バッチとして定期実行できる形にすること(今回の検証では、一連の処理を1本のスクリプトに記述しました)

想定読者

  • RDS for Oracleのダンプを取得する方法を知りたい方
  • RDS for OracleのダンプファイルをS3にアップロードする方法を知りたい方
  • ダンプ取得〜S3保管の定期実行による運用を考えている方

サマリ

本編の説明がかなり長いため、先に要点だけまとめました。

  • 処理の基本形は次の3ステップ(実装では、DB認証情報の取得を加えて4ステップにしています)
    • ダンプ取得:DBMS_DATAPUMP
    • RDS→S3アップロード:rdsadmin_s3_tasks.upload_to_s3
    • 後処理(RDS上に生成した一時ファイルの削除):UTL_FILE.FREMOVE
  • この3つを動かすまでに、事前設定がそれなりに必要(後述します)
  • S3アップロードは非同期処理なので、タスクの完了を待つ仕組みが必要
  • ステップごとに固有の終了コードを割り当てておくと、どこで失敗したかを切り分けやすい

背景

RDSのDB操作

RDSはマネージドサービスなので、DBが動いているOSに直接ログインすることはできないです。オンプレのように、サーバーにログインしてダンプファイルの実際の置き場所を確認したり、そのままコピーして別サーバーへ持っていったり、といったことができないです。ダンプの生成自体も、生成したファイルの確認・移動・削除も、すべてSQL経由でRDSに依頼する形になります。

今回は、Oracle標準の DBMS_DATAPUMP パッケージと、RDS固有の rdsadmin パッケージ群(AWSが用意している管理用パッケージで、OSに入れない代わりにSQL経由で管理操作を行うための入口)を使って、シェルスクリプトを組んでみます。

参考:

スナップショットとダンプについて

「RDSの自動スナップショットを取っているなら、ダンプはいらないのでは?」と思われるかもしれませんが、両者は役割が異なります。

用途 手段 復旧の粒度
インスタンス丸ごと復旧 スナップショット / AWS Backup インスタンス単位
誤削除・論理破損からの部分復旧 ダンプ(論理バックアップ) テーブル・スキーマ単位

スナップショットはインスタンス丸ごとのバックアップ・復旧は得意ですが、「特定のテーブルだけ昨日の状態に戻したい」といった要望には向きません。ダンプはテーブル・スキーマ単位で扱えるのが強みです。そのため、スナップショット(物理)とダンプ(論理)を組み合わせて運用することもあります。この記事で扱うのは、このうちダンプ取得の部分です。


処理の全体像

ここからが、本記事のメインになります。

処理全体として、以下の4ステップ構成としました。(参考:Oracle Data Pump を使用したインポート

  • ① DB認証情報取得:AWS Secrets Manager からDB認証情報を取得する
  • ② Oracleダンプ取得処理:RDS(Oracle)が、ダンプファイルを生成する
  • ③ S3アップロード処理:RDSが、ダンプファイルをS3へ転送する
  • ④ 一時ファイル削除処理:RDSが、一時ファイル(ダンプファイル)を削除する

これらの一連の処理を、1つのシェルスクリプトで実行するイメージです。実行環境は、EC2上(バッチサーバー)から実行する想定とします。なお、DB接続情報はAWS Secrets Managerで管理している前提としました。

①〜④の処理シーケンスのイメージは、以下になります。

oracle_dump_sequence

※バッチサーバー(シェルスクリプト)がやっているのは、RDSへの指示出しだけです。実際にダンプを作ってS3へ転送するのはRDS自身になります。

事前設定(重要)

スクリプトを組む前に、どこに何を用意するかを整理しておきます。ここが揃っていないと、スクリプト処理内で失敗します。設定対象ごとにまとめると、以下のとおりです。(意外と多い・・・)

対象 何を設定するか
RDS ① オプショングループに S3_INTEGRATION を追加する
② ダンプ実行ユーザーに権限付与(DATAPUMP_EXP_FULL_DATABASE 等)
IAM(RDS用) 対象バケットへの s3:ListBucket / s3:GetBucketLocation、オブジェクトへの s3:GetObject / s3:PutObject / s3:DeleteObject を許可したロールを作成し、RDSにアタッチ
S3 保管先バケットを作成(+世代管理するならライフサイクルルール)
ネットワーク ① プライベートサブネット上のRDSがS3へ到達できる経路(S3ゲートウェイエンドポイントを作成し、ルートテーブルに紐付け)
② バッチサーバー(EC2)がInstant Client・jq を取得するためのアウトバウンド経路
Secrets Manager DB接続に使う認証情報(ユーザー/パスワード)をシークレットとして用意
※RDSの「マスターユーザーのパスワードをSecrets Managerで管理」機能を使うと自動でシークレットが作られる
バッチサーバー(EC2) ① SQLPlus(Instant Client)を導入しPATH/LD_LIBRARY_PATHを通す
jq を導入(AWS CLIはAmazon Linux 2023に同梱済み)
③ RDS(1521)へ到達できるSG・経路
④ S3/Secrets Manager を操作できるIAMロールをインスタンスにアタッチ
⑤ EC2自身への接続手段(今回はSSM Session Manager用のロールも付けています)

上の表のうち、今回の検証でCloudFormationテンプレートから作ったものと、EC2にログインして手で入れたものは、次のように分かれます(環境の構成は後述の「検証環境」を参照)。

アプローチ 対象
CloudFormationで自動作成 RDS(オプショングループ含む)/IAMロール(RDS用・EC2用)/S3バケット/ネットワーク(VPC・サブネット・S3ゲートウェイエンドポイント・セキュリティグループ)/Secrets Manager(RDSの機能で自動生成)
EC2にログインして手動 SQLPlus(Instant Client)の導入、jq の導入

RDSの「②ダンプ実行ユーザーへの権限付与」は、今回は行っていません。DATAPUMP_EXP_FULL_DATABASE は、自分以外のスキーマもエクスポートできるようにするためのOracleのロールです。今回はマスターユーザーで自身のスキーマを取っただけなので、追加の権限付与は割愛しました。

CloudFormationデプロイ時に設定

CloudFormationデプロイ時におこなった事前準備のうち、いくつか抜粋して紹介します。

まずRDS側のS3統合です。オプショングループに S3_INTEGRATION を付与し、S3へ読み書きできるIAMロールを AssociatedRoles でRDSに紐付けます。あわせて ManageMasterUserPassword: true を指定すると、マスターユーザーのパスワードがSecrets Managerで自動管理されます。また、紐付けている RdsS3Role は、対象バケットへの読み書きを許可したIAMロールです。

  OracleOptionGroup:
    Type: AWS::RDS::OptionGroup
    Properties:
      EngineName: oracle-se2
      MajorEngineVersion: '19'
      OptionConfigurations:
        - OptionName: S3_INTEGRATION # S3_INTEGRATION を付与
          OptionVersion: '1.0'

  OracleDb:
    Type: AWS::RDS::DBInstance
    Properties:
      # ...(中略)
      OptionGroupName: !Ref OracleOptionGroup
      ManageMasterUserPassword: true   # パスワードをRDSが生成→Secrets Managerで管理
      AssociatedRoles:
        - FeatureName: S3_INTEGRATION
          RoleArn: !GetAtt 
.Arn

※保管先のS3バケットや、バッチサーバー(EC2)用のIAMロール(S3の実在チェック用のS3権限+DB認証情報の取得用のSecrets Manager権限)も、同じスタック内で作成しています

もう1つ載せておきたいのがネットワーク経路です。プライベート配置のRDSがインターネット経由せずにS3へ到達できるよう、S3ゲートウェイエンドポイントをプライベートルートテーブルに紐付けています。

  # プライベートのRDSが インターネット経由せずに S3(ダンプアップロード)へ到達するための経路
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcId: !Ref Vpc
      ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
      VpcEndpointType: Gateway
      RouteTableIds:
        - !Ref PrivateRouteTable

手動で行った作業

EC2のOS内の設定作業については、手動で構築しました。やっているのは次の3つです。

  1. jq をインストールする
  2. Oracle Instant Client(basicsqlplus の2つ)を /opt/oracle 配下に展開する
  3. sqlplus コマンドが動くように、環境変数 PATHLD_LIBRARY_PATH を通す

Instant Client は、Oracleが配布している軽量版のクライアントです。zipを展開するだけで使えますが、sqlplus は単体では動かず、ライブラリ本体の basic とセットで入れる必要があります。環境変数を2つ通しているのもこのためで、PATHsqlplus コマンド自体を、LD_LIBRARY_PATH はそれが使うライブラリを見つけるために必要です。

# jq
sudo dnf install -y jq
# Oracle Instant Client(basic + sqlplus)を配置し PATH / LD_LIBRARY_PATH を通す
#   ※ここでは /opt/oracle 配下に展開した想定
echo 'export PATH=/opt/oracle/instantclient:$PATH'                       | sudo tee /etc/profile.d/instantclient.sh
echo 'export LD_LIBRARY_PATH=/opt/oracle/instantclient:$LD_LIBRARY_PATH' | sudo tee -a /etc/profile.d/instantclient.sh
source /etc/profile.d/instantclient.sh
sqlplus -v   # 通ればOK

/etc/profile.d/ にファイルを置いているのは、ログインし直しても環境変数が残るようにするためです。最後の sqlplus -v でバージョンが表示されれば、導入は成功です。なお jq は、Secrets Managerが返すJSONからユーザー名とパスワードを取り出すために使います。


作成したバッチ処理詳細

EC2(Amazon Linux 2023)上にSQLPlus(Instant Client)をインストールされてる前提で、RDSへ接続してダンプ取得〜S3アップロード〜後処理(全体像の①〜④)を行うシェルスクリプトを作成しました。使い方は以下です。

bash run_dump_verify.sh <SecretArn> <RdsEndpoint> <Bucket> [Schema] [DBName]

スクリプトの組み立て

中身に入る前に、先に挙げた①〜④をスクリプトにどう落としたかを整理しておきます。

RDSへの操作は、すべてSQLPlusでSQLを実行する形になります。そのSQLはファイルとして別途配らずスクリプト1本で完結させたかったため、実行時に /tmp へ書き出してから読み込ませる方式にしました。そのため各ステップは「SQLを書き出す」と「そのSQLを呼ぶ」に分かれます。

書き出しは使う直前ではなく、①の直後にまとめて実施しています(同じ書き方が何度も出てくると読みにくいので、1か所に集めました)。

結果として、スクリプトは以下の7つのブロックが上から順に並ぶ構成になりました。(ステップ①〜④のフローの中でいくつかの事前・事後処理が入ったイメージ)

# ブロック 内容 ステップ①〜④との対応
1 事前チェック 引数の受け取り・ログ設定と、sqlplus / jq が使えるかの確認 ―(①の前)
2 実処理 Secrets Managerから認証情報を取得
3 SQL生成 ステップ②〜④で使うSQLを /tmp へ書き出す ―(②〜④の下準備)
4 実処理 書き出したSQLを呼んでダンプを取得
5 実処理 書き出したSQLを呼んでS3へアップロードし、完了を待つ
6 実処理 書き出したSQLを呼んで一時ファイルを削除
7 後片付け 書き出したSQLファイルを削除し、結果をログに出して終了

1つ目の「事前チェック」も、スクリプト内の処理です。①に入る前に sqlplusjq が使える状態かを確認し、揃っていなければそこで止めます。

3つ目の「SQL生成」で書き出すSQLは、7本になりました。ステップ②〜④の中でRDSへ投げる指示(ダンプ取得、サイズ確認、アップロード開始、完了確認…)をSQL単位に分けた結果です。内訳と、どのブロックから呼ばれるかは次のとおりです。

SQLファイル 中身 渡す引数 呼び出すブロック
/tmp/_exp.sql DBMS_DATAPUMP でスキーマをエクスポート &1 ジョブ名 / &2 ダンプファイル名 / &3 ログファイル名 / &4 スキーマ名 4(②)
/tmp/_size.sql 生成された .dmp のサイズを取得 &1 ダンプファイル名 4(②:取得後のログ出力)
/tmp/_upload.sql upload_to_s3 でアップロードを開始 &1 バケット名 / &2 ダンプファイル名 5(③)
/tmp/_readlog.sql アップロードタスクのログを読む &1 タスクID 5(③:完了待ちのポーリング)
/tmp/_cleanup.sql .dmp.log を削除 &1 ダンプファイル名 / &2 ログファイル名 6(④)
/tmp/_purge.sql export_* の残骸をまとめて削除 なし 6(④:PURGE_LEFTOVERS=1 のときだけ)
/tmp/_countleft.sql 残っている export_* の件数を数える なし 6(④:削除後の確認)

※ ブロックが7つ、SQLも7本ですが、1対1で対応しているわけではありません(SQLはすべてブロック3でまとめて書き出し、ブロック4〜6から呼び出します)。

終了コードについて

失敗時は、ブロックごとに固有の終了コードを返して即終了する作りにしています。ジョブ監視ツール等が「どこで落ちたか」を終了コードで切り分けられるようにするためです。

終了コード 落ちた箇所
0 正常終了
10 事前チェック(sqlplus / jq が無い)
20 認証情報の取得(①)
30 ダンプ取得(②)
40 S3アップロードの開始(③)
50 アップロード完了待ち/S3上の存在確認(③)
60 一時ファイルの削除(④)

スクリプトの中身(7ブロック)

ここからは実装の中身です。上の表の順に、スクリプトを7つに分けて全文を載せます。7つのコードブロックをそのままつなげると run_dump_verify.sh 1本になります。

※ スクリプト内のログに出てくる [1/4][4/4] は、ステップ①〜④の番号です(ブロック番号とは別物です)。

1/7 事前チェック(ステップ①の前)

引数の受け取り、ログ出力の準備、共通関数の定義、そして sqlplusjq の存在確認までを行います。log / die / run_sql は以降のブロックで使い回す自作の関数です。特に run_sql は、SQLPlusを実行して出力を $OUT、終了コードを $RC に受け取るためのラッパーで、ブロック4〜6で何度も登場します。

#!/bin/bash
# =============================================================================
# RDS(Oracle) dump operation verification script
#   [1] get DB credentials (Secrets Manager)
#   [2] export dump (Data Pump)
#   [3] upload to S3  (+ verify object exists in S3 = hard gate)
#   [4] remove temp dump on RDS
#
# Usage (on EC2):
#   bash run_dump_verify.sh <SecretArn> <RdsEndpoint> <Bucket> [Schema] [DBName]
#   PURGE_LEFTOVERS=1  ... also delete all export_* leftovers at step [4]
#
# Exit codes (Job monitoring watches these):
#   0=OK / 10=preflight / 20=secret / 30=dump / 40=upload-start
#   50=upload-wait-or-verify / 60=cleanup
# =============================================================================
set -o pipefail
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}"   # profile側の $LD_LIBRARY_PATH 参照を安全に

# ---- args ----
ARN="${1:?usage: bash run_dump_verify.sh <SecretArn> <RdsEndpoint> <Bucket> [Schema] [DBName]}"
RDS_EP="${2:?RdsEndpoint is required}"
BUCKET="${3:?Bucket is required}"
SCHEMA="${4:-ADMIN}"
DB_NAME="${5:-ORCL}"
REGION="ap-northeast-1"
PURGE_LEFTOVERS="${PURGE_LEFTOVERS:-0}"

# ---- logging (screen + file) ----
TS=$(date +%Y%m%d_%H%M%S)
LOGFILE="/tmp/run_dump_verify_${TS}.log"
log(){ echo "[$(date '+%F %T')] $*" | tee -a "$LOGFILE"; }
STEP="init"
die(){ local rc="$1"; shift; log "[ERROR] step=${STEP} failed: $* (rc=${rc})"; log "Process End.  [RESULT] FAILED step=${STEP} rc=${rc} log=${LOGFILE}"; exit "$rc"; }
# sqlplusを実行し、生出力を$OUT/戻り値を$RCに入れつつログファイルへ追記
OUT=""; RC=0
run_sql(){ OUT=$(sqlplus -S "$CONN" "$@" 2>&1); RC=$?; printf '%s\n' "$OUT" >> "$LOGFILE"; }
# 出力からエラー理由らしき行を抽出(ログの[ERROR]行に載せる用)
reason_of(){ printf '%s\n' "$1" | grep -iE 'ORA-|PLS-|SP2-|error|denied|not *found' | head -3 | tr '\n' ' '; }

log "==== Dump verification started (log=${LOGFILE}) ===="

# ---- [0] preflight ----
STEP="0-preflight"
command -v sqlplus >/dev/null 2>&1 || source /etc/profile.d/instantclient.sh 2>/dev/null || true
command -v sqlplus >/dev/null 2>&1 || die 10 "sqlplus not found (check Instant Client PATH)"
command -v jq      >/dev/null 2>&1 || die 10 "jq not found (sudo dnf install -y jq)"

$STEP にブロック名を入れておき、die が呼ばれたときにログへ出す作りにしています。どこで落ちたかが終了コードとログの両方から分かるようにするためです。

2/7 Secrets Manager から認証情報を取得(ステップ①)

DBのユーザー/パスワードはスクリプトに直書きせず、Secrets Managerから取得します。取得したJSONから jq でユーザー名とパスワードを取り出し、SQLPlusの接続文字列 $CONN を組み立てます。

# ---- [1] credentials ----
STEP="1-secret"
log "[1/4] Getting DB credentials from Secrets Manager ..."
CREDS=$(aws secretsmanager get-secret-value --secret-id "$ARN" --query SecretString --output text --region "$REGION" 2>>"$LOGFILE") \
  || die 20 "Secrets Manager get-secret-value (check ARN/permission/region)"
DB_USER=$(echo "$CREDS" | jq -r .username)
DB_PASS=$(echo "$CREDS" | jq -r .password)
CONN="${DB_USER}/${DB_PASS}@//${RDS_EP}:1521/${DB_NAME}"
log "  -> OK (user=${DB_USER}, db=${DB_NAME}, schema=${SCHEMA})"

DUMP="export_${TS}.dmp"; DPLOG="export_${TS}.log"; JOB="EXP_${TS}"

最後の行で、この後使うダンプファイル名・ログファイル名・Data Pumpのジョブ名を決めています。$TS は起動時刻なので、実行するたびに別名(export_20260717_083000.dmp のような形)になります。

3/7 SQLファイルの生成(ステップ②〜④の下準備)

ステップ②〜④の処理で使う7本のSQLを /tmp へまとめて書き出します。ヒアドキュメント(cat > ファイル名 <<'SQL' … SQL)で、SQL から SQL までの中身をそのままファイルへ流し込んでいます。

# ---- generate temp SQL (quoted heredoc protects KU$ constants) ----

cat > /tmp/_exp.sql <<'SQL'
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET SERVEROUTPUT ON VERIFY OFF
DECLARE
  h1 NUMBER; st VARCHAR2(30);
BEGIN
  h1 := DBMS_DATAPUMP.OPEN('EXPORT','SCHEMA',NULL,'&1');
  DBMS_DATAPUMP.ADD_FILE(h1,'&2','DATA_PUMP_DIR',NULL,DBMS_DATAPUMP.KU$_FILE_TYPE_DUMP_FILE);
  DBMS_DATAPUMP.ADD_FILE(h1,'&3','DATA_PUMP_DIR',NULL,DBMS_DATAPUMP.KU$_FILE_TYPE_LOG_FILE);
  DBMS_DATAPUMP.METADATA_FILTER(h1,'SCHEMA_EXPR','IN (''&4'')');
  DBMS_DATAPUMP.START_JOB(h1);
  DBMS_DATAPUMP.WAIT_FOR_JOB(h1, st);
  IF st != 'COMPLETED' THEN
    RAISE_APPLICATION_ERROR(-20001, 'Data Pump export state = '||st);
  END IF;
END;
/
EXIT
SQL

cat > /tmp/_size.sql <<'SQL'
SET HEADING OFF PAGESIZE 0 FEEDBACK OFF VERIFY OFF
SELECT TO_CHAR(ROUND(filesize/1024/1024,2))
FROM   TABLE(rdsadmin.rds_file_util.listdir('DATA_PUMP_DIR')) WHERE filename='&1';
EXIT
SQL

cat > /tmp/_upload.sql <<'SQL'
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET HEADING OFF PAGESIZE 0 FEEDBACK OFF VERIFY OFF
SELECT rdsadmin.rdsadmin_s3_tasks.upload_to_s3(
         p_bucket_name=>'&1', p_prefix=>'&2', p_s3_prefix=>'',
         p_directory_name=>'DATA_PUMP_DIR') FROM dual;
EXIT
SQL

cat > /tmp/_readlog.sql <<'SQL'
SET HEADING OFF PAGESIZE 0 FEEDBACK OFF VERIFY OFF LINESIZE 4000 LONG 1000000
SELECT text FROM TABLE(rdsadmin.rds_file_util.read_text_file('BDUMP','dbtask-&1..log'));
EXIT
SQL

cat > /tmp/_cleanup.sql <<'SQL'
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET SERVEROUTPUT ON VERIFY OFF
BEGIN
  UTL_FILE.FREMOVE('DATA_PUMP_DIR','&1');
  UTL_FILE.FREMOVE('DATA_PUMP_DIR','&2');
END;
/
EXIT
SQL

cat > /tmp/_purge.sql <<'SQL'
SET SERVEROUTPUT ON VERIFY OFF
BEGIN
  FOR f IN (SELECT filename FROM TABLE(rdsadmin.rds_file_util.listdir('DATA_PUMP_DIR'))
            WHERE filename LIKE 'export\_%' ESCAPE '\') LOOP
    BEGIN
      UTL_FILE.FREMOVE('DATA_PUMP_DIR', f.filename);
      DBMS_OUTPUT.PUT_LINE('purged '||f.filename);
    EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE('skip '||f.filename||': '||SQLERRM);
    END;
  END LOOP;
END;
/
EXIT
SQL

cat > /tmp/_countleft.sql <<'SQL'
SET HEADING OFF PAGESIZE 0 FEEDBACK OFF VERIFY OFF
SELECT COUNT(*) FROM TABLE(rdsadmin.rds_file_util.listdir('DATA_PUMP_DIR'))
WHERE filename LIKE 'export\_%' ESCAPE '\';
EXIT
SQL

各SQLが何をしているかは、呼び出し側のブロック4〜6であらためて触れます。ここでは、書き方について2点だけ補足します。

1点目は、SQLの中に出てくる &1&4 です。これはSQLPlusの置換変数で、呼び出し時に引数を指定することで置換されます。引数の順番がそのまま &1&2… に対応するので、呼び出し側の引数の順序と、SQL側の番号がずれないよう注意が必要です(対応は前掲の表を参照)。

2点目は、書き出しの終わりの目印をシングルクォートで囲んでいる(<<'SQL')点です。これには理由があるのですが、後述の「気づき」で触れます。

なお、書き出した7本は、最後のブロック7で rm -f によりまとめて削除します。

4/7 Data Pump でダンプ取得(ステップ②)

_exp.sql を呼び出します。中身は DBMS_DATAPUMP でスキーマ単位のエクスポートジョブを定義し、WAIT_FOR_JOB で完了まで待つPL/SQLです。出力先の DATA_PUMP_DIR は、RDSにあらかじめ用意されているダンプ出力用のディレクトリオブジェクトです。

ダンプを取得できたら、続けて _size.sql を呼び、rdsadmin.rds_file_util.listdir でファイルサイズを取得してログに残します(RDSではOSの ls が使えないので、これもSQL経由です)。

# ---- [2] export dump ----
STEP="2-dump"
log "[2/4] Exporting dump (schema=${SCHEMA}) ..."
run_sql @/tmp/_exp.sql "$JOB" "$DUMP" "$DPLOG" "$SCHEMA"
[ "$RC" -eq 0 ] || die 30 "Data Pump export | reason: $(reason_of "$OUT")"
DUMP_MB=$(sqlplus -S "$CONN" @/tmp/_size.sql "$DUMP" 2>>"$LOGFILE" | tr -d '[:space:]')
log "  -> Dump finished. File: ${DUMP}  Size: ${DUMP_MB} MB"

5/7 S3 へアップロード(ステップ③)

_upload.sqlrdsadmin.rdsadmin_s3_tasks.upload_to_s3)を呼ぶとS3アップロードが始まります。ただしこれは非同期で、関数はタスクIDを返してすぐに戻ってきます。アップロードはバックグラウンドで進みます。

upload_to_s3 に渡している p_prefix は「アップロード対象とするファイル名の前方一致条件」です。ここにダンプのファイル名を渡すことで、DATA_PUMP_DIR の中から今回作った .dmp だけを対象にしています(空にするとディレクトリ内の全ファイルが対象になります)。

ここですぐに次のステップ(一時ファイル削除)へ進むと、アップロードが終わる前にダンプを消してしまう危険があります。そこで、返ってきたタスクIDのタスクログ(BDUMP/dbtask-<タスクID>.logBDUMP はRDSがログ類を出力するディレクトリ)を _readlog.sql でポーリングして、完了を待ちます。

さらにもう一段、aws s3 ls でオブジェクトが実際にS3にあるかを確認します。「タスクログは成功と言っているのに、なぜかS3に無い」というケースを弾くための保険です。

# ---- [3] upload to S3 ----
STEP="3-upload"
log "[3/4] Uploading to S3 (bucket=${BUCKET}) ..."
run_sql @/tmp/_upload.sql "$BUCKET" "$DUMP"
TASK_ID=$(printf '%s' "$OUT" | tr -d '[:space:]')
{ [ "$RC" -eq 0 ] && [ -n "$TASK_ID" ]; } || die 40 "S3 upload start | reason: $(reason_of "$OUT")"
log "  -> upload task started (task_id=${TASK_ID}), waiting for completion (max 15min, poll 10s) ..."

STEP="3-upload-wait"
DONE=0
for i in $(seq 1 90); do
  sleep 10   # タスクログ(dbtask-*.log)はRDSが非同期生成。生成前に読むとORA-29283になるので先に待つ
  OUT=$(sqlplus -S "$CONN" @/tmp/_readlog.sql "$TASK_ID" 2>/dev/null || true)
  # ログ未生成(ORA-29283=ファイル未存在)は「まだ完了待ち」であって失敗ではない → 次のポーリングへ
  if echo "$OUT" | grep -qi "ORA-29283"; then continue; fi
  # 【暫定】完了判定を "finished successfully" の文字列で拾っている(後述の「今後の課題」参照)
  if echo "$OUT" | grep -qi "finished successfully"; then DONE=1; break; fi
  # ここまで来て失敗マーカー/実ORAエラーが出ていれば本物のアップロード失敗
  if echo "$OUT" | grep -qiE "task failed|ORA-[0-9]"; then
    printf '%s\n' "$OUT" >> "$LOGFILE"
    die 50 "S3 upload task error | reason: $(reason_of "$OUT")"
  fi
done
[ "$DONE" -eq 1 ] || die 50 "S3 upload wait timeout after 15min (task log: dbtask-${TASK_ID}.log)"

# ---- verify the object actually exists in S3 (hard gate) ----
STEP="3-verify-s3"
S3LS=$(aws s3 ls "s3://${BUCKET}/${DUMP}" --region "$REGION" 2>>"$LOGFILE"); S3RC=$?
printf '%s\n' "$S3LS" >> "$LOGFILE"
{ [ "$S3RC" -eq 0 ] && [ -n "$S3LS" ]; } || die 50 "uploaded object NOT found in S3: s3://${BUCKET}/${DUMP}"
log "  -> S3 Upload finished. Location: s3://${BUCKET}/${DUMP}  (verified present in S3)"

6/7 RDS 内の一時ファイルを削除(ステップ④)

S3アップロードの成功を確認できて初めて、RDS内(DATA_PUMP_DIR)の一時ファイルを削除します。RDSのストレージを食い続けないための後処理です。_cleanup.sqlUTL_FILE.FREMOVE.dmp.log を消し、最後に _countleft.sql で残存件数を数えてログに残します。

=1 を付けて実行したときだけ、_purge.sql で過去に消し損ねた export_* もまとめて削除します(検証用のオプションです)。

# ---- [4] remove temp dump on RDS ----
STEP="4-cleanup"
log "[4/4] Removing temp dump from RDS (DATA_PUMP_DIR) ..."
run_sql @/tmp/_cleanup.sql "$DUMP" "$DPLOG"
[ "$RC" -eq 0 ] || die 60 "temp dump removal | reason: $(reason_of "$OUT")"
if [ "$s" = "1" ]; then
  log "  あ=1: purging all export_* leftovers ..."
  sqlplus -S "$CONN" @/tmp/_purge.sql >>"$LOGFILE" 2>&1
fi
LEFT=$(sqlplus -S "$CONN" @/tmp/_countleft.sql 2>>"$LOGFILE" | tr -d '[:space:]')
log "  -> Dump remove finished. (remaining export_* files: ${LEFT})"

7/7 スクリプト実行後の後片付け

/tmp に書き出した7本のSQLを削除し、結果をログに出して終了します。

# ---- done ----
rm -f /tmp/_exp.sql /tmp/_size.sql /tmp/_upload.sql /tmp/_readlog.sql /tmp/_cleanup.sql /tmp/_purge.sql /tmp/_countleft.sql
STEP="done"
log "Process End.  [RESULT] SUCCESS  dump=${DUMP} size=${DUMP_MB}MB bucket=${BUCKET} log=${LOGFILE}"
exit 0

検証

検証環境準備

検証は、使い捨ての最小構成をCloudFormationスタックデプロイにて構築しました。毎回手で作らず、スタック1発で構築・破棄できるようにしています。

リソース 構成
VPC 新規・最小構成(EC2はパブリックサブネット、RDSはプライベートサブネット×2)
S3 VPCエンドポイント RDS → S3 アップロード用に配置
EC2 t2.micro / Amazon Linux 2023 / gp3 10GB、SQLPlus(Instant Client) を導入
RDS RDS for Oracle SE2 / db.t3.small / 19c / gp3 20GB / シングルAZ
その他 「事前設定」の各項目(オプショングループ・IAMロール・S3バケット・Secrets Manager)も本スタック内で作成

検証結果

ここまで、スクリプトの処理説明がかなり長くなってしまいましたが、、、ようやく検証結果の章になります。

実行前

RDS(DATA_PUMP_DIR)にダンプファイルはまだありません。

SQL> SELECT * FROM TABLE(rdsadmin.rds_file_util.listdir('DATA_PUMP_DIR'));

FILENAME              TYPE         FILESIZE MTIME
--------------------- ---------- ---------- ---------
datapump/             directory        4096 16-JUL-26

S3バケットも、空の状態です。(S3バケット名は一部マスキングしています)

s3_before_masked

実行

bash run_dump_verify.sh \
  "arn:aws:secretsmanager:ap-northeast-1:<ACCOUNT_ID>:secret:rds!db-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx-XXXXXX" \
  "<rds-endpoint>.ap-northeast-1.rds.amazonaws.com" \
  "<dump-bucket-name>"

実行後

ダンプ取得処理が終わると、RDS内にダンプができています。

-- ダンプ取得直後
SQL> SELECT * FROM TABLE(rdsadmin.rds_file_util.listdir('DATA_PUMP_DIR'));

FILENAME                        TYPE         FILESIZE MTIME
------------------------------- ---------- ---------- ---------
datapump/                       directory        4096 24-JUL-26
export_20260724_110756.dmp      file           380928 24-JUL-26
export_20260724_110756.log      file             1203 24-JUL-26

RDS→S3へダンプファイルがアップロードされました。

s3_after_masked

S3アップロードの成功後、一時ファイルもきちんと削除されていました。

-- 後処理後(.dmp/.log が消えている)
SQL> SELECT * FROM TABLE(rdsadmin.rds_file_util.listdir('DATA_PUMP_DIR'));

FILENAME                        TYPE         FILESIZE MTIME
------------------------------- ---------- ---------- ---------
datapump/                       directory        4096 24-JUL-26

スクリプト実行ログ(抜粋・固有情報はマスキング)

[2026-07-24 11:07:56] ==== Dump verification started (log=/tmp/run_dump_verify_20260724_110756.log) ====
[2026-07-24 11:07:56] [1/4] Getting DB credentials from Secrets Manager ...
[2026-07-24 11:07:56]   -> OK (user=****, db=ORCL, schema=ADMIN)
[2026-07-24 11:07:56] [2/4] Exporting dump (schema=ADMIN) ...
[2026-07-24 11:08:39]   -> Dump finished. File: export_20260724_110756.dmp  Size: .36 MB
[2026-07-24 11:08:39] [3/4] Uploading to S3 (bucket=<dump-bucket-name>) ...
[2026-07-24 11:08:39]   -> upload task started (task_id=xxxxxxxxxxxxx-44), waiting for completion (max 15min, poll 10s) ...
[2026-07-24 11:08:50]   -> S3 Upload finished. Location: s3://<dump-bucket-name>/export_20260724_110756.dmp  (verified present in S3)
[2026-07-24 11:08:50] [4/4] Removing temp dump from RDS (DATA_PUMP_DIR) ...
[2026-07-24 11:08:50]   -> Dump remove finished. (remaining export_* files: 0)
[2026-07-24 11:08:50] Process End.  [RESULT] SUCCESS  dump=export_20260724_110756.dmp size=.36MB bucket=<dump-bucket-name> log=...

※ ログの時刻はEC2のシステム時刻(UTC)です。前掲のS3スクリーンショットはマネジメントコンソールのJST表示なので、9時間ずれて見えます

確認できたこと

  1. DBMS_DATAPUMP によるダンプ取得 → rdsadmin_s3_tasks.upload_to_s3 によるS3アップロード → UTL_FILE.FREMOVE による一時ファイル削除が、一連の流れで一気通貫に回った
  2. 非同期アップロードの完了待ち(タスクログのポーリング)とS3の実在チェックを挟むことで、「アップロード前に消す」「成功と誤認する」事故は、ひとまず防げた
  3. ステップごとの終了コードにより、失敗時に「どこで落ちたか」がログと終了コードで分かる

気づき

  • S3アップロード処理は非同期upload_to_s3 の戻り値は「実行結果」ではなく「タスクID」です。実際にアップロードが完了したかどうかは、タスクログを見にいかないと分からないです
  • タスクログ自体も非同期で生成されるupload_to_s3 の直後にログを読むと、タイミングによっては ORA-29283(ファイルが存在しない)で落ちてしまいました・・・最初はこれを失敗として拾っていました。ログが生成されるまで少しの間待ったほうが良いです
  • SQLを書き出すときは終わりの目印をシングルクォートで囲む:シェルは $XXX を変数として展開してしまうため、DBMS_DATAPUMP.KU$_FILE_TYPE_DUMP_FILE のような $ を含むOracleの定数がそのままだと壊れます。<<'SQL' のように目印をシングルクォートで囲めば、中身が展開されず書いたままRDSへ渡せることができました
  • RDSではOSコマンドが使えない:ファイル一覧・サイズ・ログ読み取りも、すべてSQL(rds_file_util)経由になります

今後の課題

今回は「事前設定を揃えてS3にアップロードできること」の確認が目的だったため、スクリプトは目的を満たすレベルで作っています。実運用に向けては、次の点が課題だと思っています。

  • S3アップロードタスクの完了待ち方法:今回はタスクログの finished successfully という文字列を拾って完了判定しています(暫定)。文字列に依存するため、将来RDS側の出力文言が変わると検知できなくなる懸念があります
  • S3の実在チェックの精度:細かい話ですが、aws s3 ls s3://<bucket>/<key> は前方一致による一覧取得なので、厳密に「そのオブジェクトがあるか」を見るならもっと適切なやり方があるかもしれないです
  • Data Pumpのログの扱いp_prefix.dmp のファイル名を渡している都合上、S3に上がるのは .dmp だけです。一方で後処理では .dmp.log の両方を消しているため、証跡として残すなら、ログもS3へ上げるか別途保管する形にしたいところです
  • 終了コードの改善:今回の検証目的からは外れてしまうため、仮置きしています。(「完了待ちのタイムアウト」と「S3実在チェックのNG」がどちらも同じ終了コードだったりする)
  • スクリプトの分割検討:今回は検証目的で①〜④を1本のスクリプトにまとめました。実運用では処理ごと(取得/アップロード/後処理)に分割し、途中からの再実行やステップ単位のエラーハンドリングをしやすくする構成も検討したいところです

まとめ

RDS for Oracleのダンプ取得〜S3アップロードを、EC2上のシェルバッチとして作成し検証しました。実運用に向けた課題はあるものの、本記事の目的である「Oracleのダンプを取得してS3に上げられるか」は果たせたかなと思っています。

ポイントをまとめると、以下3点になります。

  • 処理の骨格は DBMS_DATAPUMPrdsadmin_s3_tasks.upload_to_s3UTL_FILE.FREMOVE の3ステップ
  • 処理を組む前に、事前設定がそれなりに必要(特にRDSとバッチサーバー側のEC2)
  • 実装上の工夫が必要なのはS3アップロードの非同期処理

本記事が、RDS for Oracleのダンプ取得〜S3アップロードの実装・運用をこれから検討される方の参考になれば幸いです。

この記事をシェアする

関連記事