Akatsuki Hackers Lab | 株式会社アカツキ(Akatsuki Inc.)

Akatsuki Hackers Labは株式会社アカツキが運営しています。

AWS Device Farm の adb プロセスを Airtest に認識してもらうための方法

この記事は Akatsuki Games Advent Calendar 2023 10日目の記事です。

昨日は Yusuke Nakajima さんの UnityのProjectWindowに表示履歴機能を付けるエディタ拡張 でした。表示履歴機能があるだけで開発中のストレスが軽減されますね。導入もシンプルにしてくれているのがとても良いと思いました!

zenn.dev

はじめに

QAエンジニアの山﨑@tomo_tk11です。私のチームでは「ゲーム開発を行うプロジェクトメンバーと連携しながら、QA コストや QA リードタイムの最適化」を行っています。 現在はマスターデータのチェッキング作業の効率化や、Airtest と AWS Device Farm(以下、Device Farm) を用いてリグレッションテストなどの自動化に取り組んでいます。

今回は、使用している Airtest のアップデートを行った際に Device Farm で動作しなくなったため、その原因と解決策をまとめました。 Airtest と Device Farm の自動テストシステムで過去に出した資料を合わせて読むことで理解が深まると思います。最後に載せておくのでよければご覧ください。

TL;DR

  • デバイスを指定する uri に serial_noadb_path を入力しよう
# OK
auto_setup(f"Android:///{serial_no}?adb_path={adb_path}")

# NG
auto_setup(f"Android:///")

目次

Airtest アップデートのきっかけ

今回は Airtest v1.3.0 から v1.3.1 へのアップデートを行いました。 最近のAirtest は2ヶ月くらいの間隔でバージョンアップされています。 アップデート内容もかなり積極的でとても助かっています。

  • 最近のアップデートで嬉しかったもの
    • デバイスの画面録画機能で cv2, ffmpeg サポート
    • iOS の自動テストサポートの拡張(今までは一癖ありました)

そして、v1.3.1 では adb に関する気になるアップデートがありました。

adb will now give priority to using the current adb process, or the system variable is set to adb in ANDROID_HOME. If neither is found, adb in airtest will be used.

Release v1.3.1 · AirtestProject/Airtest · GitHub

Device Farm では adb プロセスが既に立ち上がっており、その adb と Airtest に内蔵されている adb が競合するという問題が以前ありました。なので、私たちのシステムではひと工夫しています。

Airtestの FAQ によると、競合した際はpackage内のadbを任意のものに差し替えるとのことです

なのでtestspecのpre_testでinstallしたAirtest package内のadbをDevice Farmのものに差し替えます

hackerslab.aktsk.jp

v1.3.1 のアップデートでそのひと工夫が不要になるのではないかと思い、さっそく自動テストを動かしてみました。

Device Farm 上でテスト実行すると、 ADB Error が発生する

AdbError が発生し、テストスクリプトの実行にすら行きつけませんでした。

airtest.core.error.AdbError: stdout[] stderr[/bin/bash: devices: No such file or directory

原因

このエラーは、Airtest の adb に関する仕様変更による影響です。Device Farm では常に adb プロセスが動作しているという話をしましたが、実際には /bin/bash/opt/dev/android-sdk-linux/platform-tools/adb.orig の2つの実行ファイルが adb を名乗って動いています。 Device Farm は adb の機能に一部制限を加えるために元々の adb バイナリファイルを adb.orig とし、ラッパースクリプト(bash ファイル)を adb として実行しています。今回のアップデートによって Airtest が Device Farm の adb ラッパーの仕組みを吸収できず、adb パスを認識しなくなったためエラーが発生しています。

ちょっとややこしいので深掘りします。

なぜエラーが起きるようになったのか?

Airtest の adb パス決定ロジックが追加されたから

adb will now give priority to using the current adb process, or the system variable is set to adb in ANDROID_HOME. If neither is found, adb in airtest will be used.It also supports directly specifying adb_path, for example:

from airtest.core.android.android import Android, ADB
adb = ADB(adb_path=r"D:\adb\adb.exe")
dev = Android(serialno="5TSSMVBYUSEQNRY5", adb_path=r"D:\test\adb41\adb.exe")

Release v1.3.1 · AirtestProject/Airtest · GitHub

v1.3.1 から以下の優先度で adb パスが自動的に決定されるようになりました。

  1. 現在動いている adb プロセス のパス
  2. 環境変数 ANDROID_HOME で指定されているディレクトリ内の adb のパス
  3. Airtest に内蔵されている adb のパス

また、手動で adb パスを与えることもできます。

どうやらこのロジックが Device Farm の adb ラッパーの仕組みとうまく噛み合わず、エラーが起きるようになってしまったようです。

Airtest の adb パス決定ロジックを詳しく調査する

まず、Device Farm の adb ラッパーの仕組みについて見てみます。Device Farm 上で which adb を実行するとこのように返ってきます。

[DeviceFarm] which adb
/opt/dev/android-sdk-linux/platform-tools/adb

ちなみに、Device Farm では $ANDROID_HOME は /opt/dev/android-sdk-linux で定義されています。

また、Device Farm では adb に関して、以下の2プロセスが動いています。

{'exe': '/bin/bash', 'name': 'adb'}
{'exe': '/opt/dev/android-sdk-linux/platform-tools/adb.orig', 'name': 'adb.orig'}

adb は Device Farm 側で用意された adb のラッパースクリプト、 adb.orig はオリジナルの adb バイナリファイルです。 adb/bin/bash が実行していますが、実体は /opt/dev/android-sdk-linux/platform-tools/ に存在しています。

次に、Airtest の adb パスを探しに行くコードを見てみます。

@staticmethod
def get_adb_path():
    if platform.system() == "Windows":
        ADB_NAME = "adb.exe"
    else:
        ADB_NAME = "adb"
 
    # Check if adb process is already running
    for process in psutil.process_iter(['name', 'exe']):
        if process.info['name'] == ADB_NAME:
            return process.info['exe']

Airtest/airtest/core/android/adb.py at v1.3.1 · AirtestProject/Airtest · GitHub

このロジックだと name が adb のプロセスを探しに行きますが、Device Farm 上の adb スクリプトは bash のラッパースクリプトのため、psutil で取得される実行コマンドのパスは /bin/bash となってしまいます。 しかし、そのパスには adb の実体が存在しないので、Device Farm 上の Airtest では adb をうまく認識できない、という結果になっていました。 ADB_NAME = "adb.orig" とすることで adb は実行できるようになると思いますが、得られる adb パスは オリジナルの adb.orig ファイルを示すのでラッパースクリプトを実行することができません。また、 Airtest のソースコードにパッチを当てることになり保守性が高くなってしまうため、それは選択しませんでした。

Airtest 上でエラーが起きている箇所はどこか?

エラーは、 airtest.core.api の init_device を呼び出したときに発生します。

def init_device(platform="Android", uuid=None, **kwargs):
    """
    Initialize device if not yet, and set as current device.
 
    :param platform: Android, IOS or Windows
    :param uuid: uuid for target device, e.g. serialno for Android, handle for Windows, uuid for iOS
    :param kwargs: Optional platform specific keyword args, e.g. `cap_method=JAVACAP` for Android
    :return: device instance
    :Example:
 
        >>> init_device(platform="Android",uuid="SJE5T17B17", cap_method="JAVACAP")
        >>> init_device(platform="Windows",uuid="123456")
    """
    cls = import_device_cls(platform)
    dev = cls(uuid, **kwargs)
    # Add device instance in G and set as current device.
    G.add_device(dev)
    return dev

Airtest/airtest/core/api.py at v1.3.1 · AirtestProject/Airtest · GitHub

init_device は 自動テストの初期設定を行う auto_setup や デバイスの接続を行う connect_device に device の uri を指定したときに呼び出されます。

uri は "Android:///""Android:///SJE5T17B17?cap_method=javacap&touch_method=adb" のような形で表現されており、デバイスの OS と OS 固有のパラメータを入力するために指定します。

init_device では、各 OS ごとのクラスからインスタンスが生成されます。 今回の場合、Android デバイスを使っているので、 Android クラスのインスタンスが生成されることになります。

次のコードは Android クラスの init の一部です。

class Android(Device):
    """Android Device Class"""
 
    def __init__(self, serialno=None, host=None,
                 cap_method=CAP_METHOD.MINICAP,
                 touch_method=TOUCH_METHOD.MINITOUCH,
                 ime_method=IME_METHOD.YOSEMITEIME,
                 ori_method=ORI_METHOD.MINICAP,
                 display_id=None,
                 input_event=None,
                 adb_path=None,
                 name=None):
        super(Android, self).__init__()
        self.serialno = serialno or self.get_default_device()
        self._uuid = name or self.serialno
        self._cap_method = cap_method.upper()
        self._touch_method = touch_method.upper()
        self.ime_method = ime_method.upper()
        self.ori_method = ori_method.upper()
        self.display_id = display_id
        self.input_event = input_event
        # init adb
        self.adb = ADB(self.serialno, adb_path=adb_path, server_addr=host, display_id=self.display_id, input_event=self.input_event)
        self.adb.wait_for_device()

Airtest/airtest/core/android/android.py at v1.3.1 · AirtestProject/Airtest · GitHub

そして、エラーが発生している箇所は self.serialno = serialno or self.get_default_device() の部分です。 今回、接続しているデバイスは1デバイスのみでしたので、初期設定に渡す uri は "Android:///" だけ設定していました。 そのため、serialno は None であり、デバイスを特定するために get_default_device が呼ばれていました。

def get_default_device(self, adb_path=None):
    """
    Get local default device when no serialno
 
    Returns:
        local device serialno
 
    """
    if not ADB(adb_path=adb_path).devices(state="device"):
        raise IndexError("ADB devices not found")
    return ADB(adb_path=adb_path).devices(state="device")[0][0]

Airtest/airtest/core/android/android.py at v1.3.1 · AirtestProject/Airtest · GitHub

ここでは、要は adb devices を実行し、マシンに接続されているデバイス一覧を取得し、その一番上のデバイスを接続先として指定しています。 ここで問題なのは、 get_default_device の呼び出し元で adb_path を指定しないまま実行していることです。

ADB クラスに adb_path = None を渡すとどうなるか?

このコードは ADB クラスの init の一部です。

class ADB(object):
    """adb client object class"""
 
    _instances = []
    status_device = "device"
    status_offline = "offline"
    SHELL_ENCODING = "utf-8"
 
    def __init__(self, serialno=None, adb_path=None, server_addr=None, display_id=None, input_event=None):
        self.serialno = serialno
        self.adb_path = adb_path or self.get_adb_path()
        self.display_id = display_id

Airtest/airtest/core/android/adb.py at v1.3.1 · AirtestProject/Airtest · GitHub

adb_pash = None だった場合は、 self.get_adb_path() が実行されます。 そして、この関数内で、「すでに動作している adb プロセスの実行パス」である /bin/bash が取得されてしまいます。

ここで、冒頭の「Airtest の adb パス決定ロジック」に戻ります。

@staticmethod
def get_adb_path():
    if platform.system() == "Windows":
        ADB_NAME = "adb.exe"
    else:
        ADB_NAME = "adb"
 
    # Check if adb process is already running
    for process in psutil.process_iter(['name', 'exe']):
        if process.info['name'] == ADB_NAME:
            return process.info['exe']

今回のアップデートで adb_path を外から入力し指定できるようになっていますが、デバイスの初期設定のタイミング(init_device)で serialno = None だった場合は、 adb_path を指定することができません。

解決策

デバイス初期化時の uri を正確に書く

auto_setupconnect_device に対して、 "Android:///" だけを渡していたことが今回の一番の問題でした。 serialno さえあれば、接続するデバイスは一意に特定できるのです。更に、uri に adb_path パラメータを含めれば、Android クラスに adb_path を渡すことができるので、 Android クラスを介して呼び出される adb は指定した adb となります。つまり、以下のような uri にすることで Device Farm の adb_path を指定して Android デバイスに接続することができます。

auto_setup(f"Android:///{serial_no}?adb_path={adb_path}")

肝心の serial_noadb_path は テスト環境を構築するための testspec.yaml(Device Farm の環境構築ファイル) で adb deviceswhich adb を使えば取得可能なので、それを環境変数かなにかで設定しておけばホストマシンからテストスクリプトである Python 側に渡すことができます。

v1.3.1 のアップデートで不要となった処理

Airtest に内蔵されている adb をホストマシンのものに差し替える対応

冒頭でも述べましたが、 v1.3.1 以前では、 Airtest に含まれている adb とシステム側の adb が競合した場合は差し替える対応を行う必要でした。 しかし、今回から任意の adb を用いる場合は adb_path を指定する方式に変更されたので、差し替え対応が不要となりました。

従って、差し替えを行っているコードを削除しても正常に動作することが確認できました。

まとめ

adb 周りのアップデートがあった Airtest v1.3.1 へのアップデートを試みましたが、 Device Farm の adb ラッパーの仕組みと競合した結果、ADB Error が発生しました。Airtest のアップデートにより追加された adb パスを良い感じに探す仕組みでは、 Device Farm の adb.orig を動かすプロセスを探しきれていませんでした。その問題は、Airtest のデバイス初期化時に Device Farm のデバイスの serial_no と、adb プロセスの adb_path を指定することで解決しました。そうすることで、Airtest は接続しているデバイスを始めから認識できるようになり、「デバイスを探す」動作と、そのための「adb パスを探す」動作を行う必要がなくなります。

今後も自動テストの取り組みは続けていくので、興味深い内容があれば記事にしたいと思います。この記事がゲームアプリケーションのテスト自動化に取り組まれる方の助けになれば嬉しいです。

明日は Koki Watanabe さんの記事です。お楽しみに!

参考資料

システムの全体像が分かる資料

speakerdeck.com

システムの詳細が分かる資料

hackerslab.aktsk.jp

hackerslab.aktsk.jp

hackerslab.aktsk.jp

Terraform で CloudRun + Identity-Aware Proxy をやっていく 2023

こんにちは。

株式会社アカツキゲームスで ATLAS というチームに所属してゲーム内通貨管理基盤を開発及び運用しています、なかひこくん (@takanakahiko) です。 最近やっと CB250R が納車されましたが、寒すぎて全然乗れていません。気候よ、早く暖かくなりなさい。

この記事は Akatsuki Games Advent Calendar 2023 の7日目の記事です。

この記事では、私が所属するチームで Terraform を利用し Identity-Aware Proxy を導入した際の知見を共有できればと思います。 今回紹介する知見は、インターン生と一緒に取り組んだタスクで得られた内容です。この場をお借り致しましてお礼申し上げます。

続きを読む

Redash を GCP の Workload Identity 連携に対応させた話

この記事は Akatsuki Games Advent Calendar 2023 の6日目の記事です。昨日はShuさんの Patch Your EC2 Instances Automatically using Systems Manager and Terraform でした。偶々ですが今日もある意味でセキュリティ運用を楽にするお話です。

Redash について

Redashは、BigQuery、Googleスプレッドシート、Athena、MySQL等、様々なデータソースからクエリした結果を可視化するダッシュボードを作成することができるオープンソースのツールです。
クエリパラメータという機能でクエリの一部に指定値を埋め込んで実行するといったことが非常に手軽にできるため、クエリの文法を知らない人にも簡単にデータ分析ができるようにしたりするのに活用しています。

Google のサービスを利用する際の認証方法

Redash のようなツールから Google のサービスへのアクセス時の認証方法としては、サービスアカウントキーが最も使用されることが多いでしょうか。ただし、サービスアカウントキーは機密データを守るためにも慎重に管理を行う必要があります。サービスアカウントキー管理のベストプラクティスのドキュメントには「可能であれば、認証のためのより安全な代替手段を選択してください」とまで書かれています。

Google のサービスには他にも様々な認証方法があります。

これらは、管理の手軽さとセキュリティを同時に向上させる良い機能だと思います。

しかしこれまで Redash は、Googleのサービスとの接続方法としてサービスアカウントキーにしか対応していませんでした。*1

そこで、Redash に Pull Request を送ってこれらの認証機能に対応させました。
本記事では、その内容や利用方法などを紹介します。

Pull Request の内容

Redash は元々、oauth2clientというライブラリをGoogleの認証に使用していましたが、これはかなり前に廃止されてメンテナンスも止まっているものでした。機能も限定的です。これをgoogle-authという現行の認証ライブラリに置き換えたというのがメインの内容です。

また、Redashが対応しているGoogleのサービスである BigQuery、Google スプレッドシート、Google Searchコンソール、Google Analytics のデータソース登録時に、サービスアカウントキーのアップロードをオプションとし、アップロードされなかった場合は Application Default Credentials (ADC) を使用するようにしました。
ADCの仕組みは以下に説明されていますが、これを利用することで前項で説明した様々な認証方法に対応させることができるようになっています。
アプリケーションのデフォルト認証情報の仕組み  |  Google Cloud

利用方法

まず、この機能はv10等の既存のリリースには含まれていないので、 Redash の開発ブランチを使用する必要があります。GitHub のmasterブランチや、Docker イメージ redash/redash:preview から取得してください。
以下では利用例が多いと思われる Docker を前提とします。

初めて利用する場合は、

docker compose run --rm server create_db

でDBの構築を行ってから docker compose up [-d] で起動します。

既存で v8 を利用している場合は、CHANGELOG のUpgradingの説明に従って docker-compose.yml を変更し、

docker compose run --rm server manage db upgrade

でデータベース移行が必要です。

GCE のメタデータを利用する場合、データソースを登録する際にサービスアカウントキーをアップロードしなければ自動的にメタデータが利用されるはずです。

AWS上でWorkload Identity を使用する場合には設定時に生成された構成JSONファイルが必要ですが、Redashにアップロードするのではなく、環境変数の GOOGLE_APPLICATION_CREDENTIALS でパスを指定するか、デフォルトの場所*2に配置するようにします。

参考までに、docker-compose.yml の例を示しておきます。

version: "2"
x-redash-service: &redash-service
  image: redash/redash:preview
  depends_on:
    - postgres
    - redis
  env_file: .env
  volumes:
    - ./config:/config  # ← 構成JSONファイルの共有用ディレクトリ
  restart: always
x-redash-environment: &redash-environment
  REDASH_REDIS_URL: "redis://redis:6379/0"
services:
  server:
    <<: *redash-service
    command: server
    environment:
      <<: *redash-environment
      REDASH_WEB_WORKERS: 4
  scheduler:
    <<: *redash-service
    command: scheduler
    environment:
      <<: *redash-environment
  worker:
    <<: *redash-service
    command: worker
    environment:
      <<: *redash-environment
      QUEUES: "periodic emails default"
      WORKERS_COUNT: 1
  scheduled_worker:
    <<: *redash-service
    command: worker
    environment:
      <<: *redash-environment
      QUEUES: "scheduled_queries,schemas"
      WORKERS_COUNT: 1
  adhoc_worker:
    <<: *redash-service
    command: worker
    environment:
      <<: *redash-environment
      QUEUES: "queries"
      WORKERS_COUNT: 2
  redis:
    image: redis:7-alpine
    restart: always
  postgres:
    image: pgautoupgrade/pgautoupgrade:15-alpine3.8
    env_file: .env
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    restart: always
  nginx:
    image: redash/nginx:latest
    ports:
      - "8000:80"
    depends_on:
      - server
    links:
      - server:redash
    restart: always

そして .env ファイルでシークレット等の環境変数を適切に設定します。この時に GOOGLE_APPLICATION_CREDENTIALS も設定できます。

GOOGLE_APPLICATION_CREDENTIALS=/config/google-auth-config.json
REDASH_COOKIE_SECRET=SampleSampleSampleSample
REDASH_DATABASE_URL=postgresql://postgres:aaaaaaaa@postgres/postgres
POSTGRES_PASSWORD=aaaaaaaa

まとめ

本記事では Redash で Google のサービスの認証に、サービスアカウントキーを使用するのを止め、GCEのメタデータや、GKE・AWS等のWorkload Identityに紐付いたサービスアカウントを使用できるようにした話を紹介しました。
ちなみにこの対応のために送った Pull Request は、送った数時間後には最初のコメントがもらえ、テスト方法に関する議論を経て3日後にはマージというスピード感で対応してもらえました。開発コミュニティがアクティブなのは良いですね。

*1: 厳密にはBigQuery + GCEのみ、専用のデータソース実装を使うことで対応させる方法はありましたが、煩雑な実装となっていました。

*2: ~/.config/gcloud/application_default_credentials.json

プロダクション環境で使用しているEC2をArmベースのAWS Gravitonに移行しました

この記事は Akatsuki Games Advent Calendar 2023 の2日目の記事です。昨日は @tkmruさんの「CODE BLUE参加記:食べて Decompile 寝て 繰り返す」でした。コロナで減ってしまったリアルイベントにも活気が戻ってきて良いですね!

はじめに

アカツキゲームスでサーバエンジニアをやっています柴原です。今回は自分の担当しているモバイルゲームのプロジェクトでは、ECS on EC2の環境で運用をしており、Graviton搭載のEC2に移行を行いました。 詳しい手順の紹介というよりは、その思考の過程や工夫を紹介できたらと思います。

経緯

2018年にAWSはArmアーキテクチャのプロセッサGravitonを独自開発したことを発表し、サービス提供を開始しました。 特徴としては、x86ではなくArmベースのアーキテクチャである点とIntelのプロセッサを搭載した従来インスタンスに比べコストパフォーマンスが優れている点が挙げられます。 以下に4xlargeまでのC5(第5世代intel搭載)、C6i(第6世代intel搭載)、C6g(初登場第6世代Graviton搭載)、C7g(最新の第7世代Graviton搭載)のリザーブドインスタンスの価格をまとめた表を示します。 このように前世代と同世代のintel搭載のEC2と比較してもGraviton搭載のEC2は安いことが分かります。

インスタンスタイプ C5 C6i C6g C7g
large USD 551 USD 577 USD 411 USD 490
xlarge USD 1,102 USD 1,154 USD 882 USD 981
2xlarge USD 2,205 USD 2,308 USD 1,764 USD 1,962
4xlarge USD 4,409 USD 4,615 USD 3,527 USD 3,923

ap-northeast-1のリージョンで1年分全額前払いのLinuxの価格

aws.amazon.com

また、AWS re:invent 2021の「Deep dive into AWS Graviton3 and Amazon EC2 C7g instances」にて、ベンチマークにおいてIntel搭載のEC2と比較して性能は向上していることも挙げられており、コスト面以外にも恩恵を受けられる可能性があります。 コスト面、性能面から見て利点がありそうということで、私のプロジェクトではGraviton移行に取り組み始めました。

移行する時の問題点

Gravitonに移行するにあたって考えなければいけない点として次の観点が挙がりました。

  • キャパシティ
  • ビルド
  • Arm未対応ミドルウェアの対応

このブログではキャパシティとビルドについてのみ言及します。

キャパシティ

運用型ゲームの性質上、イベントなどでアクセスが局所的に集中する傾向にあります。 AWSではAutoScalingGroupという機能によってEC2の台数を簡単に増減させることが可能です。 EC2を使っているような構成のインフラでは、アクセスの集中に合わせてスケールアウトすることで局所的なアクセスに対応することができます。 しかし、局所的にアクセスが集中する時では一度にたくさんのインスタンスを起動することがあったり、他のAWS利用者がたまたま多く同じタイプのインスタンスを使用している場合にAWS側のリソースが枯渇することがあります。 仮にコンピューティング最適化のインスタンスを使うと仮定すると、以下のタイプが存在します。

  • Graviton: C6g、C7g
  • Intel: C4、C5、C5a、C6i、C6a

タイプとしての種類が少ないだけでなく、昔から用意されてきたIntel系に比べるとGraviton搭載のEC2が多くないことは推測できます。 先ほどの例で言うとGraviton だけの運用をした場合、C6gとC7gのリソースを使い切ってしまうと、EC2はスケールアウトすることができなくなり、サーバの負荷が高まることが懸念されます。 ゲームに限らず言えることですが、意図しないサービスが継続できない状態は良くありません。

そこで私のプロジェクトでは、x86アーキテクチャ(Intel プロセッサ)とArmアーキテクチャ(Graviton)の両方で動作させるという方針にしました。 Gravitonだけの運用に比べて冗長性があり、リソースの枯渇に対しての対策としては十分な効果が期待できます。

ビルド

x86のみのシングルアーキテクチャの場合と異なり、マルチアーキテクチャではビルドがどうしても複雑になります。

マルチアーキテクチャビルドの問題点

よく利用されるマルチアーキテクチャの方法には以下の2つがあるようです。

CodeBuildは、コードをビルドし単体テスト実行して、イメージを作ることができるサービスです。 ビルドを実行する環境として、x86やArmを選ぶことができます。 両方のアーキテクチャでビルドを実行させることで、それぞれのアーキテクチャに対応したイメージを作成できます。 CodeBuildでのビルドを試したところ、ビルド環境の起動までの時間が長くなるケースがありました。(※2023/12月現在では、CodeBuildがLamdbaをサポートしたので起動速度について改善する可能性があります。) 開発環境のデプロイ時に毎回イメージのビルドを行っていたため、これは開発のペースが悪化する懸念がありました。 一方、buildxはdockerコマンドを拡張するCLIプラグインで、自身のマシンのアーキテクチャとは異なるものを選択してビルドを実行することできます。 buildxはqemuを用いたエミュレーションを行うため、速度低下がありますし、使用する言語・VMによってはエラーの原因になることもあります。 実際、コンテナ内に含まれていたRuby Gemのネイティブバイナリのコンパイルが非常に遅くなるケースもありました。 ここで一度立ち返ってみると、問題点は以下に集約されます。

  • ビルド速度低下によるデプロイ自体の速度の低下
  • buildxを採用する場合のqemuのエミュレーションの懸念

ビルドの速度低下については、単一のアーキテクチャでなくなった以上ある程度は仕方ない部分はあります。 しかし、これを最小限にしつつも速いビルドを実現し、エミュレーションであることを気にしたくはないです。 そこで、次の方針を立てました。

ビルドの工夫

デプロイは、コードを変更した時やライブラリの更新をした時などに行います。 一般的に、ライブラリの更新以上にコードの更新の方が多いです。 つまり、コードの更新の時だけでも速くなればストレスは少ないということが言えます。 そこで、イメージを分割することを考えました。 以下の図に示すように、Gem等ネイティブバイナリ部分を含むベースイメージとそのベースイメージにコードをコピーしただけのコードイメージに分けるという方法です。

ベースイメージにはネイティブイメージが含まれエミュレーションを気にしたくないため、CodeBuildを用いて必要な時(ライブラリ更新やセキュリティパッチの適用)にのみビルドさせます。 そして、そのベースイメージに対してコードのコピーや環境変数の設定のみの作業をdocker buildxに実行させます。 こうすることでコード更新によるデプロイの速度の低下を最小限にしつつ、エミュレーションを気にしないで済みます。

まとめ

このような問題点を考慮しつつビルド方針を定め、私のプロジェクトではGravitonへの移行に成功しました。 年間でかかるインフラ費用を抑えつつ、スケールアウト時の冗長性を高めることができました。 また、既存デプロイへの組み込みを無事終了し、この体制で運用をしています。 インフラにかかる費用を抑える工夫は、常に考え今後も実施していきたいですね。 AWS re:Invent 2023では、CEOのKeynote セッションにてGraviton4搭載のR8gが発表されました! 更なる高速化が期待できるので、東京リージョンに来るのが楽しみですね! 皆様も良きGravitonライフを送ってください。

明日は、Jingyuan Zhaoさんの記事となります。お楽しみに!

CODE BLUE 2023参加記:食べて Decompile 寝て 繰り返す

セキュリティエンジニアの小竹(aka tkmru)です。 先月、CODE BLUEというセキュリティカンファレンスに行ったので、その参加記を書きました。 このエントリーはAkatsuki Games Advent Calendar 2023の1日目の記事です。

CODE BLUE とは

CODE BLUEは、2014年より東京で開催されているセキュリティカンファレンスです。 日本国内で開催されているセキュリティカンファレンスの中では、最大級のカンファレンスです。 去年は「オンライン配信」+「リアル会場」によるハイブリッド開催となっていましたが、今年は会場のみでの開催でした。 去年は登壇者として参加していましたが、今年は去年よりも参加者が増えているように感じました。

Hex Rays社のグッズに心を奪われる

今年のCODE BLUEでは、なんとIDA Proの開発元であるHex Rays社がスポンサーになっていました。 Hex Rays社のブースでは、サッカーのゲームのスコア次第でTシャツやノートなどをもらえるというイベントをやっていました。

私はTシャツをもらうことができました。愛用しているツールグッズをもらえるというのは、とても嬉しいですね。 食べて、Decompile、寝て繰り返し、マントノン侯爵夫人と一生を添い遂げようと決意しました。

印象に残った発表

ここでは印象に残った2つの発表を紹介します。

休憩中に飲む紅茶が非常に美味しい!!!!!

stelftools: クロスアーキテクチャに対応した静的結合されたライブラリ関数の特定ツール

stelftoolsという静的リンクされたライブラリ関数を特定するツールの発表が行われました。 この発表はツールに関する発表が行われるBluebox枠での発表です。 本ツールのリポジトリは以下です。 github.com

多くのIoTマルウェアでは、ライブラリを静的リンクしており、解析時に攻撃者が定義した関数なのか、ライブラリ内の関数なのかが特定しづらくなっています。 動作を詳細に解き明かしたいのは攻撃者が定義した関数であり、ライブラリ内の関数と区別できれば、効率的に解析が行なえます。 また、ライブラリ内のどの関数がどの部分で用いられているかが分かれば動作を理解する助けになります。

しかし、IoTマルウェアは様々なアーキテクチャのCPU向けに開発されており、関数のシグネイチャを用意するのは大変です。 この問題を解決するためにstelftoolsは開発されました。stelftoolsは17のアーキテクチャと700以上のツールチェーンに対応しています。 静的リンクされたELFバイナリを解析するためのツールということでst(atic)-elf-toolsと名付けられています。

関数の発見にはパターンマッチだけではなく、分岐先のアドレスや呼び出しの依存関係を比較することで精度の向上が図られています。 rand(3)のような、内部で別の関数をそのまま呼び出すだけの関数はサイズが小さすぎ、単純なパターンマッチだけでは大量に一致してしまう問題を解決しており、 IDA F.L.I.R.T.、BinDiff、rizzoなどの他のツールよりも精度が高いものを実装できたとのことです。 リポジトリ内のYARAのパターンファイルを見ると、膨大な量のシグネイチャが用意されており、 開発の大変さがうかがえます。

IDAやGhidraのプラグインが用意されており、 非常に便利そうで私自身も使いたいツールではありますが、チーターの皆さんには使ってほしくないツールだなと思いました。

PowerAutomate C2: クラウド寄生型ステルスC2フレームワーク

PowerAutomate C2というツールの発表が行われました。この発表もBluebox枠での発表です。 Power AutomateはMicrosoftが提供している「新しいPowerShell」とも称される強力なローコードツールです。 このPower AutomateをC2マルウェアとして活用しようという発表でした。 本ツールのリポジトリは以下です。 github.com

去年のAVTOKYOでも発表されており、その時の内容は発表者のブログにまとめられています。 jp.security.ntt

Power Automate C2を活用するための攻撃シナリオは次のようになります(上記ブログより引用)。 PowerAutomateで構築された自動化処理はフローと呼ばれ、フローの実行ログは1ヶ月程度保管されますが、フローを消すとログが消失します。

  1. 攻撃者はユーザーを侵害して Power Automate へのアクセス権を入手し、悪意あるフローを作成する。
  2. 侵害されたことを知らないユーザーが、たまたまパスワードを変更する。
  3. 攻撃者は C2 経由で Power Automate へのアクセスを継続し、任意のフローを作成・実行する。
  4. 攻撃者は目的を達成したらフローとともに痕跡を削除する。

Power Automateを活用する利点として、クラウド上で完結するため、ユーザーの端末を必要とせず、EDRやIDSの監視対象外で活動を行える点、ログを消すことが容易な点が挙げられます。

ノーコードツールを攻撃に使用する方法のドキュメントは不足しており、大変勉強になる発表でした。 他のノーコードツールでも似たことができないか確認してみたいと思いました。

PwCによるアフターパーティー

CODE BLUEでは全ての発表が終了した後、最後に公式のネットワーキングパーティーが開催されています。 その後にPwCが去年から独自にアフターパーティーをクラブで行っています。 今年は六本木のクラブに観光バスで移動しての開催となりましたが、移動費、参加費もなんと無料です!!!! 普段クラブに行かないので新鮮な体験でした。ネットワーキングパーティでは会えなかった人ととも出会えて非常に楽しかったです。

まとめ

発表を楽しむだけでなく、なかなか普段会う機会がない知人たちとも会えて、とても楽しいカンファレンスでした。 カスペルスキーの学生招待枠での参加だったり、学生スタッフとしての参加だったり、発表者としての参加だったり、形態は様々ですが、2015年より毎年参加している(多分....)ので、来年も機会があればぜひ参加したいと思います。