AWS Lambda上でAthena-Expressを使おうとして上手く動かなかった話

代替手段としてAWS SDKを直接呼び出すべきという結論となりました。
2022.10.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、CX事業本部 IoT事業部の若槻です。

Athena-Expressは、Athenaのクエリ実行と結果取得の処理をシンプルにすることができるAWS SDKのラッパーです。

前回および前々回の記事でも検証のため触ってみましたがコードの記述を大幅に簡略化できとても便利でした。

今回は、そのAthena-ExpressをAWS Lambda上で使おうとして上手く動かなかった話についてです。

環境

$  npm ls aws-sdk athena-express
aws-cdk-app@0.1.0 /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/aws-cdk-app
├── athena-express@7.1.5
└── aws-sdk@2.1238.0

事象

次のLambdaハンドラーのコードでAthena-ExpressによるAthenaクエリの実行を行おうとしました。

src/athena-query-func-handler.ts

import { AthenaExpress } from 'athena-express';
import * as AWS from 'aws-sdk';

const ATHENA_WORK_GROUP_NAME = process.env.ATHENA_WORK_GROUP_NAME || '';
const GLUE_DATABASE_NAME = process.env.GLUE_DATABASE_NAME || '';
const SOURCE_GLUE_TABLE = process.env.SOURCE_GLUE_TABLE || '';

AWS.config.update({ region: 'ap-northeast-1' });

const athenaExpress = new AthenaExpress({
  aws: AWS,
  workgroup: ATHENA_WORK_GROUP_NAME,
});

export const handler = async (): Promise<void> => {
  const sqlQuery = `SELECT * FROM ${GLUE_DATABASE_NAME}.${SOURCE_GLUE_TABLE}`;

  const results = await athenaExpress.query(sqlQuery);

  console.log(results);
};

デプロイはAWS CDKで行いました。(使用したCDKスタックのコードは記事末尾に載せてあります)

このLambdaを実行したところ、次のエラーが発生しました。

{
  "errorType": "Error",
  "errorMessage": "InvalidRequestException: Bucket name should not contain uppercase characters",
  "trace": [
    "Error: InvalidRequestException: Bucket name should not contain uppercase characters",
    "    at AthenaExpress.query (/var/task/index.js:8023:17)",
    "    at processTicksAndRejections (internal/process/task_queues.js:95:5)",
    "    at async Runtime.handler (/var/task/index.js:8058:19)"
  ]
}

InvalidRequestException: Bucket name should not contain uppercase charactersとあり、作成されるバケット名に大文字が含まれているというエラーです。CDKでリソースを作成するタイミングならまだしも、Lambda実行時にこのエラーが出るというのはなんとも不思議ですね。

調査、結論

ビルドされたLambdaのコード(≒Athena-Expressのソース・コード)を精査してみると、Athena-Expressが一時領域としてバケットを作成している動作が見て取れました。

    s3Bucket: init.s3 || `s3://athena-express-${init.aws.config.credentials.accessKeyId.substring(0, 10).toLowerCase()}-${new Date().getFullYear()}`

ちなみに以前にローカル環境でAthena-Expressを実行した際はathena-express-asiaul6wvp-2022という名前のバケットが作成されていました。(当時は気が付きませんでした)

ドキュメントを読むと、s3パラメータを指定しない場合にAthena-Expressは新しいバケットを作成するとのことです。

The location in Amazon S3 where your query results are stored, such as s3://path/to/query/bucket/. athena-express will create a new bucket for you if you don't provide a value for this param but sometimes that could cause an issue if you had recently deleted a bucket with the same name. (something to do with cache). When that happens, just specify you own bucket name. Alternatively you can also use workgroup.

解決策はいくつかありそうですが、ここではinit.s3を明示的に指定してみます。

import { AthenaExpress } from 'athena-express';
import * as AWS from 'aws-sdk';

const ATHENA_WORK_GROUP_NAME = process.env.ATHENA_WORK_GROUP_NAME || '';
const GLUE_DATABASE_NAME = process.env.GLUE_DATABASE_NAME || '';
const SOURCE_GLUE_TABLE = process.env.SOURCE_GLUE_TABLE || '';
const ATHENA_EXPRESS_TEMP_BUCKET_NAME =
  process.env.ATHENA_EXPRESS_TEMP_BUCKET_NAME || '';

AWS.config.update({ region: 'ap-northeast-1' });

const athenaExpress = new AthenaExpress({
  aws: AWS,
  workgroup: ATHENA_WORK_GROUP_NAME,
  s3: `s3://${ATHENA_EXPRESS_TEMP_BUCKET_NAME}/express/`,
});

export const handler = async (): Promise<void> => {
  const sqlQuery = `SELECT * FROM ${GLUE_DATABASE_NAME}.${SOURCE_GLUE_TABLE}`;

  const results = await athenaExpress.query(sqlQuery);

  console.log(results);
};

またs3パラメータの指定値を確認するため出力するようにしてみます。

しかし引き続き同じエラーが発生します。またそもそもinit.s3を使う場合でも使わない場合でもバケット名に大文字は含まれていません。

2022-10-25T05:41:01.832Z	undefined	INFO	s3://query-result-bucket-123456789012/express
2022-10-25T05:41:01.833Z	undefined	INFO	s3://athena-express-asiaul6wvp-2022

いよいよ分からなくなったので、ここでconsole.log()を追加しながら愚直にエラー発生行を追跡してみると、AWS SDKのathena:startQueryExecutionの呼び出し時にエラーが発生していました。

      await config2.athena.startQueryExecution(params).promise();

athena:startQueryExecutionの実行時にBucket name should not contain uppercase charactersが発生するというのはますますよく分からないですね。。。

さらに踏み込んだデバッグは必要ですが、こうなるとAthena-ExpressというよりはAWS SDKも絡んだ問題である可能性もあります。

代替手段

差し当たりの代替手段としては、Athena-Expressを使わずにAWS SDKを直接使用するようにします。次の記事でも紹介していますのでご参照ください。

おまけ

CDKコード

環境構築はAWS CDKで行いました。使用したCDKスタックのコードをおまけとして残しておきます。

lib/aws-cdk-app-stack.ts

import {
  aws_athena,
  aws_lambda_nodejs,
  aws_s3,
  aws_iam,
  Duration,
  RemovalPolicy,
  Stack,
  StackProps,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as aws_glue_alpha from '@aws-cdk/aws-glue-alpha';

export class AwsCdkAppStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const queryResultBucket = new aws_s3.Bucket(this, 'queryResultBucket', {
      bucketName: `query-result-bucket-${this.account}`,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    const athenaWorkGroup = new aws_athena.CfnWorkGroup(
      this,
      'athenaWorkGroup',
      {
        name: 'athenaWorkGroup',
        workGroupConfiguration: {
          engineVersion: {
            selectedEngineVersion: 'Athena engine version 3',
          },
          resultConfiguration: {
            outputLocation: `S3://${queryResultBucket}/query-result`,
          },
        },
      }
    );

    const sourceBucket = new aws_s3.Bucket(this, 'sourcebucket', {
      bucketName: `source-bucket-${this.account}`,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    const glueDatabase = new aws_glue_alpha.Database(this, 'glueDatabase', {
      databaseName: 'gluedatabase',
    });

    const sourceGlueTable = new aws_glue_alpha.Table(this, 'sourceGlueTable', {
      tableName: 'source_glue_table',
      database: glueDatabase,
      bucket: sourceBucket,
      s3Prefix: 'data/',
      dataFormat: aws_glue_alpha.DataFormat.JSON,
      columns: [
        {
          name: 'deviceId',
          type: aws_glue_alpha.Schema.STRING,
        },
        {
          name: 'maxTemperature',
          type: aws_glue_alpha.Schema.FLOAT,
        },
      ],
    });

    const athenaQueryFunc = new aws_lambda_nodejs.NodejsFunction(
      this,
      'athenaQueryFunc',
      {
        functionName: 'athenaQueryFunc',
        entry: 'src/athena-query-func-handler.ts',
        timeout: Duration.seconds(30),
        environment: {
          GLUE_DATABASE_NAME: glueDatabase.databaseName,
          SOURCE_GLUE_TABLE: sourceGlueTable.tableName,
          ATHENA_WORK_GROUP_NAME: athenaWorkGroup.name,
          ATHENA_EXPRESS_TEMP_BUCKET_NAME: queryResultBucket.bucketName,
        },
      }
    );

    //権限を大きく取る
    athenaQueryFunc.addToRolePolicy(
      new aws_iam.PolicyStatement({
        actions: ['s3:*', 'athena:*', 'glue:*'],
        resources: ['*'],
      })
    );
  }
}

おわりに

AWS Lambda上でAthena-Expressを使おうとして上手く動かなかった話でした。

Lambda上でも当たり前のように動くだろうと考えていたため面食らいました。後ほどAthena-Expressに対してIssueを上げようと思います。進展があればまた共有させて頂きます。 とりあえずIssueに上げました。

以上