AWS Data PipelineのShellCommandActivityでDockerイメージを（暫定的に）動かす

# 背景・目的

我々のチームでは、以前から[AWS Data Pipeline](https://aws.amazon.com/jp/datapipeline/)を利用して、次のような形でETLのプログラムを管理していました。

* RubyやPythonや必要なツールが入ったAMIを用意する
* ETLプログラムはチーム管理サーバーのGitlabで管理し、masterブランチへのpush時にJenkinsでzipで固めてS3にアップロードする
* Data Pipelineの[ShellCommandActivity](https://docs.aws.amazon.com/ja_jp/datapipeline/latest/DeveloperGuide/dp-object-shellcommandactivity.html)でEC2サーバーを立ち上げ、S3からソースコードをダウンロードして実行する

ただ、数年間の運用を経て、以下のような問題点が出てきています。

* AMIのバージョンアップが面倒で、できればコンテナ化してDockerfileで管理したいこと。Data Pipeline自体にはDockerイメージの実行機能が無いこと
* 全社的にGithubのOrganizationが標準になったが準拠できておらず、独自管理のGitlabやJenkinsが負担になっていること
* （これは我々の使い方が悪いのですが）Data Pipelineを単なるcron機能しか使っておらず、バッチの依存関係が定義できていないこと

ただ、システム全体を一気に移行するのは大変なので、次のような方針で実行しようとしています。

* まず（特に新規の）バッチ処理をコンテナ化してGithubに移行し、Github ActionsでECRにpushする
* ひとまずData Pipelineでそのコンテナイメージを実行するようにする
* その後、DataPipeline自体は後で適切なワークフローエンジンとコンテナ実行基盤を利用した形に移行する

一旦Data Pipelineを利用することにしたのは、バッチの再実行方法など運用が今までと大きく変わらないようにして、無理なくプロジェクトを進めるのが目的です。そのためあまりきれいな形ではなく、より適切なサービス（AWSでは[AWS Batch](https://aws.amazon.com/jp/batch/)や[MWAA](https://aws.amazon.com/jp/blogs/news/introducing-amazon-managed-workflows-for-apache-airflow-mwaa/)など）を利用すべきところかもしれませんが、一旦はData Pipeline上でコンテナイメージを実行する方法を調べて実装しました。

また、「[DataPipelineを使ってdockerコンテナを定期実行してみる](https://qiita.com/Yarimizu14/items/f1268acf637940e524cd)」という記事を見つけたのですが、とりあえずの運用のためにECSクラスターを導入するのも大変なので、Data Pipelineで起動したEC2インスタンス自体にDockerを実行させる方法にしています。


# 実装方法

次のような方針で実装しました。

* 前提としてGithub Actionsを利用して、Dockerイメージ自体はECRに既にpushされている状態
* AMIは素のAmazon Linux 2 (idは `ami-00f045aed21a55240` 64 ビット x86) を利用する
* 認証情報などは、パラメータストアにkmsで暗号化した形で保存し、そこから取得して環境変数やファイル形式でイメージ内に渡す
  * 詳しくは公式ドキュメントの「[AWS Systems Manager Parameter Store で AWS KMS を使用する方法](https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/services-parameter-store.html)」を読んでください
  * 余談ですが「余力があれば、環境変数よりコマンドラインのオプションとして渡したほうがいい。一般的にコマンドラインパーサーのほうがバリデーションがしっかりしているため」というアドバイスも貰っています


## ShellCommandActivityの実装

次のようなシェルスクリプトで実装しました。以下が注意点です。

* `aws ssm get-parameter`で、テキストのみ取得するためのオプションは[こちらの記事](https://stackoverflow.com/questions/48226592/aws-ssm-get-parameter-rsa-key-output-to-file)を参考にした
* ほとんどのバッチでBigQueryを利用しているため、GCPのcredentialsをファイルに吐き出してイメージ内に渡している
* Dockerイメージ内でawscliやAWS SDKを利用する場合、`AWS_ACCESS_KEY_ID`と`AWS_SECRET_ACCESS_KEY`としてコンテナ内に渡す必要がある。事前に適切な権限を持ったIAMユーザーを作る必要がある
* `set -eu`を行うことで、エラーが出た行で処理を止めることができる。詳しくは「[シェルスクリプトを書くときはset -euしておく](https://qiita.com/youcune/items/fcfb4ad3d7c1edf9dc96)」を参照。
* awscli2のインストール方法がx86かARMかでも違うので、[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-cliv2.html)を参照してください

もう少しスッキリ書けそうな気もしますが、ひとまずこれで行ってます。

```bash
set -eu

account="{AWSアカウントID}"
region="{リージョン名}"
repository="{ECRのリポジトリ名}"
tag="latest"
batch="{Dockerの引数}"

sudo yum -y update

# パラメータストアから環境変数や設定ファイルを読み込む
IMAGE_AWS_ACCESS_KEY_ID=`aws ssm get-parameter --name "{パラメータストアのキー名}" \
  --with-decryption --region "${region}" --output text --query Parameter.Value`
IMAGE_AWS_SECRET_ACCESS_KEY=`aws ssm get-parameter --name "{パラメータストアのキー名}" \
  --with-decryption --region "${region}" --output text --query Parameter.Value`
aws ssm get-parameter --name "{パラメータストアのキー名}" --with-decryption \
  --region "${region}" --output text --query Parameter.Value > /tmp/bigquery.json

# `aws ecr get-login-password`を使う方式がAWS CLIのバージョン2からなのでインストールする
# DataPipelineの再実行時はファイルが残っているので、削除コマンドが無いとエラーが起きる場合がある
rm -rf ./aws
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install --update

# Dockerをインストールして起動
sudo amazon-linux-extras install -y docker
sudo service docker start

# ECRにログインしてイメージをpullする
aws ecr get-login-password --region "${region}" | \
  sudo docker login --username AWS --password-stdin "https://${account}.dkr.ecr.${region}.amazonaws.com"
sudo docker pull "${account}.dkr.ecr.${region}.amazonaws.com/${repository}:${tag}"

# 環境変数や設定ファイルを渡して実行する
sudo docker run --env "AWS_ACCESS_KEY_ID=${IMAGE_AWS_ACCESS_KEY_ID}" \
  --env "AWS_SECRET_ACCESS_KEY=${IMAGE_AWS_SECRET_ACCESS_KEY}" \
  --env "GOOGLE_APPLICATION_CREDENTIALS=/credential/bigquery.json"
  -v "/tmp/bigquery.json:/credential/bigquery.json:ro" \ 
  "${account}.dkr.ecr.${region}.amazonaws.com/${repository}" "${batch}"
```

## Resource Roleの設定方法

もう一つ設定で困るのが、EC2インスタンスに付与されるResource Roleです。基本的にはDockerイメージ内で処理は実行されるので、そこで利用するIAMユーザーに適切に権限を持たせればいいのですが、ECRやパラメータストアを参照できる権限を付与する必要があります。

* [AmazonEC2RoleforDataPipelineRole](https://docs.aws.amazon.com/ja_jp/datapipeline/latest/DeveloperGuide/dp-iam-roles.html)などのDataPipeline自体のロギング等に必要なポリシー
* [AmazonEC2ContainerRegistryReadOnly](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecr_managed_policies.html#AmazonEC2ContainerRegistryReadOnly)などのECSからpullするためのポリシー
* パラメータストアからデータを取得するためのロール（以下に記載）
  * 復号のために、暗号化で利用したkmsも許可する必要があります
  * 特に重要な情報なので、ここは厳しく権限を設定しています

```json:パラメータストアからデータを取得するためのポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "ssm:DescribeParameters",
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "ssm:GetParameters",
            "Resource": "arn:aws:ssm:ap-northeast-1:{アカウントID}:parameter/{適切なパラメータストアの階層}/*"
        },
        {
            "Sid": "VisualEditor2",
            "Effect": "Allow",
            "Action": "ssm:GetParameter",
            "Resource": "arn:aws:ssm:ap-northeast-1:{アカウントID}:parameter/{適切なパラメータストアの階層}/*"
        },
        {
            "Sid": "VisualEditor3",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:ap-northeast-1:{アカウントID}:key/{復号化キー}"
            ]
        }
    ]
}
```

# まとめ

これらの設定で、今まで通りData Pipelineでのバッチ管理を止めずに、コンテナ移行を進めることができます。道のりは長いですががんばります。