社内 DeepRacerLeague の取り組みと縁の下から支えた超短期間での構築手法(後編)

こんにちは。
株式会社日立システムズ　ビジネスクラウドサービス事業グループの[藤巻](https://jawsdays2020.jaws-ug.jp/supporter/%e8%97%a4%e5%b7%bb%e9%9b%84%e8%a3%95/)です。

当社当事業部では昨年2019年9月より2020年3月にかけて、事業部内 DeepRacer League を開催していました。

前編は[こちら](https://qiita.com/hirosys-biz/private/a3229f403601f373c782)

後編は限られた超短期間で実施した設計・構築についてお伝えします。

# 縁の下から支えた超短期間での構築手法
## 構築にあたっての調査・準備
話は戻って、2019年8月末です。
夏季休暇をはさみ、数度の打ち合わせでレギュレーションなどを突貫ですり合わせ、支払いの準備やルートアカウント用のメールアドレスの発行といった AWS の新規アカウント作成の**準備**も終わったころです。

社内 DeepRacer は2019年9月初週開始の予定だったので残された時間は、2 ～ 3 営業日しかありませんでした。
通常業務をこなしながら、自身の AWS 認定ダブル受験の追い込みや AWS 社主催のセミナー参加、プライベートでの LT 登壇の準備などがあり、正味1人日未満しか時間が取れない状況でした。

そんな中で、この取り組みに必要なものを洗い出しました。

* AWS のアカウント
* 参加者用 AWS アカウント
* ログ管理
* ユーザー作成、権限設計・設定
* 利用料の通知方法の設計・開発 

これらを早急かつ省力に済ませられる方法を編み出さないとなりませんでした。

## 構築開始
というわけで、 環境構築を始めました。
結果からお伝えすると、2019年9月3日の夜に構築し、9月4日朝に参加者への説明会で引き渡しとなりました。

構成図は以下のとおりです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/9e48b51a-3798-e520-a043-e15e1c84a17d.png)

アカウントごとの簡単な説明です

* 親アカウント
 * Payer アカウントのことであり、事務局用のマスターアカウント
 * 子アカウントのCloudTrailを集約する S3 バケット あり
 * 後述の利用料通知用の Amzon SNS と Lambda 関数を配置。Cost and Usage から取得
 * 子アカウントをサクッと作るための Lambda 関数も配置
 * 各チームメンバー用の IAM User を管理
 * CloudFormation StackSets の管理など
* 子アカウント
 * 各チーム用のアカウント
 * CloudTrail の情報を 親アカウントの S3 バケット へ送信
 * DeepRacer の利用

ここからは以下の流れで構築・設定した内容についてお伝えします
現時点ではさらに便利な機能が実装されており、もっと省力に環境構築・設定が行えます。

1. 親アカウントの開設
2. 親カウントの初期設定
3. 子アカウントの開設
4. 子アカウントの初期設定
5. 子アカウントユーザーのログイン

## 親アカウントの開設
通常の新規アカウントと同じ作り方でアカウントを作成します。

## 親アカウントの初期設定
### CloudFormation StackSets の設定
以下の CloudFormation テンプレートを使って、CloudFormation StackSets を実行するアカウント(親アカウント)から、ほかのアカウント(子アカウント)にリソースを作成するための IAM ロール(AWSCloudFormationStackSetAdministrationRole)を作成します。

https://s3.amazonaws.com/cloudformation-StackSet-sample-templates-us-east-1/AWSCloudFormationStackSetAdministrationRole.yml


### 環境の設定
以下の CloudFormation テンプレートを使って環境設定を行います。

* CloudTrail を格納する S3 バケットの作成
* CloudTrail を格納する S3 バケットのポリシーの作成
* CloudTrail の設定
* IAM ユーザー用の共通ポリシーの作成
 
<details><summary>CloudFormation テンプレートを見るには開く</summary><div>

```yml:setupMasterEnv.yml
---
AWSTemplateFormatVersion: 2010-09-09

Resources:
  # S3 bucket for CloudTrail log
  S3BucketForCloudTrailLogs:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: '!your-s3-bucket-name!'
      AccessControl: "Private"
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "AES256"
    DeletionPolicy: Delete

  S3BucketForCloudTrailLogsPolicy:
    Type: AWS::S3::BucketPolicy    
    DependsOn: S3BucketForCloudTrailLogs
    Properties:
      Bucket:
        Ref: "S3BucketForCloudTrailLogs"
      PolicyDocument:
        Statement:
          -
            Action: "s3:GetBucketAcl"
            Effect: "Allow"
            Resource:
              Fn::Join:
                - ""
                -
                  - "arn:aws:s3:::"
                  -
                    Ref: "S3BucketForCloudTrailLogs"
            Principal:
              Service: "cloudtrail.amazonaws.com"
          -
            Action: "s3:PutObject"
            Effect: "Allow"
            Resource:
              Fn::Join:
                - ""
                -
                  - "arn:aws:s3:::"
                  -
                    Ref: "S3BucketForCloudTrailLogs"      
                  - "/*"        
            Principal:
              Service: "cloudtrail.amazonaws.com"
            Condition:
              StringEquals:
                s3:x-amz-acl: "bucket-owner-full-control"

  # CloudTrail
  CloudTrailConf:
    Type: AWS::CloudTrail::Trail
    DependsOn: S3BucketForCloudTrailLogsPolicy
    Properties:
      TrailName: "default-trail"
      IsLogging: true
      IsMultiRegionTrail: true
      S3BucketName:
      　Ref: S3BucketForCloudTrailLogs
      IncludeGlobalServiceEvents: true
      EnableLogFileValidation: true

  # IAM Policy
  IAMPolicyCommon:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: "pol-Common"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Action:
              - "sts:DecodeAuthorizationMessage"
              - "sts:GetCallerIdentity"
              #- "sts:GetSessionToken"
            Effect: "Allow"
            Resource: "*"
          -
            Action:
              - "iam:Get*"
              - "iam:List*"
              - "iam:ChangePassword"
              - "iam:*MFA*"
            Effect: "Allow"
            Resource: "*"
```
</div></details>

### 課金情報通知
無尽蔵に予算があるわけではないので、各チームに対して利用料を通知するための Lambda 関数も整備しました。

使用しているのは、 Amazon SNS, Cost and Usage Report です。
Amazon SNS については、後続の子アカウントの初期設定の中で行っています。
この Lambda 関数は AWS Lambda の Python 3.6 で実装・動作確認をしております。

<details><summary>Lambda 関数を見るには開く</summary><div>

```python:getCostReport
import datetime
import json
import os
import boto3
from decimal import *
from dateutil.relativedelta import relativedelta

# Set account_id and region
my_account = str(boto3.client('sts').get_caller_identity()['Account'])
my_region = str(os.getenv("AWS_DEFAULT_REGION"))

# Set sns_topic_arn_base
topic_arn_base = 'arn:aws:sns:'+ my_region + ':'+ my_account + ':'

# Set Timezone
JST = datetime.timezone(datetime.timedelta(hours=+9), 'JST')

# Get AWS Organizations Account List
accounts = boto3.client('organizations').list_accounts()['Accounts']
budget = os.environ['budget']

# AWSアカウントIDとそのチーム名を定義
# メールにチーム名を出さないのであればこの定義は不要
teamMap={
    "AccoudId":"TeamsName",
    "AccoudId":"TeamsName"
}

def lambda_handler(event, context):

    today = datetime.datetime.today()
    client = boto3.client('ce', my_region)

    # 1回で使用料をとっちゃう(API利用料削減)
    cau = client.get_cost_and_usage(TimePeriod={'Start': '{0}-{1:02}-01'.format(today.year, today.month),
                                                'End': '{0}-{1:02}-{2:02}'.format(today.year, today.month, today.day)},
                                    Granularity='MONTHLY', Metrics=['UnblendedCost'],
                                    GroupBy=[
                                        {
                                            'Type': 'DIMENSION',
                                            'Key': 'LINKED_ACCOUNT'
                                        }
                                    ])
                                    
    # アカウント毎に処理
    for result in cau['ResultsByTime']:
        for group in result['Groups']:
            msg = []
            accountId = str(group['Keys'][0])
            topic_arn = topic_arn_base + accountId
            teamName = teamMap.get(accountId)
            subject=('使用料のお知らせ({0})'.format(teamName))
            #subject=('使用料のお知らせ({0})'.format(accountId))            
            msg.append('チーム「 {0} 」各位\n\n本日までの使用料をお伝えします。\n'.format(teamName))
            #msg.append('チーム「 {0} 」各位\n\n本日までの使用料をお伝えします。\n'.format(accountId))

            msg.append(' {0} / {2} ({1}) 使用率：{3} %'.format(round(float(group['Metrics']['UnblendedCost']['Amount']),3),
                                                 group['Metrics']['UnblendedCost']['Unit'],
                                                 budget,
                                                 round((round((float(group['Metrics']['UnblendedCost']['Amount'])),3) / float(budget)),3)*100
                                                 ))
            msg.append('--------------------------------------------\n')
            
            #print(topic_arn)
            #print(subject)
            #print(msg)
            
            # SNS publish
            client = boto3.client('sns')
            request = {
               'TopicArn': topic_arn,
               'Message': '\r\n'.join(msg),
               'Subject': subject
            }

            response = client.publish(**request)
```
</div></details>

* 環境変数
 * budget：レギュレーションで定めた毎月の予算額。ドルで指定 

この Lambda 関数は CloudWatch Events で月曜日から金曜日の毎朝9時に実行するようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/41934b83-413d-3f14-06c0-f2e561c587f1.png)


## 子アカウントの開設
親アカウントに作成した以下の Lambda 関数にアカウント名とルートアカウント用メールアドレスを環境変数に設定して実行します。
この Lambda 関数は AWS Lambda の Python 3.6 で実装・動作確認をしております。

<details><summary>Lambda 関数を見るには開く</summary><div>

```python:createAccount
import boto3
import os
def lambda_handler(event, context):
    mail = os.environ['email']
    accountName = os.environ['accountName']
    roleName =  'default-role-by-organizations'

    org = boto3.client('organizations')
    response = org.create_account(
        Email=mail,
        AccountName=accountName,
        RoleName=roleName,
        IamUserAccessToBilling='ALLOW'
    )
    print(response)
```
</div></details>

* 環境変数
 * email：ルートアカウント用メールアドレス
 * accountName：アカウントの名前

実行結果として、12桁のアカウント ID が返ってくるので控えておきます。
※AWS Organizations の画面からも確認可能です。

## 子アカウントの初期設定
### ルートアカウントのパスワード設定
このままでは、ルートアカウントのパスワードが分からない状態になっています。
この[ドキュメント](https://docs.aws.amazon.com/ja_jp/organizations/latest/userguide/orgs_manage_accounts_access.html#orgs_manage_accounts_access-as-root)を参考にして、パスワードの設定を行います。

### CloudFormation StackSets の設定
以下の CloudFormation テンプレートを使って、CloudFormation StackSets を実行するアカウント(親アカウント)からのリクエストを受け付けるための IAM ロール(AWSCloudFormationStackSetExecutionRole)を子アカウントに作成します。

本テンプレートは、親アカウントのアカウントID(12桁の数字)を指定して実行する必要があります。

https://s3.amazonaws.com/cloudformation-StackSet-templates-us-east-1/managed_account_role.template

### CloudFormation StackSets を利用した 子アカウント側の設定
※親アカウント側で実施します。
※この流れを1回実施後に、子アカウントが増えた際には、次の項から実行します。

１. CloudFormation の画面を表示し、StackSets を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/830fa5fc-2754-dddc-7772-a444d0e79e06.png)

２．StackSet の作成ボタンをクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/1e635529-ace2-38a0-3cb8-74add137faa4.png)

３．テンプレートファイルのアップロードを行い、次へをクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/6327dc4b-56d6-aa4b-aba5-dfabe357347e.png)

アップロードするのは以下の内容の CloudFormation テンプレートです。

<details><summary>CloudFormation テンプレート見るには開く</summary><div>

```yml:setup_target_stack.yml
---
AWSTemplateFormatVersion: 2010-09-09

Parameters:
    masterAccountId:
        Type:                 String

Resources:
  AdminIAMRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "admin_role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Action:
              - "sts:AssumeRole"
            Principal:
              AWS: !Sub "arn:aws:iam::${masterAccountId}:root"

  AdminIAMPolicy:
    DependsOn: AdminIAMRole
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: "admin"
      PolicyDocument:
        Statement:
          -
            Effect: "Allow"
            Action:
              - "*"
            Resource: "*"
      Roles:
        - "admin_role"

  CloudTrailConf:
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: "default-trail"
      IsLogging: true
      IsMultiRegionTrail: true
      S3BucketName: "dracer-cloudtrail-logs"
      IncludeGlobalServiceEvents: true
      EnableLogFileValidation: true
```
</div></details>

４．StackSet の設定を行います
StackSet 名や、パラメーターの指定を行い、次へをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/d2af7c7e-7a5b-3434-ab76-fff054465ec0.png)

５．アクセス許可の設定を行い、次へをクリックします。
「セルフサービスのアクセス許可」を選択し、**AWSCloudFormationStackSetAdministrationRole** を選択しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/5081c815-a0c9-6a67-e82d-7dea970e5fc1.png)

>補足：
>本環境は AWS Organizations を利用しているので「サービスマネージドアクセス許可」のほうが簡単です。
>しかし、この機能がリリースされたのは2020年2月半ばのため、2019年9月時点では使用できませんでした。
>本稿を参考にしてAWS Organizations を利用環境で構築される方は、「サービスマネージドアクセス許可」をお勧めします。これまでお伝えした IAM ロールの事前設定なども不要になります。

６．デプロイ先を指定します
「スタックをアカウントにデプロイ」を選択し、デプロイ先の子アカウントのID(12桁の番号)を指定します。複数ある場合は、カンマ区切りで指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/68c4ce34-4720-efc2-a374-43a04c065f65.png)

７．デプロイ先のリージョンを指定します
今回は DeepRacer を使うことがメインなので、 バージニアリージョン(米国北東部：us-east-1)を指定しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/8dbebfb8-62ae-8947-ed61-6a86ec16711c.png)

８．デプロイオプションを指定し「次へ」をクリックします
デプロイ先のアカウントが複数ある場合は、同時アカウントの最大数を増やすことで速く終わります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/b8164a15-fb55-df02-81cb-06ff8f9beba3.png)

９．レビュー画面で問題がなさそうであれば(適宜チェックボックスにチェックを入れて)「送信」ボタンをクリックします

これで、指定したアカウントに対して設定が行われます。

### CloudFormation StackSets を利用して 新たに追加された子アカウント側の設定
１．前項で作成した StackSet を選択して[アクション]＞[StackSet に新しいスタックを追加]をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/7694a54a-819a-3685-b96e-c4a2601287da.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/e7190250-1462-39ff-7aa8-2c6e09925f83.png)

２．あとは、前項の 6 から 9 と同様の操作を行い、[次へ]をクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/f7ae02ff-4a8f-8b4d-70a2-e8a5dbb4a587.png)

３．レビュー画面で問題がなさそうであれば(適宜チェックボックスにチェックを入れて)「送信」ボタンをクリックします

### 子アカウント用の設定
※親アカウント側で実施します。
以下の CloudFormation テンプレートを適宜編集し、親アカウント側の「スタック」として実行します。
子アカウントの数だけスタックを作成・実行します。

<details><summary>CloudFormation テンプレート見るには開く</summary><div>

```yml:setup_Template.yml
---
AWSTemplateFormatVersion: 2010-09-09

Metadata:
    "AWS::CloudFormation::Interface":
        ParameterGroups:
            - Label:
                  default: "AccountId"
              Parameters:
                  - AccountId
            
            - Lable:
                defaut: "Team Id"
              Parameters:
                  - TeamId

            - Label:
                  default: "UserId"
              Parameters:
                  - userN

Parameters:
    AccountId:
        Type:                 String
        Default:              ''

    TeamId:
        Type:                 String
        Default:              'dracer_N'
    userN:
        Type:                 String
        Default:              ''

Resources:
  # IAM
  # IAM Policy
  IAMPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "pol-${TeamId}"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Action:
              - "sts:AssumeRole"
            Effect: "Allow"
            Resource:
              !Sub "arn:aws:iam::${AccountId}:role/admin_role"
          -
            Action:
              - "iam:ChangePassword"
              - "iam:*LogionProfile"
              - "iam:*AccessKey*"
            Effect: "Allow"
            Resource:
              Fn::Join:
                - ""
                - 
                  -
                    !Sub "arn:aws:iam::${AccountId}:user/"
                  - "${aws:username}"  

  # IAM Group
  IAMGroup:
    DependsOn: IAMPolicy
    Type: AWS::IAM::Group
    Properties: 
      GroupName: !Sub ${TeamId}
      ManagedPolicyArns:
        -
          Fn::Join:
            - ""
            -
              - "arn:aws:iam::"
              - 
                !Ref AWS::AccountId
              - ":policy/pol-Common"             
        - 
          Fn::Join:
            - ""
            - 
              - "arn:aws:iam::"
              - 
                !Ref AWS::AccountId
              - ":policy/pol-"
              -
                !Sub  ${TeamId}

  # IAM User 人数分増やし適宜編集
  IAMUserN:
    DependsOn: IAMGroup
    Type: AWS::IAM::User
    Properties:
      UserName: !Sub ${userN}
      Groups:
        - !Sub ${TeamId}
      LoginProfile:
        Password: "P@ssw0rd"
        PasswordResetRequired: true

  # SNS Topic
  SNSTeams:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: !Sub ${AccountId}
      TopicName: !Sub ${AccountId}
      Subscription: 
        - Endpoint: !Sub "${TeamId}@example.com"
          Protocol: "email"
```
</div></details>

この CloudFormation テンプレートを実行すると、SNSTeams の Endpoint で指定したメールアドレスに対して以下のようなメールが届きますので、Confirm subscriptionリンクをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/7bfbd15f-54ad-355c-16c6-99dc3ebf30c5.png)

すると、ブラウザが開き以下のような画面が表示されるので設定が完了します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/f7af1aa6-e67c-d065-7abf-209b0f173738.png)


## 子アカウントユーザーのログイン方法
マネージメントコンソールのスイッチロール機能を利用してログインを行います。
※2020年3月時点での画面構成です。 AWS 社の仕様変更により画面の内容や構成が変わることがあります。

1. AWSへアクセスする　https://dracer-hisys-mng.signin.aws.amazon.com/console
2. 各自に連絡したユーザーIDとパスワードでログインし、初回はパスワードの設定を行います
3. 以下の流れでチームごとのコンソールに移行します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/725a4ad4-4553-e171-4198-ff5d132f6a3b.png)
4.ロールの切り替え
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/c6de09f4-6de7-dfc5-6239-9bc11342cdcd.png)
5.アカウントIDやロール、表示名の指定を行い[ロール切り替え」ボタンをクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/790a3d8a-7bb5-fdd3-4958-a979d271ac83.png)
6.切り替え完了
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/548623/e691850b-c68e-fbda-28bb-56a04fcc8700.png)
1度設定したら、「ロール履歴」にある、表示をクリックするだけでOKです

# まとめ・振り返り
実質1人日未満という限られた時間の中で、多数の環境を省力かつ迅速に構築することができました。
これもクラウドを使ううえでの利点であると感じています。
今回は、AWS DeepRacer を題材としていましたが、例えばハッカソン用などさまざまな活動に横展開を行い活用ができそうです。

登壇資料作成やブログ化にあたり、既存の設定や定義、画面などを見直してみました。
その際、機能拡充が行われていることがわかったので、次回以降の活動ではそのあたりも盛り込み、最適化を図っていきたいです。

# 日立システムズについて
https://jawsdays2020.jaws-ug.jp/supporter/%e6%a0%aa%e5%bc%8f%e4%bc%9a%e7%a4%be%e6%97%a5%e7%ab%8b%e3%82%b7%e3%82%b9%e3%83%86%e3%83%a0%e3%82%ba/