はじめに

AWS CLI 使っていますか?
私も折りに触れ使っています。複数のアカウントを横断して一度にリソースの状況を知りたいときに利用したり、Web コンソールではわかりづらいリソースの包含関係がわかったりもして AWS のリソースの理解の一助になるのもいいんですよね。
ただ、AWS CLI の取得時や結果に対するフィルタリングの --filters--query オプションや JSON で出力した場合に jq でゴニョゴニョするのもぜんぜん慣れません :persevere: 。やりだすとパズル解くみたいで楽しいですし、オプションの記載も宣言的で好感をもっていますが。
なので、慣れたプログラミング言語で IDE の恩恵を受けながら AWS の API を気軽に叩けるツールを書けたらいいなと思い、AWS SDK for JavaScript を利用した Node.js ツールを VS Code で書く、みたいなことをしていました。
しかし、その際に必要になるたびに毎回1から「npm init して〜」と小さなツールを量産するのは管理が面倒だしそのたびに GitHub リポジトリに上げるのも大仰だなと思っていました。

oclif

そこで知ったのが oclif です。

The Open CLI Framework Create command line tools your users love

oclif: The Open CLI Framework · Create command line tools your users love

とのことで、Node.js のコマンドラインツールを便利に作れるフレームワークのようです。
オプションやヘルプの記載もフォローしてくれていて、開発者はロジックの記載に専念できそうです。
また「必要になるたびに」コマンドを増やしていくだけでリポジトリも増やす必要ないし一元管理できてこれも楽ちんです。試してみます。

なお、以下の環境で試しました。

$ node -v
v18.13.0

0. 前提

IAM ロールの名前一覧を取得する CLI アプリを作りたいと思います。その後、オプションを付与することで ARN も一覧に加えて表示できるようにしてみます。
参考までに、AWS CLI で取得できるバルクデータはこんな感じです。

$ aws iam list-roles --profile default
{
    "Roles": [
        {
            "Path": "/aws-service-role/organizations.amazonaws.com/",
            "RoleName": "AWSServiceRoleForOrganizations",
            "RoleId": "************",
            "Arn": "arn:aws:iam::**********:role/aws-service-role/organizations.amazonaws.com/AWSServiceRoleForOrganizations",
            "CreateDate": "2019-07-26T01:51:57+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "[organizations.amazonaws.com](http://organizations.amazonaws.com)"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "Description": "Service-linked role used by AWS Organizations to enable integration of other AWS services with Organizations.",
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/aws-service-role/support.amazonaws.com/",
            "RoleName": "AWSServiceRoleForSupport",
            "RoleId": "************",
            "Arn": "arn:aws:iam::**********:role/aws-service-role/support.amazonaws.com/AWSServiceRoleForSupport",
            "CreateDate": "2019-07-26T01:47:23+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "[support.amazonaws.com](http://support.amazonaws.com)"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "Description": "Enables resource access for AWS to provide billing, administrative and support services",
            "MaxSessionDuration": 3600
        },
        {
            "Path": "/aws-service-role/trustedadvisor.amazonaws.com/",
            "RoleName": "AWSServiceRoleForTrustedAdvisor",
            "RoleId": "************",
            "Arn": "arn:aws:iam::**********:role/aws-service-role/trustedadvisor.amazonaws.com/AWSServiceRoleForTrustedAdvisor",
            "CreateDate": "2019-07-26T01:47:23+00:00",
            "AssumeRolePolicyDocument": {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Service": "[trustedadvisor.amazonaws.com](http://trustedadvisor.amazonaws.com)"
                        },
                        "Action": "sts:AssumeRole"
                    }
                ]
            },
            "Description": "Access for the AWS Trusted Advisor Service to help reduce cost, increase performance, and improve security of your AWS environment.",
            "MaxSessionDuration": 3600
        }
    ]
}

1. CLI アプリの新規作成

$  npx oclif generate aws-tools

     _-----_
    |       |    ╭──────────────────────────╮
    |--(o)--|    │  Time to build an oclif  │
   `---------´   │    CLI! Version: 3.7.3   │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

Cloning into '/path/to/aws-tools'...
? npm package name aws-tools
? command bin name the CLI will export qcaws
? description AWS Cli Tools for Quartetcom
? author Naoyasu @naoyes
? license MIT
? Who is the GitHub owner of repository (https://github.com/OWNER/repo) quartetcom
? What is the GitHub name of repository (https://github.com/owner/REPO) aws-tools
? Select a package manager npm
    force aws-tools/package.json
    force aws-tools/.gitignore
    force aws-tools/LICENSE
    :

Yeoman おじさんの質問に答えていくと初期状態を作成してくれます。

2. サンプルツールを動かしてみる

$ cd ./aws-tools
$ ./bin/run
AWS Cli Tools for Quartetcom

VERSION
  aws-tools/0.0.0 darwin-arm64 node-v18.13.0

USAGE
  $ qcaws [COMMAND]

TOPICS
  hello    Say hello to the world and others
  plugins  List installed plugins.

COMMANDS
  hello    Say hello
  help     Display help for qcaws.
  plugins  List installed plugins.

初期状態では hello, help, plugins の3つのコマンドがあるようです。

$ ./bin/run hello --help
Say hello

USAGE
  $ qcaws hello PERSON -f <value>

ARGUMENTS
  PERSON  Person to say hello to

FLAGS
  -f, --from=<value>  (required) Who is saying hello

DESCRIPTION
  Say hello

EXAMPLES
  $ oex hello friend --from oclif
  hello friend from oclif! (./src/commands/hello/index.ts)

COMMANDS
  hello world  Say hello world

$ ./bin/run hello everyone --from naoyes
hello everyone from naoyes! (./src/commands/hello/index.ts)
$ ./bin/run hello world
hello world! (./src/commands/hello/world.ts)

hello コマンドのヘルプを見て実際に使ってみました。

3. IAM ロール一覧コマンドを実装する。

3-1. 雛形作成

$ npx oclif generate command list-iam-roles

     _-----_
    |       |    ╭──────────────────────────╮
    |--(o)--|    │    Adding a command to   │
   `---------´   │ aws-tools Version: 3.8.1 │
    ( _´U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |
   __'.___.'__
 ´   `  |° ´ Y `

   create src/commands/list-iam-roles.ts
   create test/commands/list-iam-roles.test.ts

No change to package.json was detected. No package manager install will be executed.

上記のようにコマンド用とテスト用の TS ファイルが追加されました。
src/commands/list-iam-roles.ts の内容は以下です。

import {Args, Command, Flags} from '@oclif/core'

export default class ListIamRoles extends Command {
  static description = 'describe the command here'

  static examples = [
    '<%= config.bin %> <%= command.id %>',
  ]

  static flags = {
    // flag with a value (-n, --name=VALUE)
    name: Flags.string({char: 'n', description: 'name to print'}),
    // flag with no value (-f, --force)
    force: Flags.boolean({char: 'f'}),
  }

  static args = {
    file: Args.string({description: 'file to read'}),
  }

  public async run(): Promise<void> {
    const {args, flags} = await this.parse(ListIamRoles)

    const name = flags.name ?? 'world'
    this.log(`hello ${name} from /Users/naoyasu.manabe/ghq/github.com/quartetcom/aws-tools/src/commands/list-iam-roles.ts`)
    if (args.file && flags.force) {
      this.log(`you input --force and --file: ${args.file}`)
    }
  }
}

3-2. 実装

上記を参考に IAM ロールの Name が一覧されるよう実装しました。
結果、src/commands/list-iam-roles.ts の内容は以下です。

import {Args, Command, Flags} from '@oclif/core'
import { ListRolesCommand, IAMClient } from '@aws-sdk/client-iam'

export default class ListIamRoles extends Command {
  static description = 'IAM Role のリストアップ'

  static examples = [
    '<%= config.bin %> <%= command.id %>',
  ]

  static flags = {
  }

  static args = {
  }

  public async run(): Promise<void> {
    const {args, flags} = await this.parse(ListIamRoles)

    const client = new IAMClient({})
    const command = new ListRolesCommand({});
    try {
      const data = await client.send(command);
      // process data.
      data.Roles?.map(_ => this.log(_.RoleName))
    } catch (error) {
      // error handling.
      this.logJson(error)
    } finally {
      // finally.
    }
  }
}

3-3. 実行

さっそく追加したコマンドが実装できているか見てみます。

$ ./bin/run help
AWS Cli Tools for Quartetcom

VERSION
  aws-tools/0.0.0 darwin-arm64 node-v18.13.0

USAGE
  $ qcaws [COMMAND]

TOPICS
  hello    Say hello to the world and others
  plugins  List installed plugins.

COMMANDS
  hello    Say hello
  help     Display help for qcaws.
  plugins  List installed plugins.

あれ?COMMANDS に追加したはずの list-iam-roles コマンドが見当たりませんね。

$ ./bin/dev help
AWS Cli Tools for Quartetcom

VERSION
  aws-tools/0.0.0 darwin-arm64 node-v18.13.0

USAGE
  $ qcaws [COMMAND]

TOPICS
  hello    Say hello to the world and others
  plugins  List installed plugins.

COMMANDS
  hello           Say hello
  help            Display help for qcaws.
  list-iam-roles  IAM Role のリストアップ
  plugins         List installed plugins.

実行コマンドを ./bin/dev にしたら list-iam-roles が出てきました。src/commands/list-iam-roles.ts に記載した description も表示されています。
./bin/dev はビルド前のソースコードを ts-node で実行しているようです。

$ ./bin/dev list-iam-roles
Name: AWSServiceRoleForOrganizations
Name: AWSServiceRoleForSupport
Name: AWSServiceRoleForTrustedAdvisor

無事に IAM ロールの Name 一覧が取得できました。

3-4. 改造

次はフラグオプション -a あるいは --arn が付与された場合には ARN も加えて表示するようにしてみます。
src/commands/list-iam-roles.ts` の内容の差分は以下です。

   static flags = {
+    arn: Flags.boolean({char: 'a', description: 'ARN も表示したい場合は指定してください'}),
   }
 
   static args = {
@@ -22,7 +23,11 @@ export default class ListIamRoles extends Command {
     try {
       const data = await client.send(command);
       // process data.
-      data.Roles?.map(_ => this.log(_.RoleName))
+      data.Roles?.map(_ => {
+        const name = `Name: ${_.RoleName}`
+        const arn = flags.arn ? `Arn: ${_.Arn}` : ''
+        this.log([name, arn].join(' / '))
+      })
$ ./bin/dev list-iam-roles --help
IAM Role のリストアップ

USAGE
  $ qcaws list-iam-roles [-a]

FLAGS
  -a, --arn  ARN も表示したい場合は指定してください

DESCRIPTION
  IAM Role のリストアップ

EXAMPLES
  $ qcaws list-iam-roles

追加したフラグオプションが FLAGS に記載され

$ ./bin/dev list-iam-roles -a
Name: AWSServiceRoleForOrganizations / Arn: arn:aws:iam::**********:role/aws-service-role/organizations.amazonaws.com/AWSServiceRoleForOrganizations
Name: AWSServiceRoleForSupport / Arn: arn:aws:iam::**********:role/aws-service-role/support.amazonaws.com/AWSServiceRoleForSupport
Name: AWSServiceRoleForTrustedAdvisor / Arn: arn:aws:iam::**********:role/aws-service-role/trustedadvisor.amazonaws.com/AWSServiceRoleForTrustedAdvisor

実行したら所望の出力結果が得られました。

さいごに

結構細かい作業のログまで記載はしましたが内容としてはそれほどのボリュームではないと思います。oclif こんな感じなんだーと思っていただけたら幸いです。
oclif 自体はもっと多機能ですがそれほど複雑だということでもないのでよかったら公式ドキュメントをご覧いただいて遊んでみてください。
AWS から取得したデータを openai - npm を使って ChatGPT API に食わせてみる、なんていうのも手軽にできるかなーと思います。