AWS LambdaからIAM RoleのCredential情報を取得し、RedshiftのCOPY処理に利用する

2014.12.22

小ネタです。

先日投稿した以下AWS LambdaのエントリにてRedshiftへのCOPY処理を行う際のコード記述を行いましたが、この時はCredential情報(AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN)を別途用意したIAM Roleの情報を直打ちして処理していました。

何か方法があるはずよね、でもどうやるんだろう?とこの時は保留事項としていましたが、先日ブログのコメント欄に、片山 暁雄 a.k.a. #ヤマンさんから『認証情報、変数から取れますよ』とのコメントを頂き、取得方法及び取得した情報を用いてのRedshiftへのCOPY処理も確認出来たので、備忘録として残しておこうと思います。

目次

情報取得はAWS.Credentialsから

パッケージaws-sdkを取り込む事で利用出来る情報AWS.credentialから処理に必要な情報3つ(accessKeyId, secretAccessKey, sessionToken)を得ることが出来ます。(認証情報を用いるだけであれば、パッケージング処理は必要無く、Lambdaの管理コンソール上で試す事が出来ます)

var aws = require('aws-sdk');
:
console.log('[access_key_id]:' + aws.config.credentials.accessKeyId);
console.log('[secret_access_key]:' + aws.config.credentials.secretAccessKey);
console.log('[session_token]:' + aws.config.credentials.sessionToken);

実行結果は以下の様になります。

2014-12-22 00:37:44 UTC+9 2014-12-21T15:37:44.594Z	794efeac-8926-11e4-affd-bd21d15d47b9	[access_key_id]:ASIAXXXXXXXXXXXXXXX
2014-12-22 00:37:44 UTC+9 2014-12-21T15:37:44.594Z	794efeac-8926-11e4-affd-bd21d15d47b9	[secret_access_key]:xeGOXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2014-12-22 00:37:44 UTC+9 2014-12-21T15:37:44.594Z	794efeac-8926-11e4-affd-bd21d15d47b9	[session_token]:AQoD(中略)BQ==

利用するIAM Policyへの権限付与

後述するNode.jsプログラムをパッケージングし、実行(任意のS3バケット上にCSVファイルをアップロード)してみた結果、処理は実行されたものの途中でログの情報が途絶えてしまっていました。試しにそこで生成されたCOPY文をローカルで実行してみると、以下の様なログが表示されてしまいました。実行権限が足りなかったようです。

# COPY public.table_bbb \
# FROM 's3://xxxxxxxxxx/table_bbb/2014/12/21/query-earthquake-20141221_223919.csv' \
# CREDENTIALS 'aws_access_key_id=XXXXXXXXXX;aws_secret_access_key=YYYYYYYYYY;token=ZZZZZZZZZZ' \
# CSV IGNOREHEADER 1 TIMEFORMAT 'auto' DELIMITER ','; 
ERROR:  S3ServiceException:Access Denied,Status 403,Error AccessDenied,Rid XXXXXXXXXXXXXXXX,ExtRid XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX,CanRetry 1
DETAIL:  
  -----------------------------------------------
  error:  S3ServiceException:Access Denied,Status 403,Error AccessDenied,Rid XXXXXXXXXXX,ExtRid XXXXXXXXX,CanRetry 1
  code:      8001
  context:   Listing bucket=xxxxxxxxxx prefix=table_bbb/2014/12/21/query-earthquake-20141221_223919.csv
  query:     341932
  location:  s3_utility.cpp:529
  process:   padbmaster [pid=28089]
  -----------------------------------------------

取り敢えず、必要そうな権限として『S3フルアクセス』及び『Redshiftフルアクセス』の権限を別途追加したIAM Policyを作成、Lambda Function実行の際のPolicy Nameに指定してみました。(※実利用・本番環境で利用する際にはこの辺り権限を絞って定める必要があると思われます。今回はサンプル実行と言う事でざっくりな設定としてみました)

lambda-redshift-01

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:*"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    },
   {
      "Action": [
        "redshift:*",
        "ec2:DescribeAccountAttributes",
        "ec2:DescribeAddresses",
        "ec2:DescribeAvailabilityZones",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeSubnets",
        "ec2:DescribeVpcs",
        "ec2:DescribeInternetGateways",
        "sns:CreateTopic",
        "sns:Get*",
        "sns:List*",
        "cloudwatch:Describe*",
        "cloudwatch:Get*",
        "cloudwatch:List*",
        "cloudwatch:PutMetricAlarm",
        "cloudwatch:EnableAlarmActions",
        "cloudwatch:DisableAlarmActions"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

サンプルコードとコード実行(確認)

以下が実行時に利用したサンプルコードとなります。(Node.jsとしてのコードとして美しくない点についてはご了承ください...m(_ _;)m

利用した環境は先日エントリで用いたものを流用しました。指定バケットのtable_bbb配下日付フォルダにファイルをアップロードすると、指定のテーブル(ここではtable_bbb)にデータをCOPYするという流れです。

フォルダを作成した際にもイベントとして検知してしまうようなので、サンプルではファイル名を取れなかった場合はCOPY処理を実行しない様にしています。この辺りも実際の利用環境に応じて必要な制御を行う必要が出てくるでしょう。

console.log('Loading event');
var aws = require('aws-sdk');
var pg = require('pg');
var s3 = new aws.S3({apiVersion: '2006-03-01'});

// 接続文字列
var rsConnectionString = "tcp://<接続ユーザー名>:<接続パスワード>@<接続サーバ名>:<接続ポート番号>/<接続DB名>";

var rollback = function(client) {
  console.log("ERROR!");
  client.query('ROLLBACK', function() {
    client.end();
  });
};

exports.handler = function(event, context) {
   console.log('Received event:');
   console.log(JSON.stringify(event, null, '  '));
   // Get the object from the event and show its content type
   var bucket = event.Records[0].s3.bucket.name;
   console.log(bucket);
   var key = event.Records[0].s3.object.key;
   console.log(key);
   console.log(aws.config.credentials);
   s3.getObject({Bucket:bucket, Key:key},
      function(err,data) {
        if (err) {
           console.log('error getting object ' + key + ' from bucket ' + bucket + 
               '. Make sure they exist and your bucket is in the same region as this function.');
           context.done('error','error getting file'+err);
        }
        else {
            console.log('CONTENT TYPE:',data.ContentType);
            console.log('[access_key_id]:' + aws.config.credentials.accessKeyId);
            console.log('[secret_access_key]:' + aws.config.credentials.secretAccessKey);
            console.log('[session_token]:' + aws.config.credentials.sessionToken);
           
            var paths = key.split("/");
            console.log("-------------------------");
            console.log("tablename:" + paths[0]);
            console.log("year     :" + paths[1]);
            console.log("month    :" + paths[2]);
            console.log("day      :" + paths[3]);
            console.log("filename :" + paths[4]);
            console.log("-------------------------");
           
            var queryString = "";
            queryString += "COPY public." + paths[0] + " ";
            queryString += "FROM \'s3://" + bucket + "/" + key + "\' ";
            queryString += "CREDENTIALS \'aws_access_key_id=" + aws.config.credentials.accessKeyId + ";aws_secret_access_key=" + aws.config.credentials.secretAccessKey + ";token=" + aws.config.credentials.sessionToken + "\' "
            queryString += "CSV ";
            queryString += "IGNOREHEADER 1 ";
            queryString += "TIMEFORMAT \'auto\' ";
            queryString += "DELIMITER \',\'; ";
            console.log(queryString);

            if (paths[4] == "") {
              console.log("not csv files.");
            } else {
              console.log("it is csv files.");
              // 接続開始
              var client = new pg.Client(rsConnectionString);
              console.log(rsConnectionString);
              client.connect();
              console.log("connect go.");

              // クエリ実行(1).COPY
              client.query(queryString, function(err, result) {
                if(err) { console.log(err); return rollback(client); }
                console.log("COPY Operation Done.");

                // クエリ実行(2).COMMIT
                client.query("COMMIT;", client.end.bind(client));
                console.log("transaction commit.");

                // トランザクション終了
                client.end();
                console.log("transaction end.");

              });
              context.done(null,'');
            }
            
        }
      }
   );
};

実行前のデータベースを確認してみます。件数は0件です。

# SELECT COUNT(*) FROM public.table_bbb;
 count 
-------
     0
(1 row)

ファイルを所定のバケットにアップロードし、実行ログを確認してみます。何度か処理が走ってますが、実際に接続に向かったのは1度だけであり、時間差で処理が進んでいる事が確認出来ています。

$ aws logs get-log-events \
--log-group-name /aws/lambda/copyS3CsvToRedshift \
--log-stream-name 8af25dcf008948b2ba0b13093358426b | jq -r '.events[].message' | grep connect
2014-12-21T16:25:41.598Z	00376a08-892e-11e4-b02a-3bac3e086988	connect go.
$ aws logs get-log-events \
--log-group-name /aws/lambda/copyS3CsvToRedshift \
--log-stream-name 8af25dcf008948b2ba0b13093358426b | jq -r '.events[].message'
2014-12-21T16:25:23.675Z	2ab9hi0ns7vkgexw	Loading event START RequestId: f6291af9-892d-11e4-a022-dd2e8d0539fa
2014-12-21T16:25:25.675Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	Received event:
2014-12-21T16:25:25.676Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	{}
2014-12-21T16:25:25.676Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	xxxxxx-xxxx-xxxxxx
2014-12-21T16:25:25.676Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	table_bbb/2014/12/22/
2014-12-21T16:25:25.676Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	{ expired: false,
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	CONTENT TYPE: binary/octet-stream
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	[access_key_id]:ASIAXXXXXXXXXXXXX
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	[session_token]:AQoXXXXXXXXXXXXXXXXX
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	-------------------------
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	tablename:table_bbb
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	year     :2014
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	month    :12
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	day      :22
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	filename :
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	-------------------------
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	COPY public.table_bbb FROM 's3://xxxxxx-xxxx-xxxxxx/table_bbb/2014/12/22/' CREDENTIALS 
2014-12-21T16:25:26.377Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	not csv files.
2014-12-21T16:25:39.855Z	00376a08-892e-11e4-b02a-3bac3e086988	{
:
2014-12-21T16:25:41.518Z	00376a08-892e-11e4-b02a-3bac3e086988	filename :query-earthquake-20141222_012502.csv
2014-12-21T16:25:41.518Z	00376a08-892e-11e4-b02a-3bac3e086988	-------------------------
2014-12-21T16:25:41.518Z	00376a08-892e-11e4-b02a-3bac3e086988	COPY public.table_bbb FROM 's3://xxxxxx-xxxx-xxxxxx/table_bbb/2014/12/22/query-
2014-12-21T16:25:41.518Z	00376a08-892e-11e4-b02a-3bac3e086988	it is csv files.
2014-12-21T16:25:41.596Z	00376a08-892e-11e4-b02a-3bac3e086988	tcp://xxxxx:XXXXXXXXXX@xxxxx.xxxxx.com:xxxx/XXXXXXXX
2014-12-21T16:25:41.598Z	00376a08-892e-11e4-b02a-3bac3e086988	connect go.
END RequestId: 00376a08-892e-11e4-b02a-3bac3e086988
REPORT RequestId: 00376a08-892e-11e4-b02a-3bac3e086988	Duration: 4005.84 ms	Billed Duration: 4100 ms 	Memory Size: 128 MB	Max Memory Used: 25 MB	
START RequestId: f6291af9-892d-11e4-a022-dd2e8d0539fa
2014-12-21T16:28:22.257Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	Received event:
:
2014-12-21T16:28:22.485Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	filename :
2014-12-21T16:28:22.485Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	-------------------------
2014-12-21T16:28:22.485Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	not csv files.
:
2014-12-21T16:28:23.775Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	COPY Operation Done.
2014-12-21T16:28:23.775Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	transaction commit.
2014-12-21T16:28:23.775Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	transaction end.
Process exited before completing request
END RequestId: f6291af9-892d-11e4-a022-dd2e8d0539fa
REPORT RequestId: f6291af9-892d-11e4-a022-dd2e8d0539fa	Duration: 1641.45 ms	Billed Duration: 1700 ms 	Memory Size: 128 MB	Max Memory Used: 27 MB	
START RequestId: f6291af9-892d-11e4-a022-dd2e8d0539fa
:
2014-12-21T16:31:25.197Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	filename :
2014-12-21T16:31:25.197Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	-------------------------
2014-12-21T16:31:25.197Z	f6291af9-892d-11e4-a022-dd2e8d0539fa	not csv files.
Process exited before completing request
END RequestId: f6291af9-892d-11e4-a022-dd2e8d0539fa
REPORT RequestId: f6291af9-892d-11e4-a022-dd2e8d0539fa	Duration: 3058.05 ms	Billed Duration: 3100 ms 	Memory Size: 128 MB	Max Memory Used: 14 MB	

ログファイルが出力されている事を確認後、改めて件数を確認してみます。所定の件数分、データがCOPYされているのが確認出来ました。

# SELECT COUNT(*) FROM public.table_bbb;
 count 
-------
 19337
(1 row)

まとめ

以上、AWSのCredential情報を直書き設定では無く、IAM Role経由で取得し、その情報を以ってAmazon RedshiftへのCOPY処理を行う実行サンプルの御紹介でした。細かい設定や制御は内容を詰めて行く必要がありますが、これでセキュリティの面でも不安要素が取り除けますね。こちらからは以上です。