AnsibleでAmazon Linux 2023にdbt-athena環境を構築してみた
データアナリティクス事業本部 機械学習チームの鈴木です。
データマート作成に、dbt-athenaを使いたく、環境をどこに構築するか考えていました。
ローカル環境上に準備してもいいのですが、今後のことを考えてAnsibleでAmazon Linux 2023のインスタンス上に準備をしてみました。
EC2のあるVPC内からエンドポイント経由でAthenaに通信する必要もあるため、ネットワークも含めたCloudFormationテンプレート例も合わせてご紹介します。
この記事の内容について
Amazon Linux 2023のインスタンス上にdbt-athena環境を構築し、動作確認を行いました。構築は再現できるようAnsibleで行いました。
dbtのドキュメントのAthena setupページで以下のdbt-athena-community
を使うように案内があったため、このアダプタを使いました。
dbt Coreの利用は初めてだったので、設定と動作確認は以下の資料を参考に進めました。
なお、Amazon Linux 2023にAnsibleで環境構築する例は、以前に以下のブログを公開していました。Ansibleの実行も記事内で行いますが、手順はこのブログを踏襲しています。
やってみる
1. AWSのリソース作成
キーペアの作成
まず、EC2にSSH接続するためのキーペアを作成しました。CloudFormationテンプレートでまとめて作ってもいいのですが、検証中作成の旅にキーファイルをローカルに配置し直すのが手間なので分けて作成しました。
名前はなんでもいいですが、今回は分かりやすいのでcm-nayuts-dbt-athena
としました。
作成するとキーファイルが自動的にダウンロードされるので、~/.ssh/dbt-athena/cm-nayuts-dbt-athena.pem
のように分かる場所に配置しました。
IAMロールの作成
以下のymlファイルを使って、IAMロール用のCloudFormationスタックを作成しました。この後に作成するEC2インスタンスおよびネットワークのテンプレートに混ぜてもよいのですが、ネットワークは料金の関係でこまめに消したかった一方で、IAMロールはそうではなかったので別にしました。
AWSTemplateFormatVersion: "2010-09-09" Parameters: EnvironmentName: Type: String Resources: EC2IAMRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${EnvironmentName}-role AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore - arn:aws:iam::aws:policy/AmazonAthenaFullAccess - arn:aws:iam::aws:policy/AmazonS3FullAccess
ポリシーは検証のため強めの権限をアタッチしています。設計時には必要なものに絞って頂ければと思います。
EC2およびネットワークの作成
以下のymlファイルを使って、EC2インスタンスおよびネットワークのCloudFormationスタックを作成しました。
少し長いのでトグルに隠しておきます。(※ ▶️を押すと開きます。)
EC2インスタンスおよびネットワークのテンプレート
AWSTemplateFormatVersion: "2010-09-09" Parameters: EnvironmentName: Type: String VPCCIDR: Type: String Default: 10.192.0.0/16 PublicSubnetCIDR: Type: String Default: 10.192.1.0/24 PrivateSubnetCIDR: Type: String Default: 10.192.0.0/24 Ec2ImageId: Type: AWS::SSM::Parameter::Value<String> Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 Ec2InstanceType: Type: String Default: t3.micro KeyPair: Type: String Default: xxxxx-key EC2RoleName: Type: String Resources: VPC: Type: AWS::EC2::VPC Properties: CidrBlock: !Ref VPCCIDR EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub ${EnvironmentName}-VPC # InternetGateway InternetGateway: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: !Sub ${EnvironmentName}-igw AttachGateway: Type: AWS::EC2::VPCGatewayAttachment Properties: VpcId: !Ref VPC InternetGatewayId: !Ref InternetGateway NatGateway: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGatewayEIP.AllocationId SubnetId: !Ref PublicSubnet Tags: - Key: Name Value: !Sub ${EnvironmentName}-ngw NatGatewayEIP: Type: AWS::EC2::EIP Properties: Domain: vpc # Public Subnetのネットワーク設定 PublicSubnet: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select [0, !GetAZs ""] CidrBlock: !Ref PublicSubnetCIDR VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-PublicSubnet PublicRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-PublicRouteTable PublicRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PublicRouteTable DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref InternetGateway PublicSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PublicSubnet RouteTableId: !Ref PublicRouteTable # Private Subnetのネットワーク設定 PrivateSubnet: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Select [0, !GetAZs ""] CidrBlock: !Ref PrivateSubnetCIDR VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-PrivateSubnet PrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-PrivateRouteTable PrivateRoute: Type: AWS::EC2::Route Properties: RouteTableId: !Ref PrivateRouteTable DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NatGateway PrivateSubnetRouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnet RouteTableId: !Ref PrivateRouteTable # エンドポイントの設定 EndpointSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: EndpointSecurityGroup VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-EndpointSecurityGroup SecurityGroupIngress: - IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: !Ref VPCCIDR EndpointSSM: Type: AWS::EC2::VPCEndpoint Properties: PrivateDnsEnabled: true SecurityGroupIds: - !Ref EndpointSecurityGroup ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm SubnetIds: - !Ref PrivateSubnet VpcEndpointType: Interface VpcId: !Ref VPC EndpointSSMMessages: Type: AWS::EC2::VPCEndpoint Properties: PrivateDnsEnabled: true SecurityGroupIds: - !Ref EndpointSecurityGroup ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages SubnetIds: - !Ref PrivateSubnet VpcEndpointType: Interface VpcId: !Ref VPC EndpointEC2Messages: Type: AWS::EC2::VPCEndpoint Properties: PrivateDnsEnabled: true SecurityGroupIds: - !Ref EndpointSecurityGroup ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages SubnetIds: - !Ref PrivateSubnet VpcEndpointType: Interface VpcId: !Ref VPC EndpointAthena: Type: AWS::EC2::VPCEndpoint Properties: PrivateDnsEnabled: true SecurityGroupIds: - !Ref EndpointSecurityGroup ServiceName: !Sub com.amazonaws.${AWS::Region}.athena SubnetIds: - !Ref PrivateSubnet VpcEndpointType: Interface VpcId: !Ref VPC EndpointS3: Type: AWS::EC2::VPCEndpoint Properties: RouteTableIds: - !Ref PrivateRouteTable ServiceName: !Sub com.amazonaws.${AWS::Region}.s3 VpcEndpointType: Gateway VpcId: !Ref VPC EC2InstanceProfile: Type: AWS::IAM::InstanceProfile Properties: Path: / Roles: - Ref: EC2RoleName InstanceProfileName: !Sub ${EnvironmentName}-EC2InstanceProfile EC2SecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: EC2SecurityGroup VpcId: !Ref VPC Tags: - Key: Name Value: !Sub ${EnvironmentName}-EC2SecurityGroup EC2Instance: Type: AWS::EC2::Instance Properties: InstanceType: !Ref Ec2InstanceType SubnetId: !Ref PrivateSubnet ImageId: !Ref Ec2ImageId SecurityGroupIds: - !Ref EC2SecurityGroup IamInstanceProfile: !Ref EC2InstanceProfile BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeSize: 50 VolumeType: gp3 EbsOptimized: true SourceDestCheck: true KeyName: !Ref KeyPair Tags: - Key: Name Value: !Sub ${EnvironmentName}-EC2Instance Outputs: VPC: Value: !Ref VPC Export: Name: !Sub ${EnvironmentName}-VPC VPCCIDR: Value: !Ref VPCCIDR Export: Name: !Sub ${EnvironmentName}-VPCCIDR PrivateSubnet: Value: !Ref PrivateSubnet Export: Name: !Sub ${EnvironmentName}-PrivateSubnet PrivateRouteTable: Value: !Ref PrivateRouteTable Export: Name: !Sub ${EnvironmentName}-PrivateRouteTable SecurityGroup: Value: !Ref EndpointSecurityGroup Export: Name: !Sub ${EnvironmentName}-EndpointSecurityGroup EndpointSSM: Value: !Ref EndpointSSM Export: Name: !Sub ${EnvironmentName}-EndpointSSM EndpointSSMMessages: Value: !Ref EndpointSSMMessages Export: Name: !Sub ${EnvironmentName}-EndpointSSMMessages EndpointEC2Messages: Value: !Ref EndpointEC2Messages Export: Name: !Sub ${EnvironmentName}-EndpointEC2Messages EndpointS3: Value: !Ref EndpointS3 Export: Name: !Sub ${EnvironmentName}-EndpointS3 EndpointAthena: Value: !Ref EndpointAthena Export: Name: !Sub ${EnvironmentName}-EndpointAthena EC2SecurityGroup: Value: !Ref EC2SecurityGroup Export: Name: !Sub ${EnvironmentName}-EC2SecurityGroup EC2Instance: Value: !Ref EC2Instance Export: Name: !Sub ${EnvironmentName}-EC2Instance
今回のポイントはAthenaのエンドポイントをVCPに作成したことでした。Glueのエンドポイントも必要かなと考えていましたが、以下のドキュメントで具体的に記載されていたのはAthenaエンドポイントのみでしたので、Athenaに直接関係するエンドポイントはこれのみ作成しました。今回試した内容ではこれで問題ありませんでした。
EC2インスタンスへのSSH接続確認
デプロイしたEC2インスタンスはセッションマネージャー経由でSSH接続できるので、初回接続しました。
.ssh/config
に以下を追記しました。ただし、インスタンスID
・プロファイル名
・秘密鍵のパス
は作成したインスタンスやローカル環境に設定しているものに置き換えました。
host cm-nayuts-dbt-athena ProxyCommand sh -c "aws ssm start-session --target <インスタンスID> --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --profile <プロファイル名> --region ap-northeast-1" User ec2-user IdentityFile <秘密鍵のパス>
sshコマンドで接続すると、ログインできました。
ssh cm-nayuts-dbt-athena
, #_ ~\_ ####_ Amazon Linux 2023 ~~ \_#####\ ~~ \###| ~~ \#/ ___ https://aws.amazon.com/linux/amazon-linux-2023 ~~ V~' '-> ~~~ / ~~._. _/ _/ _/ _/m/' Last login: Mon Aug 14 05:17:01 2023 from 127.0.0.1
AthenaおよびGlueのリソース作成
dbt-athenaからのAthenaでの処理実行に必要な、以下のリソースを作成しました。
- Glueデータベース
- 動作確認に使うGlueテーブル
- dbtから実行したAthenaのクエリ結果を保存するS3バケット
- dbtが使うAthenaのワークグループ
このリソースは説明と動作確認に使いたいだけなので、詳細はこだわりません。
特に動作確認に使うGlueテーブルは、よく検証で使っているUCI Machine Learning RepositoryのIris Data Setが入ったiris
テーブルを作成して使いました。
なお、データセットのリンクは以下になります。
- https://archive.ics.uci.edu/ml/datasets/iris
Athenaのエディタで見るとこのようになる状態にしました。
2. Ansibleによるインスタンスの環境構築
先ほどインスタンスを作成したので、Ansibleを使って環境を構築していきます。
Ansible用のファイルの作成
ローカルでansible
ディレクトリを作成し、以下のようにファイルを作成しました。
ansible
ディレクトリを作成し、以下のようにファイルを作成しました。
tree ansible # . # ├── ansible.cfg # ├── hosts # ├── playbook.yml # └── ssh_config
プレイブックの作成
playbook.yml
ファイルは以下のようにしました。
- hosts: cm-nayuts-dbt-athena tasks: - name: Install python3 ansible.builtin.dnf: name: python3.11-3.11.2-2.amzn2023.0.7.x86_64 state: present - name: Install python3-pip ansible.builtin.dnf: name: python3.11-pip-22.3.1-2.amzn2023.0.2.noarch state: present - name: Install Git ansible.builtin.dnf: name: git-2.40.1-1.amzn2023.0.1.x86_64 state: present - name: pip dbt-athena ansible.builtin.pip: name: - dbt-athena-community==1.5.1 executable: pip3.11
Amazon Linux2023にはシステムPythonとしてPython3.9が入っていますが、dbtのインストールに使うためにPython3.11をインストールしました。
同じくインストールしたpip3.11
を使ってdbt-athena-community
をインストールしました。dbt-core
をはじめとした依存するパッケージはdbt-athena-community
と一緒にインストールされる旨が以下のガイドに記載されていました。
name
にはバージョンまで記載しましたが、dnf search
の--showduplicates
オプションでバージョンまで表示して確認しました。特定バージョンをインストールできれば良かったのでstate
はpresent
としました。
プレイブック以外のファイルの作成
ansible.cfg
ファイルは以下のようにしました。
[defaults] inventory = hosts [privilege_escalation] become = True [ssh_connection] control_path = %(directory)s/%%h-%%r ssh_args = -o ControlPersist=15m -F ssh_config -q scp_if_ssh = True
ssh_config
ファイルは以下のようにしました。ただし、インスタンスID
・プロファイル名
・秘密鍵のパス
は作成したインスタンスやローカル環境に設定しているものに置き換えました。
host cm-nayuts-dbt-athena ProxyCommand sh -c "aws ssm start-session --target <インスタンスID> --document-name AWS-StartSSHSession --parameters 'portNumber=%p' --profile <プロファイル名> --region ap-northeast-1" User ec2-user IdentityFile <秘密鍵のパス>
hosts
ファイルは以下のようにしました。
[cm-nayuts-dbt-athena] cm-nayuts-dbt-athena
プレイブックの実行
以下のようにプレイブックを実行しました。
# playbook.ymlのあるディレクトリに移動 cd ansible # プレイブックの実行 ansible-playbook ./playbook.yml
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details [WARNING]: Found both group and host with same name: cm-nayuts-dbt-athena PLAY [cm-nayuts-dbt-athena] *************************************************************************************************** TASK [Gathering Facts] ******************************************************************************************************** [WARNING]: Platform linux on host cm-nayuts-dbt-athena is using the discovered Python interpreter at /usr/bin/python3.11, but future installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible- core/2.14/reference_appendices/interpreter_discovery.html for more information. ok: [cm-nayuts-dbt-athena] TASK [Install python3] ******************************************************************************************************** ok: [cm-nayuts-dbt-athena] TASK [Install python3-pip] **************************************************************************************************** ok: [cm-nayuts-dbt-athena] TASK [Install Git] ************************************************************************************************************ ok: [cm-nayuts-dbt-athena] TASK [pip dbt-athena] ********************************************************************************************************* ok: [cm-nayuts-dbt-athena] PLAY RECAP ******************************************************************************************************************** cm-nayuts-dbt-athena : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
EC2にログインしてdnf history
を実行すると、インストールが実行されていることが確認できました。
また、pipでインストールしたdbtのバージョンは以下でした。
pip3.11 freeze | grep -e "dbt-core" -e "dbt-athena-community" -e "dbt-extractor" # dbt-athena-community==1.5.1 # dbt-core==1.5.4 # dbt-extractor==0.4.1
3. dbtの初期設定
dbt initの実行
インスタンスにログインし、dbt init
を実行しました。ログにあるようにいくつか質問をされるので答えていきました。
dbt init test_project
07:51:21 Running with dbt=1.5.4 Which database would you like to use? [1] athena (Don't see the one you want? https://docs.getdbt.com/docs/available-adapters) Enter a number: 1 s3_staging_dir (S3 location to store Athena query results and metadata, e.g. s3://athena_query_result/prefix/): s3://バケット名/dbt-athena-staging/ s3_data_dir (S3 location where to store data/tables, e.g. s3://bucket_name/prefix/): s3://バケット名/dbt-athena-data/ region_name (AWS region of your Athena instance): ap-northeast-1 schema (Specify the schema (Athena database) to build models into (lowercase only)): cm-nayuts-sample-db database (Specify the database (Data catalog) to build models into (lowercase only)) [awsdatacatalog]: awsdatacatalog threads (1 or more) [1]: 1 07:52:21 Profile test_project written to /home/ec2-user/.dbt/profiles.yml using target's profile_template.yml and your supplied values. Run 'dbt debug' to validate the connection. 07:52:21 Your new dbt project "test_project" was created! For more information on how to configure the profiles.yml file, please consult the dbt documentation here: https://docs.getdbt.com/docs/configure-your-profile One more thing: Need help? Don't hesitate to reach out to us via GitHub issues or on Slack: https://community.getdbt.com/ Happy modeling!
入力した内容に沿って~/.dbt/profiles.yml
と~/test_project
が作成されました。Athena実行時にはワークグループを指定したかったため、~/.dbt/profiles.yml
は以下のようにwork_group
を追記しました。
test_project: outputs: dev: database: awsdatacatalog region_name: ap-northeast-1 s3_data_dir: s3://バケット名/dbt-athena-data/ s3_staging_dir: s3://バケット名/dbt-athena-staging/ schema: cm-nayuts-sample-db threads: 1 type: athena work_group: dev target: dev
モデルの作成
以下のようにtest_project/models/iris_dbt.sql
を作成しました。
with iris_dbt as ( select sepal_length, sepal_width, petal_length, petal_width, class from iris ) select * from iris_dbt
『AWSのリソース作成』で記載したテーブルからデータをSELECTするだけのモデルです。
サンプルのモデルは不要だったので削除しておきました。
rm -r test_project/models/example
4. 処理の実行
ビュー・マテリアライゼーションを使う
dbt run
を実行し、Athena側にビュー(デフォルト設定なので)が作成されることを確認しました。
dbt run
08:01:44 Running with dbt=1.5.4 08:01:44 Registered adapter: athena=1.5.1 08:01:45 Unable to do partial parsing because profile has changed 08:01:46 [WARNING]: Configuration paths exist in your dbt_project.yml file which do not apply to any resources. There are 1 unused configuration paths: - models.test_project.example 08:01:46 Found 1 model, 0 tests, 0 snapshots, 0 analyses, 336 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups 08:01:46 08:01:47 Concurrency: 1 threads (target='dev') 08:01:47 08:01:47 1 of 1 START sql view model cm-nayuts-sample-db.iris_dbt ....................... [RUN] 08:01:49 1 of 1 OK created sql view model cm-nayuts-sample-db.iris_dbt .................. [OK -1 in 1.65s] 08:01:49 08:01:49 Finished running 1 view model in 0 hours 0 minutes and 3.02 seconds (3.02s). 08:01:49 08:01:49 Completed successfully 08:01:49 08:01:49 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1
クエリ履歴からは、以下のようなビュー作成とテーブルプロパティの変更が実施されたことを確認しました。
-- /* {"app": "dbt", "dbt_version": "1.5.4", "profile_name": "test_project", "target_name": "dev", "node_id": "model.test_project.iris_dbt"} */ create or replace view "awsdatacatalog"."cm-nayuts-sample-db"."iris_dbt" as with iris_dbt as ( select sepal_length, sepal_width, petal_length, petal_width, class from iris ) select * from iris_dbt
テーブル・マテリアライゼーションを使う
dbt_project.yml
を開き、モデルのマテリアライズの設定を修正しました。
(略) models: test_project: +materialized: table
dbt run
を実行し、Athena側にテーブルが作成されることを確認しました。
dbt run
08:23:33 Running with dbt=1.5.4 08:23:33 Registered adapter: athena=1.5.1 08:23:33 Found 1 model, 0 tests, 0 snapshots, 0 analyses, 336 macros, 0 operations, 0 seed files, 0 sources, 0 exposures, 0 metrics, 0 groups 08:23:33 08:23:35 Concurrency: 1 threads (target='dev') 08:23:35 08:23:35 1 of 1 START sql table model cm-nayuts-sample-db.iris_dbt ...................... [RUN] 08:23:39 1 of 1 OK created sql table model cm-nayuts-sample-db.iris_dbt ................. [OK 0 in 3.97s] 08:23:39 08:23:39 Finished running 1 table model in 0 hours 0 minutes and 5.41 seconds (5.41s). 08:23:39 08:23:39 Completed successfully 08:23:39 08:23:39 Done. PASS=1 WARN=0 ERROR=0 SKIP=0 TOTAL=1
クエリ履歴からは、以下のようなテーブル作成とテーブルプロパティの変更が実施されたことを確認しました。
-- /* {"app": "dbt", "dbt_version": "1.5.4", "profile_name": "test_project", "target_name": "dev", "node_id": "model.test_project.iris_dbt"} */ create table "awsdatacatalog"."cm-nayuts-sample-db"."iris_dbt" with ( table_type='hive', is_external=true,external_location='s3://バケット名/dbt-athena-data/cm-nayuts-sample-db/iris_dbt/45643a21-ec97-4f08-ab26-62c5e8061024', format='parquet' ) as with iris_dbt as ( select sepal_length, sepal_width, petal_length, petal_width, class from iris ) select * from iris_dbt
alter table `cm-nayuts-sample-db`.`iris_dbt` set tblproperties ('classification' = 'parquet')
最後に
dbt-athenaの実行環境を構築するために、Amazon Linux 2023のEC2インスタンスにAnsibleで必要なソフトウェアをインストールしました。
また、動作確認として簡単なAthenaへのクエリ実行例を紹介しました。
実行環境としてはEC2ではないことも多いと思いますが、気軽にSSHして動作確認できる開発環境として使うのはいいかもしれないですね。
参考になりましたら幸いです。
そのほかに参考にした文献
- Athena setup | dbt Developer Hub
- dbtで始めるデータパイプライン構築〜入門から実践〜
- DNF でインストールやアップデートした履歴を確認してみた | DevelopersIO
- dnf コマンドの使い方メモ - Qiita
- ansible.builtin.dnf module – Manages packages with the dnf package manager — Ansible Documentation
- ansible.builtin.pip module – Manages Python library dependencies — Ansible Documentation
- タスクの実行順序-φ(.. ) のメモ