仮想通貨 bot を色々なクラウドサービスに自動デプロイしてみる
仮想通貨 botter 界隈ではあまり語られない DevOps を自動デプロイだけやってみる。 クラウド VM に SSH で接続して bot を実行するようなレガシーな運用方法から抜け出す。
もし全体的にまとまったら Zenn 本でも出したい。
前提条件
- クラウドサービスのアカウントを持っていて VM などのリソースを作成できる。
- Git の知識がある。
- Docker の知識がある。
このスクラップでは以下を目標にする。
自動デプロイ
- GitHub Actions による CI/CD を利用して以下のデプロイ基盤に bot を自動デプロイできる設定を行う。
デプロイ基盤
- [x]: クラウド VM (Docker エンジン)
- AWS EC2
- Google Compute Engine
- [x]: AWS ECS
- [x]: Google Kubernetes Engine (GKE)
ここでは目標としないが有用そうなソリューション
- FaaS 系
- AWS Lambda
- Google Cloud Functions
- Google Cloud Run Jobs
- HashiCorp Nomad
- Docker Swarm
テンプレートリポジトリ
ここのコードはこの GitHub リポジトリにまとまっている (作業中) 。 Compose 化したサンプル bot が main ブランチ、それをデプロイするワークフローファイルがそれぞれのサービスに対応する名称のブランチに上がっている。
クラウド VM (Docker エンジン)
Docker CLI はローカルの Docker エンジンを操作するのが一般的だが、実は -H
オプションや context
を利用すると別のマシンの Docker エンジンを操作できる。 これを利用して CI/CD からクラウド VM へコンテナをビルド・デプロイする。
DevOps 的には認証情報が SSH 鍵に紐づいておりレガシーな方法ではあるが、ややこしいクラウドのサービスを使わなくても良くてシンプルに VM インスタンス 1 つで済むので botter 向けな方法の 1 つだと思う。
事前準備
- クラウドサービスのコンソールから VM のインスタンスを建て、 Docker をセットアップする。 SSH の鍵は予めクラウドサービスに登録しておく。
- AWS EC2
- イメージはデフォルトの Amazon Linux 2023 がおすすめ。 Docker のインストールが簡単。
- (参考) コマンドでインスタンスを作成する場合。
--key-name
オプションは AWS に登録したキーペアの名前に修正する。 CloudShell で実行する。CloudShellaws ec2 run-instances --image-id $(aws ssm get-parameter --name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64 --query Parameter.Value --output text) --instance-type t2.micro --key-name <id_rsa>
- (参考) Docker インストールコマンド。 SSH 接続して実行する。Remote machine
sudo dnf -y install docker && sudo systemctl enable --now docker && sudo usermod -aG docker ec2-user
- Google Compute Engine
- イメージは Container-Optimized OS を選択するのがおすすめ。 コンテナしか動かない代わりに、 Docker が最初からセットアップされている。
- (参考) コマンドでインスタンスを作成する場合。 Cloud Shell で実行する。Cloud Shell
gcloud compute instances create instance-$(cat /dev/random | LC_ALL=C tr -dc "[:alpha:]" | tr '[:upper:]' '[:lower:]' | head -c 10) --image-family=cos-stable --image-project=cos-cloud --machine-type=e2-micro --scopes=https://www.googleapis.com/auth/cloud-platform
- AWS EC2
- SSH 接続可能なことをテストする。Local machine
ssh -T user@ip-address hostname
- SSH 接続を介して Docker エンジンが利用できることをテストする。Local machine
docker -H ssh://user@ip-address run --rm hello-world
- GitHub に空のリポジトリを作成する。 README.md を作るにチェックをしておくとあとで楽にクローンできる。
- 作成したリポジトリに GitHub Actions 用の Secrets を登録する。 Settings -> Secrets and variables -> Actions -> New repository secret
-
DOCKER_HOST
-
ssh:// + SSH 文字列
ssh://user@ip-address
-
ssh:// + SSH 文字列
-
SSH_CERT
- クライアント側に保存されている SSH サーバーの公開鍵 (known_hosts) 。 以下のコマンドで取得できる。Local machine
grep <ip-address> ~/.ssh/known_hosts
- クライアント側に保存されている SSH サーバーの公開鍵 (known_hosts) 。 以下のコマンドで取得できる。
-
SSH_KEY
- SSH クライアントの秘密鍵。 以下のコマンドで取得できる (※ RSA 認証でない場合はファイル名を変更する) 。Local machine (RSA)
cat ~/.ssh/id_rsa
- SSH クライアントの秘密鍵。 以下のコマンドで取得できる (※ RSA 認証でない場合はファイル名を変更する) 。
-
Docker Compose アプリとして bot を実装する
作成したリポジトリをローカルマシンにクローンしてきて、Docker Compose で実行できる bot を作成してコミットする。
GitHub Actions ワークフロー
以上の設定が完了して、次の YAML を GitHub Actions ワークフローファイル .github/workflows/ssh.yml
としてコミットする。
name: Docker Compose Deploy
on: push
jobs:
depoy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ArwynFr/actions-docker-context@v2
with:
docker_host: ${{ secrets.DOCKER_HOST }}
context_name: remote
use_context: true
ssh_cert: ${{ secrets.SSH_CERT }}
ssh_key: ${{ secrets.SSH_KEY }}
- name: Docker Compose Up
run: docker compose up --build -d
このワークフローファイルがコミットされると、それ以降はリポジトリにコミットが走るとごとに Compose アプリが Secrets に登録されている SSH ホストに対してデプロイされる。 デプロイのログは GitHub リポジトリの Actions タブから確認できる。
ワークフロー解説
ジョブの 2 ステップ目で ArwynFr/actions-docker-context というアクションを利用して SSH ホストのコンテキストを作成している。これはポピュラーなアクションではない (現在 9 Star) が、 Docker CLI のコンテキスト機能を簡単に利用できるようにするアクションになっている。 作成したコンテキストを使った docker compose up --build -d
コマンドによって SSH ホストの Docker エンジンでイメージがビルドされコンテナが起動され bot がデプロイされる仕組みになっている。
他にある wshihadeh/docker-deployment-action アクションの方が最もポピュラー (現在 116 Star) のようだが、このアクションは Docker Compose v1 に依存しているようなので、自ら Docker Compose コマンドを打てるコンテキストの方を採用した。
運用について (Portainer)
このスクラップは CI/CD で bot をデプロイできるようにする解説する目的だけれど、このクラウド VM に関してはおすすめの運用方法も補足する。
セルフマネージドの Docker エンジンだけの素の状態だと、bot (コンテナ) の手動停止や状態監視などの運用面がとても面倒 (結局 SSH になる) といった課題がある。 他に挙げたデプロイ基盤ではサービス専用のコンソールがあってデプロイしたコンテナや関数が管理できるが、セルフマネージドだとそれがない。 そこで Portainer(読み方:ポーテナー)といったサードパーティのコンテナ管理ツールを導入して運用するのがおすすめ。
Portainer Web UI (参考画像)
- コンテナの状況やリソース使用率が確認できる。
- コンテナの停止・再開ができる。
- 標準出力に吐いているログを確認できる。
Portainer デプロイ手順
- トンネルを開いた状態で SSH 接続する。Local machine
ssh -L 9443:localhost:9443 user@ip-address
- SSH 接続先で以下コマンドを実行して Portainer コンテナを起動する。Remote machine
docker volume create portainer_data && docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest
- Web ブラウザで https://localhost:9443 を開いて Portainer Web UI を開いて管理者ユーザーのパスワードを設定する。
- 補足: 初回の管理者ユーザー作成の為にファイアウォールを開けずに SSH トンネル経由で Portainer を開いている。
- クラウドサービスのコンソールから VM インスタンスに紐づくネットワーク設定でポート 9443 を開放する。
- クラウドで VM に静的 IP アドレスを割り振る。
- https://<static-ip-address>:9443 を開いてログインすると、外部からコンテナを管理できるようになる。
- (Optional) ロードバランサー等を設定すると SSL で怒られずにアクセスできるようになり、ブラウザのパスワード保存も効くはず。 ここでは省略する。
クラウド関数 / バッチ Job
TODO
AWS ECS
Amazon Elastic Container Service に bot をデプロイしてみる。
bot をコンテナ化することでこの ECS のマネージドな環境にデプロイできる。 ECS は Kubernetes に比べると難易度が低いコンテナオーケストレーターと言われている。
ECS に bot をデプロイするとおおかたこのようなメリットがある。
- ECS コンソール画面から bot を管理しやすくなる。
- ログを監視しやすくなる。
- サービスとしてデプロイすると bot が落ちても自動で再起動してくれる。
- インフラに Fargate を選択すると EC2 インスタンスの管理が不要になる (代わりにちょっと割高かも ? ) 。
- 1 タスク (= 1 コンテナ = 1 bot) ごとに課金。
- 最低スペック (0.25 vCPU / 512 MB RAM) でコンテナをデプロイして 1 bot 月千円ぐらい。
- EC2 タイプを選択すれば通常通り EC2 インスタンスの料金を払う代わりにそのリソース内で bot を動かすので bot ごとの課金にはならない。
- EC2 だとリソースを使いすぎてインスタンスが落ちると全 bot 落ちるけど、 Fargate だとその心配はない。
このスクラップではインフラは Fargate をターゲットとしてデプロイしてみる。
デプロイツールの種類について
GitHub Actions ワークフロー内で AWS CLI を組み合わるだけでもデプロイは出来なくはないが、それはちょっと面倒だし既に ECS へのデプロイツールは様々なものが存在している。
- AWS Copilot CLI
- Docker Compose ECS Integration
- Amazon ECS "Deploy Task Definition" Action for GitHub Actions
- ecspresso
結論から言うと、 今回のスクラップでは ecspresso を利用して GitHub Actions から ECS への bot 自動デプロイを試してみる。
ecspresso は AWS の ECS 向け API のみを薄くラップしたコマンドラインツール。 ECS へコンテナをデプロイするための最低限の要素のみを管理することに焦点を当てている。 なのでロードバランサーなどの管理機能も持っていないが、逆にそれのおかげで botter 的にも不要な概念の理解と設定が必要なくコンテナのデプロイに集中できる。
中身は Go 言語でされているのだが、実はこれ 日本人の方が 1 人で開発して日本企業名義 (面白法人カヤック) で提供されている OSS になっている。 リファレンスも Zenn 本から日本語で参照できるのでありがたい。
ここまで ecspresso を紹介しておいて何だけど、弊テンプレートリポジトリの GitHub Action ワークフローに ecspresso コマンドは組み込んである。 botter 用途としてもデプロイ方法を高度なカスタマイズする必要性も少ないので基本的には ecspresso 自体の利用方法も覚えなくても弊テンプレートリポジトリを利用できる 。
その他のコマンドラインツール
ecspresso 以外の挙げたツールは AWS がオフィシャルに提供しているもの。
AWS Copilot CLI AWS CLI からコンテナ向けに独立したハイレベルコマンドラインツール。 AWS のベストプラクティスに則ってインフラからコンテナさらに CI/CD パイプラインまでデプロイしてくれる。 しかし Web アプリを基本のユースケースとして設計されているので、ロードバランサーも要らないようなシンプルなコンテナアプリである bot のユースケースには機能過多で逆に扱いづらい。 独自の抽象概念も多く学習が必要。
Docker Compose ECS Integration は Docker と AWS が連携して開発された docker コマンドにビルドインされている機能。 docker create context ecs コマンドで AWS に連携後 docker compose up -d
するだけでローカル環境と遜色なくコンテナを ECS に立ち上げられる。 botter ユースケース的にも最適で、実際これを使ってスクラップを書こうとしたのだが 2023/11 で Docker の機能から廃止されてしまう のでこれからの利用には向かない。 Web 業界的には Kubernets が標準で Docker 的にも Swarm があるので、シングルホスト向けの Compose アプリを単純に ECS にデプロイするだけのこの機能はあまり有効ではないと判断されたのかもしれない。
Amazon ECS "Deploy Task Definition" Action for GitHub Actions は AWS 提供している GitHub Action で、 GitHub による Actions のドキュメント内でも ECS にデプロイする利用例が提供されている。 しかしこのアクションはちょっといけてない部分があって、作成済みのサービスはあれば更新してくれるが サービスの新規作成はしてくれない 。 つまりこのアクションだけに頼ると新しい bot を実装したら、初回の一度は手動でサービスを作る必要があるので完全自動化にはならない。 サービスを新規作成する部分のみ別途 AWS CLI で書くものできなくはないが、その部分をサポートしてくれる ecspresso を今回は利用することにした。
事前準備
AWS
- AWS CloudShell を起動する。 (ローカル環境に AWS CLI があればそれで OK。)
- 弊テンプレートリポジトリのサンプルはデフォルトで東京リージョンを指定してある。 このリンクから東京リージョンの CloudShell を開けば東京リージョンでリソースが作成できる。
- デフォルト ECS クラスターを作成する。 (
default
という名前でクラスターが作成される。)CloudShellaws ecs create-cluster
- ECS 用のタスク実行ロール
ecsTaskExecutionRole
の作成と設定をする。- ロールがあるか確認する。 もし ECS を試したことがあればこれは自動的に作成されている。 コマンドを実行するとロールがあればその内容が表示される、なければ An error occurred ~ と表示される。 エラーなら以降の手順を実施する。CloudShell
aws iam get-role --role-name ecsTaskExecutionRole
- ロールの作成
- 以下 AWS ECS ドキュメント タスク実行 (ecsTaskExecutionRole) ロールの作成 に載っている内容だが、やり易いようにワンライナーにしている。
CloudShellPOLICY_DOCUMENT=$(cat <<"EOF" { "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF ) && aws iam create-role --role-name ecsTaskExecutionRole --assume-role-policy-document "$POLICY_DOCUMENT"
- ロールにポリシーをアタッチCloudShell
aws iam attach-role-policy \ --role-name ecsTaskExecutionRole \ --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
- ロールがあるか確認する。 もし ECS を試したことがあればこれは自動的に作成されている。 コマンドを実行するとロールがあればその内容が表示される、なければ An error occurred ~ と表示される。 エラーなら以降の手順を実施する。
- ECS 用のタスク実行ロールに足りない CloudWatch の一部権限を付与する。
- 上のコマンドでロールを作った場合でも既にロールが作成されていた場合でも
ecsTaskExecutionRole
には CloudWatch のロググループを作る権限が含まれていない。 これがないと新しい bot をデプロイしたときに問題が発生するので取り敢えずその権限を持っているCloudWatchAgentServerPolicy
というポリシーをアタッチしておく。CloudShellaws iam attach-role-policy \ --role-name ecsTaskExecutionRoleCopy1 \ --policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
- 上のコマンドでロールを作った場合でも既にロールが作成されていた場合でも
- GitHub で利用する為の AWS アクセスキーを作成する (なければ) 。
- AWS コンソール右上のユーザー ID -> セキュリティ認証情報 -> アクセスキーを作成
- 作成したアクセスキーとシークレットをそのまま表示しておく (閉じると確認できなくなる)
- ※ 個人 botter 用途ならルートユーザーのアクセスキーでもあまり問題ないが、気になるようであれば権限を抑えたユーザーのアクセスキーを作成するか、 OpenID Connect を構成する。
GitHub
- 弊テンプレートリポジトリを使用して新しくリポジトリを作成する。
- https://github.com/MtkN1/cloud-bot-deploy-template
- Use this template -> Create a new repository
- テンプレの bot をデプロイする練習なら Public リポジトリにしておいても OK 。 Public なら GitHub Actions の使用量を消費せず完全無料。 Privte だと使用量を消費する無料枠制になる。
- リポジトリのシークレットに先ほど作成した AWS のアクセスキーとシークレットを保存する。
- Settings -> Secrets and variables -> Actions -> New repository secret
-
AWS_ACCESS_KEY_ID
に アクセスキー -
AWS_SECRET_ACCESS_KEY
に シークレットアクセスキー
これで準備完了!
デプロイ 🚀
Actions -> Deploy to Amazon ECS -> Run workflows でサンプルの bot をデプロイ 🚀
デプロイのログは同画面から確認できる。 失敗していたら AWS の権限周りに問題があると予想されるので見直す。
成功例
GitHub Actions が終わったら ECS 側のサービス立ち上げが成功しているか確認する。
クラスター -> 該当サービスを開いたてこの画面になっていたら問題ない。
bot のログは ログ のタブから確認できる。 ログが毎秒出力されていれば正常動作 OK ✅
on: push
この GitHub Actions ワークフローは Run workflows ボタンから実行したが、テンプレートから作成後の初回以降は リポジトリの main ブランチにコミットがプッシュされたら自動で実行される 。 これでローカルで実装試した bot をリポジトリにあげれば手放しでデプロイされるようになる 🚀🚀
クリーンアップ
サンプル bot の確認が終わったら AWS の課金を防ぐ為にサービスを削除しておく。 サービスを削除 -> services を強制削除 。
またこのデプロイを試すと ECR にリポジトリと CloudWatch にロググループが作成される。 コンピューティングリソースではないのでさほど問題がないが、気になるようなら削除しておく。
Google Kubertenets Engine
事前準備
Google Cloud
最初に手動でクラスターを作成しておく。
クラスターのモードは Standard と Autopilot モードがある。 AWS ECS でいう EC2 タイプと Fargate タイプの違いのようなもの。 料金形態は異なるので注意が必要。 この例ではフルマネージドなインフラで bot 運用できる Autopilot モードを対象にする。
Cloud Shell または gcloud
コマンドが利用できる環境で以下のコマンドを実行する。 各 gcloud コマンドの --project
は省略するのでデフォルトのプロジェクト ID を設定しておく (gcloud config set project [PROJECT_ID]
) 。
gcloud container clusters create-auto bot-cluster \
--region asia-northeast1
gcloud container clusters get-credentials bot-cluster \
--region asia-northeast1
参考:
GitHub Actions などでサービスアカウントから自動デプロイはするには権限関係の追加設定が必要になる。 「Google Cloud のサービスアカウント」と「Kubernetes のサービスアカウント」の連携の設定を行う。 ここでは色々と省くために Google Cloud 側のサービスアカウントはデフォルトで存在する「Compute Eninge デフォルトサービスアカウント」を利用する。 (権限的に推奨される方法ではないかもしれないので注意。)
PROJECT_ID=$(gcloud config get-value project)
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
kubectl create serviceaccount compute \
--namespace default
gcloud iam service-accounts add-iam-policy-binding $PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:$PROJECT_ID.svc.id.goog[default/compute]"
kubectl annotate serviceaccount compute \
--namespace default \
iam.gke.io/gcp-service-account=$PROJECT_NUMBER-compute@developer.gserviceaccount.com
参考:
Compute Eninge デフォルトサービスアカウントのキーを出力する。 コンソール画面から、Compute Eninge デフォルトサービスアカウント -> キー -> 鍵を追加 -> 新しい鍵を作成、で JSON キーをダウンロードする。
※ ダウンロードしたキーは後の GitHub Actions のシークレットに登録する。
GitHub Actions で認証を得られるようにサービスアカウントに「サービス 管理者」ロールを与える。
gcloud iam service-accounts add-iam-policy-binding $PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--role roles/iam.serviceAccountAdmin \
--member "serviceAccount:$PROJECT_NUMBER-compute@developer.gserviceaccount.com"
参考:
コンテナレジストリーを作成しておく。
gcloud artifacts repositories create asia-northeast1-docker --repository-format=docker \
--location=asia-northeast1
GitHub
- 弊テンプレートリポジトリから新しいリポジトリを作成する。
- GitHub Actions のシークレットを設定する。
-
GKE_PROJECT
: Google Cloud のプロジェクト名 -
GCP_CREDENTIALS
: Google Cloud サービスアカウントの鍵。 事前にダウンロードした JSON ファイルの内容を張り付ける。
-
デプロイ 🚀
Actions -> Build and Deploy to GKE -> Run workflows でサンプルの bot をデプロイできる 🚀
または main ブランチに変更が掛かった時には自動で GitHub Actions のワークフローが走ってデプロイされる。
デプロイされた bot が表示される GKE ワークロード画面。
ワークロードのログ画面。
ワークフロー解説
.github/google.yml
のワークフローによって GKE に bot がデプロイされます。 このワークフローは GitHub のスターターワークフロー参考にしている。
https://github.com/actions/starter-workflows/blob/main/deployments/google.yml
このスターターワークフローについては GitHub Docs に解説がある。
所感
この手順を用いる事で GKE に自動で bot がデプロイできる。 Google のフルマネージドな環境に bot がデプロイできるのは非常に便利。 個人的に GCP のコンソール画面は Google のマテリアルデザインに沿って作られているので AWS よりユーザーフレンドリーで気に入っている。 特にログの画面は圧倒的に GCP の方が使いやすいと感じている。
しかしながら GKE に bot をデプロイするメリットがあるかは不明。 Kubernets は大規模なコンテナオーケストレーターシステムであるが、ベーシックな設計の bot は 1 つのデーモン的なプロセスとして実行しることになる。 同じ bot プログラムを同時実行させると当たり前だが重複注文などが発生し、取引戦略が正しく実行されない。 その為デーモン的なプロセスを Kubernets で稼働させる優位性はあまりない。 つまりはコンピューティングコスト的に余計な料金を支払うことになる。 GKE で 1 つの bot に掛かる料金は最低構成でも 3000円/月 程である。 bot をマイクロサービスアーキテクチャで実装するなどすると Kubernetes システムの優位性を生かせるかもしれない。