🦁

AWS CDK で AWS アカウントのユーザーを一気に100個作る

2020/11/28に公開

はじめに

  • AWS CDK を使って一気にAWSアカウントのユーザーを100個 (+adminアカウント3つ) 作るソースを公開[1]
  • TypeScript によるパスワード生成器を実装し、生成器で得られた文字列を 作成したAWS アカウントの初期パスワードとして設定
  • 使い道は謎・・・というより自己満足

ソース

前提条件

  • CDK のセットアップが完了していること[2]

動作環境

  • 下記の環境で動作することを確認

OS

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.7
BuildVersion:	19H15

AWS CDK

$ cdk --version
1.74.0 (build e86602f)

AWS CLI

$ aws --version
aws-cli/2.1.1 Python/3.9.0 Darwin/19.6.0 source/x86_64

Node.js

$ node -v
v12.19.1

npm

$ npm --version
6.14.8

AWS CDK による AWS アカウントのユーザー生成

概要は以下の通り

  • 管理者アカウント3つ
    • グループ名は admins, グループに適用するポリシーは AdministratorAccess [3]
    • ユーザー名は admin_01, admin_02, admin_03
  • 開発者パワーユーザーアカウント3つ
    • グループ名は developers, グループに適用するポリシーは PowerUserAccess [3:1]
    • ユーザー名は user_xxx (0番から99番の連番)
  • グループを作成→ポリシー適用→グループに所属するユーザーを作成と追加
  • 管理者アカウント用のパスワード、開発者パワーユーザー用のパスワードをそれぞれ TypeScriptで実装したパスワード生成器で作成し初期パスワードとして設定
  • cdk deploy実行後、出力されたパスワードでログイン
simple_iam_user_management.ts
import * as cdk from '@aws-cdk/core';
import { Group, ManagedPolicy, User } from '@aws-cdk/aws-iam';
import * as password from './password_generator';

const admins = 'AdminGroup';
const adminUsers = [
    'admin_01',
    'admin_02',
    'admin_03'
];

const developers = 'DevGroup';

export class SimpleIamUserManagementStack extends cdk.Stack {
    constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        // AWS managed policy
        const adminPolicy = ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess');
        const powerUserPolicy = ManagedPolicy.fromAwsManagedPolicyName('PowerUserAccess');

        // Admin group
        const adminGroup = new Group(this, admins, { groupName: admins });
        adminGroup.addManagedPolicy(adminPolicy);
        let adminPassword = "";
        // For Admin
        {
            const c = new password.composite_password_generator();
            c.add(new password.digit_generator(4));
            c.add(new password.lower_letter_generator(3));
            c.add(new password.symbol_generator(3));
            c.add(new password.upper_letter_generator(3));
            adminPassword = c.generate();
        }

        // Admin User
        adminUsers.forEach(admin => {
            new User(this, admin, {
                userName: admin,
                groups: [adminGroup],
                password: cdk.SecretValue.plainText(adminPassword),
                //passwordResetRequired: true // 初期パスワードでログインしたあとパスワードの再設定を促す。個人のアカウントで使う範囲であれば不要
            })
        })


        // Developer group
        const devGroup = new Group(this, developers, { groupName: developers });
        devGroup.addManagedPolicy(powerUserPolicy);
        let devPassword: string = "";

        // For Developer's Password
        {
            const c = new password.composite_password_generator();
            c.add(new password.digit_generator(3));
            c.add(new password.lower_letter_generator(3));
            c.add(new password.symbol_generator(3));
            c.add(new password.upper_letter_generator(3));
            devPassword = c.generate();
        }

        const numOfUser = 100;
        for (let n = 0; n < numOfUser; n++) {
            const user = "user_" + ('000' + n).slice(-3);
            new User(this, user, {
                userName: user,
                groups: [devGroup],
                password: cdk.SecretValue.plainText(devPassword),
                //passwordResetRequired: true // 初期パスワードでログインしたあとパスワードの再設定を促す。個人のアカウントで使う範囲であれば不要
            });
        }

        // Output Admin Password
        new cdk.CfnOutput(this, "adminPassword", {
            value: adminPassword
        });

        // Output Developer Password
        new cdk.CfnOutput(this, "devPassword", {
            value: devPassword
        });
    }
}

パスワード生成器

概要

クラス図

password_generator_cd.png

ソース

password_generator.ts
abstract class password_generator {
    public abstract allowed_chars(): string;
    public abstract length(): number;
    public abstract add(generator: password_generator): void;
}


class basic_password_generator extends password_generator {
    private readonly _length: number;

    constructor(len: number) {
        super();
        this._length = len;
    }

    public allowed_chars(): string {
        throw new Error("Method not implemented.");
    }

    public add(generator: password_generator) {
        throw new Error("Method not implemented.");
    }
    public length(): number {
        return this._length;
    }
}


export class digit_generator extends basic_password_generator {
    constructor(length: number) {
        super(length);
    }
    allowed_chars(): string {
        return "0123456789";
    }
}


export class symbol_generator extends basic_password_generator {
    constructor(length: number) {
        super(length);
    }
    allowed_chars(): string {
        return "!@^#$%&()[]{}?";
    }
}


export class upper_letter_generator extends basic_password_generator {
    constructor(length: number) {
        super(length);
    }
    allowed_chars(): string {
        return "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    }
}


export class lower_letter_generator extends basic_password_generator {
    constructor(length: number) {
        super(length);
    }
    allowed_chars(): string {
        return "abcdefghijklmnopqrstuvwxyz";
    }
}


function shuffle(str: string): string {
    let buffer = [];
    for (let i = 0; i < str.length; i++) {
        buffer.push(str[i]);
    }

    let currentIndex = str.length, temporaryValue, randomIndex;
    while (0 !== currentIndex) {
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex -= 1;
        temporaryValue = buffer[currentIndex];
        buffer[currentIndex] = buffer[randomIndex];
        buffer[randomIndex] = temporaryValue;
    }

    let shuffle_string = "";
    for (let i = 0; i < buffer.length; i++) {
        shuffle_string += buffer[i];
    }
    return shuffle_string;
}


export class composite_password_generator extends password_generator {
    public allowed_chars(): string {
        throw new Error("Method not implemented.");
    }

    public length(): number {
        throw new Error("Method not implemented.");
    }

    private generator: password_generator[];

    constructor() {
        super();
        this.generator = [];
    }

    generate(): string {
        let password = "";
        this.generator.forEach(g => {
            for (let i = 0; i < g.length(); i++) {
                const randomIndex = Math.floor(Math.random() * g.allowed_chars().length);
                password += g.allowed_chars()[randomIndex];
            }
        });
        return shuffle(password);
    }

    add(generator: password_generator): void {
        this.generator.push(generator);
    }
}

実行結果

デプロイ

  • cdk deployのコマンドを実行
  • デプロイ成功時、管理者アカウント用と開発者パワーユーザー用のパスワードを表示
  • ユーザー名とパスワードでログインできることを確認

Screen Shot 2020-11-28 at 17.59.25.png

作成したユーザーの存在を確認

管理者アカウントの確認

  • admin_0x ユーザーの存在の確認
    • aws iam list-users --query "Users[].[UserName]" --output text | grep "admin_*"

Screen Shot 2020-11-28 at 18.23.13.png

  • admin_01 所属グループの確認
    • aws iam list-groups-for-user --user-name admin_01 --query 'Groups[].GroupName'

Screen Shot 2020-11-28 at 18.45.08.png

  • グループのポリシーを確認
    • aws iam list-attached-group-policies --group-name AdminGroup --query 'AttachedPolicies[].PolicyArn'

Screen Shot 2020-11-28 at 18.31.20.png

開発者パワーユーザーアカウントの確認

  • user_xxx ユーザーの存在の確認
    • aws iam list-users --query "Users[].[UserName]" --output text | grep "user_*"

Screen Shot 2020-11-28 at 18.13.41.png

  • user_001 所属グループの確認
    • aws iam list-groups-for-user --user-name user_001 --query 'Groups[].GroupName'

Screen Shot 2020-11-28 at 18.47.06.png

  • グループのポリシーを確認
    • aws iam list-attached-group-policies --group-name DevGroup --query 'AttachedPolicies[].PolicyArn'

Screen Shot 2020-11-28 at 18.46.01.png

おわりに

  • AWS アカウントのユーザーを 100+α 個をCDKで作成
  • パスワード生成器を使いたかっただけという感じは否定できないが、ちょっとした実験用途にはなった
脚注
  1. AWS アカウントに対する最大のユーザー数は5000 ↩︎

  2. AWS CDK Intro Workshop の Prerequisites にある手順を踏むこと ↩︎

  3. 職務機能の AWS 管理ポリシー ↩︎ ↩︎

  4. https://github.com/PacktPublishing/The-Modern-Cpp-Challenge/blob/master/Chapter08/problem_68/main.cpp ↩︎

  5. 乱数の精度はあまりよろしくないらしいので、精度を気にする場合はJavaScriptでメルセンヌ・ツイスト乱数を使う方法まとめ【mt.js】等を参考 ↩︎

Discussion