Cognitoユーザープールからの送信メールをカスタマイズする

サービスの認証にCognitoユーザープールを使用する際、確認コードや、初回ログインパスワードなどをメールでお知らせする場合があると思います。

Cognitoのデフォルト設定だとこんなメールが届きます。

Your confirmation code is 927173
Your username is xxxx and temporary password is xxxxxx.

このメールタイトル、本文をカスタマイズする方法です。

カスタムメッセージが設定できるイベント

公式ドキュメントからの抜粋です。
以下がカスタムメッセージが設定できるイベントの一覧です。

AWS Lambda トリガーのリクエストおよびレスポンスパラメータ

triggerSource 値 トリガーイベント
CustomMessage_AdminCreateUser カスタムメッセージ – 新規ユーザーに一時パスワードを送信するため.
CustomMessage_ResendCode カスタムメッセージ – 既存ユーザーに確認コードを再送するため.
CustomMessage_ForgotPassword カスタムメッセージ – 忘れたパスワードのリクエスト用の確認コードを送信するため.
CustomMessage_UpdateUserAttribute カスタムメッセージ – ユーザーの E メールまたは電話番号が変更されると、このトリガーは確認コードをそのユーザーに自動的に送信します。他の属性には使用できません。
CustomMessage_VerifyUserAttribute カスタムメッセージ – ユーザーが手動で新しい E メールや電話番号の認証コードをリクエストすると、このトリガーからユーザーに認証コードが送信されます。
CustomMessage_Authentication カスタムメッセージ – 認証時に MFA コードを送信するため.

今回はtriggerSource値がCustomMessage_AdminCreateUserの場合(Cognitoユーザープールのポリシーに「管理者のみにユーザーの作成を許可する」を設定していて、管理者がユーザーを作成した際にユーザーに送信されるメール)のカスタマイズを例にします。

手順

  1. Lambdaファンクションの作成
  2. Cognitoユーザープールに1.で作成したファンクションを登録

1. Lambdaファンクションの作成

Python3.6での例です。
ファンクションのIAMロールにはCloudwatchログ出力権限の付与だけでOKです。以下IAMポリシーの例です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "*"
            ],
            "Effect": "Allow"
        }
    ]
}

Lambdaファンクションで受け取るイベントをログ出力するとこのような感じ。

{
    "version": "1",
    "region": "ap-northeast-1",
    "userPoolId": "ap-northeast-1_xxxxxxxxx",
    "userName": "hogee",
    "callerContext": {
        "awsSdkVersion": "aws-sdk-js-2.176.0",
        "clientId": "CLIENT_ID_NOT_APPLICABLE"
    },
    "triggerSource": "CustomMessage_AdminCreateUser",
    "request": {
        "userAttributes": {
            "sub": "bf6e9c66-0a69-476e-bfd8-724d38e6555f",
            "cognito:email_alias": "hoge@example.com",
            "email_verified": "True",
            "cognito:user_status": "FORCE_CHANGE_PASSWORD",
            "name": "hoge",
            "email": "hoge@example.com"
        },
        "codeParameter": "{####}",
        "usernameParameter": "{username}"
    },
    "response": {
        "smsMessage": "None",
        "emailMessage": "None", # ここをカスタムした本文に変える
        "emailSubject": "None" # ここをカスタムしたタイトルに変える
    }
}

このイベントをそのままreturn eventとするとデフォルト設定が適用されたメールが送られます。カスタマイズするにはresponseのフィールド内をカスタマイズしたい内容に書き換えてリターンします。

cognito_custom_message.py
# -*- coding:utf-8 -*-

def handler(event, context):
    if event['triggerSource'] == 'CustomMessage_AdminCreateUser':
        customed_event = custom_message_admin_create_user(event)

    return customed_event


def custom_message_admin_create_user(event):

    email_message = '''
{username} 様
<br>
<br>
管理者から招待されました。
<br>
<br>
ログインメールアドレス:{mail}
<br>
初回ログインパスワード:{password}
'''.format(username='{username}',
           mail=event['request']['userAttributes']['email'],
           password='{####}')

    event['response']['emailSubject'] = '仮パスワード発行のお知らせ'
    event['response']['emailMessage'] = email_message

    return event

{username}にユーザーネーム、{####}にパスワードが入ってメールが送信されることになります。
triggerSourceによって本文に含めなくてはいけないコードパラメータは異るため先ほどの公式ドキュメントで確認してください。
triggerSourceがCustomMessage_AdminCreateUserの場合、メッセージ本文に{username}{####}が入っていないとエラーになります。

もし追加で他のカスタムメッセージイベントにも対応したい場合、handler()

    elif event['triggerSource'] == 'CustomMessage_ForgotPassword':
        customed_event = custom_message_forgot_password(event)

とやると対応できます。

2. Cognitoユーザープールに1.で作成したファンクションを登録

Lambdaファンクション作成後はCognitoユーザープールのコンソールにいき、トリガーカスタムメッセージから作成したLambdaファンクションを指定します。

スクリーンショット 2018-02-03 20.03.27.png

serverless frameworkではこのようになります。

serverless.yml
<略>
functions:
  cognitoCustomMessage:
    handler: functions/cognito_custom_message.handler
    role: CognitoCustomMessageRole
    events:
      - cognitoUserPool:
          pool: UserPool
          trigger: CustomMessage
resources:
  Resources:
    CognitoUserPoolUserPool:
      <以下略>

設定後、アプリケーションからユーザーを登録してみると、

スクリーンショット_2018-02-03_20_27_35.png

カスタマイズされたメールが届きました。

以上です。

続きを読む

FATAL problem: DomainLabelEmpty encountered

CloudFormationでroute53を作っている時FATAL problem: DomainLabelEmpty encounteredに出会いました

ELBRecord:¬
  Type: 'AWS::Route53::RecordSet'
  Properties:
    Type: A
    AliasTarget:
      DNSName: !GetAtt␣AppELB.DNSName¬
      HostedZoneId: !GetAtt AppELB.CanonicalHostedZoneID
    Comment: 'A test Record' 
    HostedZoneName: !Ref HostedZoneName¬
    Name: !Sub 'test.001.${HostedZoneName}.'

原因

parameterで定義されたHostedZoneNameが以下のようになります

HostedZoneName:
  Type: String
  Default: 'mydomain.co.jp.'

これでELBRecordのNameプロパティの中身がtest.001.mydomain.co.jp..隣、ルーツレベルの.が二つになってしましました。Name: !Sub 'test.001.${HostedZoneName}'に直せば問題解決できました。

まとめ

FATAL problem: DomainLabelEmpty encounteredが出たらどこかに..入っているということです。

続きを読む

AWSとAzureとGCPを比較してみる – FaaS編

FaaSについてAWSとAzureとGCPを比較してみました。

注)

1. FaaS比較表

AWS Azure GCP
Lambda Functions Cloud Functions***
言語 Python,
node.js,
java,
C#,
go
ランタイムバージョン1.X
C#,
JavaScript,
F#,
Python*,
PHP*,
TypeScript*,
バッチ (.cmd、.bat)*,
Bash*,
PowerShell*

ランタイムバージョン2.X
C#**,
JavaScript**,
Java**
node.js***
最大実行時間 5分 10分 (従量課金プラン)
無制限 (App Serviceプラン)
9分
直接HTTPアクセスを受け付けるか 受け付けない(API Gatewayと連携必要) 受け付ける 受け付ける
トリガー Amazon S3,
Amazon DynamoDB,
Amazon Kinesis Data Streams,
Amazon Simple Notification Service,
Amazon Simple Email Service,
Amazon Cognito,
AWS CloudFormation,
Amazon CloudWatch Logs,
Amazon CloudWatch Events,
AWS CodeCommit,
スケジュールされたイベント (Amazon CloudWatch Events を使用),
AWS Config,
Amazon Alexa,
Amazon Lex,
Amazon API Gateway,
AWS IoT ボタン,
Amazon CloudFront,
Amazon Kinesis Data
Blob Storage,
Cosmos DB,
Event Hubs,
HTTP,
Microsoft Graph Events(2.Xのみ),
Queue storage,
Service Bus,
Timer,
Webhooks(1.Xのみ)
HTTP,
Cloud Storage,
Cloud Pub/Sub

*試験段階
**プレビュー
***ベータ

2. 対応言語の比較

言語の種類は試験段階とプレビューを含めればAzureが一番多いのですが、正式リリースされたものに限定すればAWSの方が種類が多いです。
一方GCPは機能自体がベータリリースなので、まだこれからといった感じでしょうか。

AzureはBashにも対応しているのが特徴です。運用系のシェルスクリプトをFaaS化すれば、スクリプト用のサーバが不要になりますね。

3. 最大実行時間

最大実行時間はAzureの10分(要host.jsonのfunctionTimeoutプロパティ変更)、GCPの9分に対しAWS Lamdbaは5分と約半分です。実際にAWS Lambdaを利用していると5分の壁を結構感じます。この点は他クラウドが羨ましいですね。
2017年のRe:InventでAWSはFargateというコンテナのサービスをリリースしましたが、このサービスがlambdaが5分以上実行できないことに対するAWSからの回答のように感じます。

4. 直接HTTPアクセスを受け付けるか

AWS lambdaだけ直接HTTPアクセスを受け付けることができません。HTTPアクセスを受け付けるには、API Gatewayと連携する必要がありますが、多機能な分やや設定が面倒な印象です。(但しAPI経由でLambdaを起動することは可能)

まとめ

AWS Lambdaのリリース後、Azure・GCP・Bluemix(現IBM Cloud)は超特急で追従しました。AWS LambdaがIT業界に与えたインパクトはとても大きかったと思います。
現在は「FaaS無ければばクラウドにあらず」といったところでしょうか。

また、AWS GreengrassやAzure IoT Edge**というエッジにデプロイするサービスも出てきています。
将来AWS LambdaがiPhoneやApple Watchにデプロイできるようにならないかなーと妄想中です。

**プレビュー

続きを読む

AWS EC2でHyperledger Fabricを動かす(2.Fabric前提条件の準備)

AWSにUbuntuをセットアップし、FabricをDockerで起動してみたいと思います。

前回は、AWS EC2を用意するところまででした。
https://qiita.com/tmikada/items/a1f22fd5c61c2de71d19

今回は、Fabric導入のための前提条件を準備していきます。

必要なものはこちら

  • curl
  • docker 17.06.2-ce or greater
  • docker-compose 1.14.0 or greater
  • go 1.9.x
  • node.js 6.9.x or greater (7.xは現時点でサポート対象外)
  • npm
  • python 2.7

参照: http://hyperledger-fabric.readthedocs.io/en/latest/prereqs.html

順番に導入していきます。

curl

インストール済み

$ curl --version
curl 7.47.0 (x86_64-pc-linux-gnu) libcurl/7.47.0 GnuTLS/3.4.10 zlib/1.2.8 libidn/1.32 librtmp/2.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp smb smbs smtp smtps telnet tftp 
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP UnixSockets 

docker

手順: https://docs.docker.com/engine/installation/linux/docker-ce/ubuntu/#install-using-the-repository

$ sudo apt-get update
$ sudo apt-get install 
     apt-transport-https 
     ca-certificates 
     curl 
     software-properties-common

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
OK
$ sudo apt-key fingerprint 0EBFCD88
pub   4096R/0EBFCD88 2017-02-22
      Key fingerprint = 9DC8 5822 9FC7 DD38 854A  E2D8 8D81 803C 0EBF CD88
uid                  Docker Release (CE deb) <docker@docker.com>
sub   4096R/F273FCD8 2017-02-22

$ sudo add-apt-repository 
    "deb [arch=amd64] https://download.docker.com/linux/ubuntu 
    $(lsb_release -cs) 
    stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce
$ apt-cache madison docker-ce

$ docker --version
Docker version 17.12.0-ce, build c97c6d6

docker-compose

手順: https://docs.docker.com/compose/install/#install-compose

$ sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose --version
docker-compose version 1.18.0, build 8dd22a9

go

手順: https://github.com/golang/go/wiki/Ubuntu

$ sudo apt-get install golang-go
$ go version
go version go1.6.2 linux/amd64

# バージョン古いため、新しいのを入れる

$ sudo add-apt-repository ppa:gophers/archive
$ sudo apt update
$ sudo apt-get install golang-1.9-go
$ go version
go version go1.6.2 linux/amd64

# 変わってない。。。
# 手順をよく見ると、1.9はこちらにあるらしい
$ /usr/lib/go-1.9/bin/go version
go version go1.9.2 linux/amd64

# パスを変更します
$ cd /usr/bin/
$ sudo rm go
$ sudo ln -s ../lib/go-1.9/bin/go
$ go version
go version go1.9.2 linux/amd64

GOPATHを設定します

$ mkdir ~/go
$ vi ~/.bashrc

# 以下の2行を追記
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin

$ source ~/.bashrc
$ echo $GOPATH
/home/ubuntu/go

node.js, npm

nodeのバージョン管理は、nを使うことにしてみます
手順: https://qiita.com/seibe/items/36cef7df85fe2cefa3ea

バージョンの組み合わせは
https://nodejs.org/ja/download/releases/
を参照し、nodejs 6.9.5, npm 3.10.10 を選択

$ sudo apt-get install nodejs npm
$ nodejs --version
v4.2.6
$ npm --version
3.5.2

# nをインストールしてnodeの新しいバージョンを導入する
$ npm cache clean
$ sudo npm install -g n
$ n --version
2.1.7

$ sudo n 6.9.5
$ node --version
v6.9.5

# 古いnodejsはまぎらわしいため削除しておく
$ sudo apt-get purge nodejs

# バージョン指定してnpmをインストール
$ sudo npm install -g npm@3.10.10
$ npm --version
3.10.10

Python

2.7がインストールされていました

$ python --version
Python 2.7.12

ここまでで、やっと前提条件の準備が完了です。。。
次回はFabricを動かすところまで書くつもりです。

続きを読む

ECSで運用していた社内ツールをFargate化したときに、ハマりやすかった3つのポイント

AWS Fargate Advent Calendar 2017の25日目の記事になります。

他の方が詳細な見解や調査など行われているので、大トリの記事がこれぐらいの内容で大丈夫なのかビビりながら書いています。
ひとまず、年末年始でFargateを試してみようかなあという方の参考になれば。

1.どんな環境をFargate化したか

fargate.png

上記のような構成でECSで運用していた社内ツールのprpr(※)をFargate化しました。
現在のFargateの制限としては、

  1. 東京リージョンがない
  2. SLAがない
  3. 知見が少ない

ということで、production環境にいきなり入れるというよりは、こうしたサービスレベルの低い社内ツールから移行するのがよいかと思います。

※1 SLA設定されてました Amazon Compute サービスレベルアグリーメントを Amazon ECS および AWS Fargate に拡張

※2 prprについては、下記のブログを参照
prprでGithubのPullRequestレビュー依頼をSlack通知する

2.ハマったところ

2-1.FargateがECRのコンテナイメージをpullできない

デプロイしたECSのステータスが、延々とSTOPPEDを繰り返して、ECSのログを見ると、下記のようなエラーが出力され続けているときがありました。残念ながら、Fargate化されても、デプロイ失敗したときなど、ECSが再起動しまくるのは、自前で何とか検知する仕組みを作らないといけなさそう…
ss 2017-12-24 11.51.14.png

解決例:Fargate側にPublicIPを付与する

Fargate: CannotPullContainer located on ECS registry
にもあるように、FargateはVPC内部で起動してくるため、VPC外部への通信経路を確保しておかないと、FargateがECRからコンテナイメージをおとしてくることができません。特にセキュリティ上などで問題なければ、AssignPublicIpを有効化しておきましょう。

CFn例
Service:
    Type: AWS::ECS::Service
    Properties:
        ServiceName: !Ref RoleName
        Cluster: !Ref ECSCluster
        DesiredCount: 1
        LaunchType: FARGATE
        TaskDefinition: !Ref ECSTask
        LoadBalancers:
            -
                ContainerName: !Sub ContainerName
                ContainerPort: 3000
                TargetGroupArn: !Ref ALBTargetGroup
        NetworkConfiguration:
            AwsvpcConfiguration:
                AssignPublicIp: ENABLED
                SecurityGroups:
                    - !Ref ECSSecurityGroup
                Subnets: !Ref SubnetIds

2-2.ECSの動的ポートマッピングは使えない

Fargateのデプロイ中は、下記の画像のような感じで、同一ポートでプライベートIPが異なるという状況になる。Fargateが使用するサブネットでプライベートIPが枯渇したときにどうなるかは未検証。Fargateの起動コンテナ数の制限とかないのであれば、サブネットマスクの設計とかはちょっと注意しておいた方がよさそう。
ss 2017-12-25 7.30.10.png

解決例:ホスト側のポートを固定する

CFn例
ECSTask:
    Type: AWS::ECS::TaskDefinition
    Properties:
        Family: !Ref FamilyName
        NetworkMode: awsvpc
        RequiresCompatibilities:
            - FARGATE
        Cpu: 256
        Memory: 512
        ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole"
        ContainerDefinitions:
            -
                Name: !Ref TaskName
                Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${RegistoryName}:${ImageTag}"
                PortMappings:
                    -
                        ContainerPort: 3000
                        HostPort: 3000
                Essential: "true"
                Ulimits:
                    -
                        Name: nofile
                        SoftLimit: 65535
                        HardLimit: 65535
                Environment:
                    -
                        Name: PORT
                        Value: 3000
                    -
                        Name: RACK_ENV
                        Value: production
                LogConfiguration:
                    LogDriver: awslogs
                    Options:
                        awslogs-group: !Ref CloudWatchLogGroup
                        awslogs-region: !Sub ${AWS::Region}
                        awslogs-stream-prefix: !Ref ImageTag

2-3.CodePipelineでFargateのデプロイを行う、CFnの記述方法がわからない

AWS CodePipeline に Amazon ECS および AWS Fargate のサポートを追加
CodePipeline で ECS にデプロイできるようになり、Docker 環境の継続的デリバリも簡単になりました
にもあるのですが、12/12に、CodePipeline上でFargateのデプロイがサポートされています。
ss 2017-12-25 16.47.19.png
ただ、上記のようなイメージで、CodePipelineとFargateを連携させようとしたときに、CFnのドキュメントからだとCFnでのサンプルが見つけられませんでした。
ひとまず、下記のように書いたら、CFnでも何とか通ったけど、合ってるのかしら(どこかに公式チュートリアルとか準備されてるかな)

解決例:CodeBuildでimagedefinitions.jsonを出力して、CodePipelineのDeployフェーズと連携させる

buildspec.yml例
version: 0.2

phases:
    pre_build:
        commands:
            - $(aws ecr get-login --region $AWS_DEFAULT_REGION)
            - REPOSITORY_URI="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}"
            - IMAGE_TAG=${CODEBUILD_RESOLVED_SOURCE_VERSION}
    build:
        commands:
            - echo Build started on `date`
            - docker build -t "${IMAGE_REPO_NAME}:${IMAGE_TAG}" .
            - docker tag "${IMAGE_REPO_NAME}:${IMAGE_TAG}" "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
    post_build:
        commands:
            - echo Build completed on `date`
            - docker push "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${IMAGE_REPO_NAME}:${IMAGE_TAG}"
            - printf '[{"name":"container-name-sample","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
artifacts:
    files:
        - imagedefinitions.json
    discard-paths: yes

imagedefinitions.jsonをCodeBuildで生成したフォルダ直下においておけば、
あとは下記のようなCodePipelineの書き方で、Fargateでもデプロイが可能になる。

CFn例
CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    DependsOn: CodePipelineS3
    Properties:
        Name: codepipeline-sample
        ArtifactStore:
            Type: S3
            Location: !Ref S3BucketName
        RoleArn: !Ref RoleArn 
        Stages:
            -
                Name: Source
                Actions:
                    -
                        Name: Source
                        RunOrder: 1
                        ActionTypeId:
                            Category: Source
                            Owner: ThirdParty
                            Version: 1
                            Provider: GitHub
                        Configuration:
                            Owner: hoge
                            Repo: fuga
                            Branch: master
                            OAuthToken: xxxxxxxxxxxx
                        OutputArtifacts:
                            - Name: Source
            -
                Name: Build
                Actions:
                    -
                        Name: CodeBuild
                        RunOrder: 1
                        InputArtifacts:
                            - Name: Source
                        ActionTypeId:
                            Category: Build
                            Owner: AWS
                            Version: 1
                            Provider: CodeBuild
                        Configuration:
                            ProjectName: !Ref CodeBuild
                        OutputArtifacts:
                            - Name: Build
            -
                Name: Deploy
                Actions:
                    -
                        Name: Deploy
                        ActionTypeId:
                            Category: Deploy
                            Owner: AWS
                            Version: 1
                            Provider: ECS
                        InputArtifacts:
                            - Name: Build
                        Configuration:
                            ClusterName: !Ref ClusterName
                            ServiceName: !Ref ServiceName

3.参考記事

他の方のアドベントカレンダーがすごく参考になったので、CFnまわりの実装で参考にさせていただいた記事をいくつか紹介させていただこうと思います。

Fargate を試した感想と ecs-deploy で Fargate にデプロイできるようにする話
AWS CloudFormationを使ってAWS Fargateの環境を作成してみる
ECS+EC2で動いているサービスをFargateにのせ替える

4.まとめ

社内で使っていたECSをFargate化したことで、EC2の管理(障害対応、およびセキュリティアップデート対応)をなくすことができました。VPCのサブネット設計など、Fargateにしてもインフラ面を意識しないといけないところはありそうなので、また知見がたまれば共有させていただこうと思います。

それでは、皆様メリークリスマス!

続きを読む

CloudFormationを使ってECS環境とそのデプロイシステムを作成する

こんにちは、LIFULLのchissoです。
この記事は、私が勤務するLIFULLのAdvent Calender1の24日目の記事です。

今日はクリスマス・イブですね。みなさまいかがお過ごしでしょうか。
私はQiitaに2本Advent Calenderの記事を上げています。

もう一つはこちらにAthenaの記事を書いています。

さて、早速ですが本題です。
今年の夏頃、AWSで新規サービスを作成する機会がありました。そこで、CloudFormation(以下CFn)を使って、デプロイシステムとElasticContainerService(以下ECS)のサービスを管理する仕組みを作りました。ベースはawslabsが公開しているコチラのリポジトリです。

当時、Qiitaやクラスメソッドさんのブログに大変お世話になりながらなんとかサービスインにこぎつけたのですが、自分なりに要点やハマりどころなど解説したいと思います。

私が当時調べて取り入れたこと、ハマったことなどつらつらと書くのでかなり長いです。
いざやろうと思われた際に参照してもらえれば幸いです。

はじめに

まずはじめに、なぜCFn・ECSを使ったのか簡単に述べておきます。

CFn

こちらは、AWS内のネットワーク・サービス構成もコードで管理したかった、という点につきます。もう流行りというのも憚れますが、Infrastucture as a Codeというやつですね。メリットとしては、

  1. 変更を追跡可能になる

    • 誰がいつ変更したかわからないSecurity GroupやNetwork ACLがなくなる
  2. 開発環境で作った構成がそのまま本番環境で再現できる
    • AWSコンソールはよくできていますが、ポチポチなど人の作業が発生する以上どうしても作業漏れが発生します

思いつくデメリットは、CFn templateの読み書きが辛いことでしょうか。書くときはひたすらAWSの公式ドキュメントとにらめっこです。ただし、慣れてしまえばドキュメントがよくできているので、結構スラスラ書けるようになっていきます。

ECS

こちらは、実行環境を開発環境と本番環境で揃えたい、という点です。またしても解説不要かと思いますが、dockerを使いたいだけです。
Elastic Beanstalk(EB)とどちらを採用するか結構迷ったのですが、ECRを使わないEBではbuildとdeployが一体化してしまうことが気になり、ECRを使うならECSでいいじゃん、となりました。
今年のre:InventでFargateが発表されましたし、ECSにしておいてよかったなと思っているところです。

前提となる知識

CfnとECSがどんなものか、という導入については、優れた記事がすでにたくさんありますので紹介しながら少しだけ補足します。

Cfn

CFnテンプレートは、jsonとyamlの両方で書くことが可能です。

複数のコンポーネントをCFnで管理しようとすると、1つのテンプレートが肥大化してしまいます。しかしaws cliを使うことで、複数のテンプレートに各コンポーネントを定義することが可能です(統合用のテンプレートが必要となります)。

余談ですが、このaws cloudformation packageコマンドは非常に便利で、lambdaファンクションなども参照してアップロードしてくれます。

ECS

ECSについては、はじめ用語が少しわかりにくいですが、下記の認識で良いと思います。

用語 説明
クラスター サービスが稼働するEC2インスタンス(群)
サービス タスクとクラスター、ELB(ターゲットグループ)を紐付ける。EBでいうアプリケーションのような感じ。
タスク コンテナの集まりで、docker-compose.ymlのイメージ。

CfnでECSのデプロイシステムを構築する

本題に入ります。

ベースはawslabsのリポジトリに公開されているものです。
AWSの公式ブログClassMethodさんの記事で、メリットや概略は既に紹介されているので、各yamlの役割や、私が変更を行ったところについて目的と変更方法、また細かいハマりどころを書いていきます。

全体像

ecs-refarch-continuous-deployment.yaml
 |- vpc.yaml
 |- load_balancer.yaml
 |- ecs_cluster.yaml
 |- service.yaml
 - deployment-pipeline.yml

ecs-refarch-continuous-deployment.yamlが全体像(Stack)を定義したCFn templateで、その他のyamlがネストしたStackとなっています。

ecs-refarch-continuous-deployment.yaml内に相対パスで各yamlを記載して、上述したaws cloudformation packageを実行すると、各yamlがS3へアップロードされた上でecs-refarch-continuous-deployment.yamlのパスがS3パスへ書き換えられます。
※ awslabsのecs-refarch-continuous-deployment.yamlはリポジトリ内の子templateを別でS3にあげてあり、初めからそちらを参照するように書かれています。

// before
Cluster:
  Type: AWS::CloudFormation::Stack
  Properties:
    TemplateURL: templates/ecs-cluster.yml
    Parameters:
      VpcId: !Ref VpcId

// after
Cluster:
  Properties:
    Parameters:
      VpcId:
        Ref: VpcId
    TemplateURL: https://s3.amazonaws.com/cfn-templates/{hash値}.template
  Type: AWS::CloudFormation::Stack

また、この構成の場合ecs-refarch-continuous-deployment.yamlがWeb console(またはAPI)から実行するテンプレートとなり、その際に各種パラメータ(Parametersブロックで定義したもの)を与えます。
ecs-refarch-continuous-deployment.yamlからネストしたテンプレートへのパラメータの引き渡しや、ネストしたテンプレート間での値のやりとりもecs-refarch-continuous-deployment.yaml上で行われることになります。

VPC

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/vpc.yaml

VPC, route table, subnetsなど、ネットワーク周りの基本的な定義を行っています。

私は既存のVPC内にECSクラスタを使いたかったため、こちらは利用しませんでした。
OutputsにSubnetsとVpcIdがある通り、別templateでそれらを利用しますが、既存VPC/Subnetのidが利用可能なので、Parametersブロックに定義して渡すようにしています。

LoadBalancer

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/load-balancer.yaml

LoadBalancerと、紐付けるTargetGroupやSecurityGroupを定義しています。

こちらも、初め利用しようとしていましたが最終的にやめました。
というのも、CFnテンプレートを更新した際に、ELB以外のStackが更新される場合は問題ありませんが、Stackの作り直しやELB Stackの更新が発生すると、ELBのURLが変わってしまいます。

実際にELBをCFnで管理するのであれば、Route53もCFn内で管理して、ELBのエンドポイントに対してCNAMEレコードを作成する必要があると思います。
※ 私が作ったものは社内利用のみだったため、固定されたURLさえあればよく、ELBを固定してパラメータでStackに与えることで対応しました。

ECS Cluster

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/ecs-cluster.yaml

このあと定義する、ECSのサービスを稼働させるインスタンスや、AutoScalingGroupの定義を行っています。(Fargateになればいらなくなるはずのテンプレートです。)

Resoures.SecurityGroup

デフォルトでは、SecurityGroupIngressのIpProtcolに-1が設定されており、ELBのSecurityGroupに対してすべてのポートが解放されています。
http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/APIReference/API_AuthorizeSecurityGroupIngress.html
私は、明示的にポート解放したかったことと、有事の際にsshでインスタンスにログインして調査を行うために、下記の通り変更しました。

SecurityGroupIngress:
  # hostポートが動的なため、ELBに対してエフェメラル開放
  - SourceSecurityGroupId: !Ref SourceSecurityGroup
    IpProtocol: TCP
    FromPort: 1024
    ToPort: 65535
  # ClusterSshBastionSecurityGroupは踏み台サーバーのSecurityGroup
  - SourceSecurityGroupId: !Ref ClusterSshBastionSecurityGroup
    IpProtocol: TCP
    FromPort: 22
    ToPort: 22

Service

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/service.yaml

ECSのサービス、タスクを定義しています。実際のアプリケーション周りの設定はここで行います。
例えば、コンテナで設定したい環境変数はEnvironmentブロックで定義します。

containerのログをCloudWatchに送る

こちらのスニペットにある通りですが、下記の設定を行うことでコンテナのログをCloudWatchへ送ることができます。

Resources:
  CloudWatchLogs:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${AWS::StackName}
      RetentionInDays: 14 # 2週間

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub ${AWS::StackName}
      ContainerDefinitions:
        - LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Sub ${AWS::StackName}
              awslogs-region: ap-northeast-1
              awslogs-stream-prefix: hogehoge
    # その他propertyは一旦省略

LogGroupはなんでもよいですが、私はStackNameにしました。既存のものを流用するのであれば、上記のCloudWatchLogsのブロックは不要です。

DeploymentPipeline

https://github.com/awslabs/ecs-refarch-continuous-deployment/blob/master/templates/deployment-pipeline.yaml

今回の肝になる部分です。
Github -> CodePipeline -> CodeBuild -> ECS
の流れを自動化するCFn stackをつくります。

概ねサンプルのままで動きますが、ハマりどころが多かったです。

Pipelineブロックのイメージ

私が当時ほとんどCodePipelineを使ったことがなかったためでもありますが、templateだけ見ているとイメージがかなり掴みづらかったので、少し解説します。

1. Pipeline.Stages[0](Name: Source)

Githubからソースをクローンしてきます。
そして、ArtifactStoreで定義したLocationに{なにかのhash値}.zip(※)という名前で保存します。

正直、このサンプル(というかCFnの仕様?)の一番意味がわからないところです。

このName: SourceのOutputArtifactsはAppで、Name: BuildのStage(CodeBuild)に対してInputArtifactとしてAppを渡しています。
awslabsのyamlだと、そのInputArtifactを無視して、別ブロックで定義されたCodeBuildProjectsに記載の通りArtifactとしてBucketを直参照した上で、${ArtifactBucket}/source.zipを参照してCodeBuildを実行しています。
でも、source.zipなんてS3にはないんです、、、

CodeBuildのドキュメントには、

  • source-location: 必須値 (CODEPIPELINE に source-type を設定しない場合)。指定されたリポジトリタイプのソースコードの場所。

    • Amazon S3 では、ビルド入力バケット名の後に、スラッシュ (/) が続き、ソースコードとビルド仕様 (例: bucket-name/object-name.zip) を含む ZIP ファイルの名前が続きます。これは ZIP ファイルがビルド入力バケットのルートにあることを前提としています。(ZIP ファイルがバケット内のフォルダにある場合は、代わりに bucket-name/path/to/object-name.zip を使用してください)。

と記載されていますが、object-nameってなんやねん:joy:
でもこのままでうまく動きます。source.zip以外にしてみたことはないので、どうなってるのかはよくわかりません、、

2. Pipeline.Stages[1](Name: Build)

githubから取得したソースを元に、CodeBuildProjectブロックに記載の通りにdocker buildを実行します。そしてできあがったコンテナをECRにpushします。

はまりどころというわけでもないですが、私が一瞬こんがらがった点を少し。冷静になってみると当たり前なんですが、CodeBuildProjectのEnvironmentブロックは、docker buildを行うdockerコンテナ上の環境変数です。dockerfile内で使えるENVや、最終的なdockerコンテナ(タスク)に渡される環境変数ではありません。Dockerfileで外から変数を受取りたい場合、build.commandsのdockerコマンドに--build-args HOGE=hogeなどとargsを渡してください。

3. Pipeline.Stages[2](Name: Deploy)

ECRのリポジトリがタグ付きで更新されているので、ConfigurationにそのURIなど含めることでいい感じにタスク定義が更新され、service.ymlで定義したスタックが更新されます。(多分)

多分、と書いたのは、実は私は別の方法をとっています。
ここのConfigurationの記述、探したかぎりドキュメントが見つからず、、

- Name: Deploy
  Actions:
    - Name: Deploy
      ActionTypeId:
        Category: Deploy
        Owner: AWS
        Version: 1
        Provider: ECS
      Configuration:
        ClusterName: !Ref Cluster
        ServiceName: !Ref Service
        FileName: images.json
      InputArtifacts:
        - Name: BuildOutput
      RunOrder: 1

私は複数のコンテナをTaskDefinitionに含めており、1つのURIでは対応できませんでした。images.jsonをいい感じに書き換えればうまくいくのか、、わからなかったため、下記の方法をとりました。

複数コンテナを含むタスクをCodePipelineからアップデートする

やったことは、service.yamlをdeployment-pipeline.yamlにネストさせて、DeployアクションでStackの更新を行うようにしました。

- Name: Deploy
  Actions:
    - Name: Deploy
      ActionTypeId:
        Category: Deploy
        Owner: AWS
        Version: 1
        Provider: CloudFormation
      Configuration:
        ChangeSetName: Deploy
        ActionMode: CREATE_UPDATE
        StackName: !Sub "${AWS::StackName}-Service"
        Capabilities: CAPABILITY_NAMED_IAM
        # githubからcloneしたファイルをパス指定
        TemplatePath: App::cfn_templates/packaged_service.yml
        RoleArn: !GetAtt CloudFormationExecutionRole.Arn
        ParameterOverrides: !Sub |
          {
            "Tag" : { "Fn::GetParam" : [ "BuildOutput", "build.json", "tag" ] },
            "DesiredCount": "${TaskDesiredCount}",
            "Cluster": "${Cluster}",
            "TargetGroup": "${TargetGroup}",
            "EcrRepositoryPrefix": "${EcrRepositoryPrefix}",
            "Environment": "${Environment}",
            "S3Bucket": "${S3Bucket}"
          }
      InputArtifacts:
        - Name: App
        - Name: BuildOutput
      RunOrder: 1

軽く解説を。

  • 事前にservice.yamlはaws cloudformation packageコマンドでpackageしておきます。
  • テンプレートはgithubから取得したリポジトリに含んでいるため、InputArtifact経由でのパス指定を行います。
  • ECR Repositoryのprefixと、buildした際のタグをservice.yamlにパラメータとして与えます
    • service.yaml側では下記のような形で、タグを含むURIを指定しておきます。
TaskDefinition:
  Type: AWS::ECS::TaskDefinition
  Properties:
    Family: !Sub ${AWS::StackName}
    ContainerDefinitions:
      - Name: hoge
        Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepositoryPrefix}/hoge:${Tag}
            - Name: fuga
        Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${EcrRepositoryPrefix}/fuga:${Tag}

これで、CodePipelineからCloudFormationのCREATE_UPDATEが行われ、めでたくTaskDefinitionの更新->Serviceの更新という順でDeployが行われます。

はまりどころとしては、ParameterOverridesのブロックが、1000文字しか使えません。もろもろ変数展開されたあとだと、結構簡単に1000文字超えて、

Please provide a valid JSON with maximum length of 1000 characters.

って言われます。
Forumにこんなissueがある程度で、(当時)ドキュメントは見つかりませんでした。

私は悩んだ末、はじめParameterOverridesに書いていた値を、yaml_vaultを使ってリポジトリ内で暗号化して管理するようにしました。

おわりに

すごくながくなりました。疲れました。
細かい所見直せてないかもしれませんが、クリスマスイブにこれ以上は悔しいので一度公開します。

このあたりは後日編集されるかもしれません。。

いろいろと個人的な感情が入っていますが、CloudFormationで環境作ってみたいなーという方の参考になれば幸いです。
えっ、Deploy周りはそんなことしないでCircle CI使えって?あーあー聞こえないー。
(一応今回はAWS内で完結することを目標にしてこの形に落ち着いています)

続きを読む

GCPクラウドアーキテクトの模擬試験をAWS脳で解説してみる

Fusic Advent Calendar 2017 19日目担当、@Kta-Mです。
IoTのテスト用仮想デバイス作成サービスmockmockのプロダクトオーナーをしております。

以前よりAWS アドバンスドコンサルティングパートナーの認定を頂いている弊社ですが、今年はmockmockを通じてソラコム インテグレーションパートナーGCP テクノロジーパートナー と、新たな領域に踏み出した一年でした。

で、ただいまGCPのクラウドアーキテクトの認定を取るべく、お勉強中です。
GCPの試験、AWSと違って模擬試験が無料で何度も受けられるんです!しかも正解を教えてくれる!!

ただ、さすがになぜそれが正解なのか解説まではしてくれないので、GCPにまだ慣れていないAWS脳で解説+蛇足をつらつら書いてみます。AWSは知ってるけどこれからGCPの試験を受けるという、かなりピンポイントな方々の手助けになればと思います。

問題と選択肢の順番は固定のようなので、その前提で書いています。模擬試験を受けないことには何を書いてるのか分からないと思うので、受けてきてからここに戻ってきてください!
https://cloud.google.com/certification/practice-exam/cloud-architect?hl=ja

No.1 (アクセス頻度の低いデータをクラウドに…)

  • AWSのS3に相当するCloud Storageのストレージクラスに関する問題。
  • 「アクセス頻度が低い」のでまずNearlineへ保存する。
  • 5年以上古いものは使わないので削除する。
  • ストレージMultiRegionはアクセスされるエリアへの言及がないので違う。
  • (参考)ストレージクラス
    • https://cloud.google.com/storage/docs/storage-classes?hl=ja
    • Multi-Regional Storage
      • 複数リージョンで保存(AWSにはない)
      • 世界中からアクセスされるようなデータに使う
      • Regional Storageに比べるとちょっとパフォーマンスが低いので分析対象のデータなんかには向かない
      • AWSにはCross-Region Replicationがあるけど、こちらはバケット名が別になるし、リージョン間データ転送量がかかってしまう。
    • Regional Storage : 1つのリージョンで保存 (AWS S3の標準クラス)
      • 普通の。
    • Nearline : 低頻度アクセス用(AWS S3の標準 – 低頻度アクセスクラス)
      • 月イチ程度でアクセスされるぐらいのデータ向き
      • AWSと同じく取り出し料金が発生する
      • AWSと同じく最小保存期間が30日
      • AWSと違って最小オブジェクトサイズがない
    • Coldline : さらに低頻度アクセス用(AWS Glacier)
      • 年イチ程度でアクセスされるぐらいのデータ向き
      • AWSと同じく取り出し料金が発生する
      • AWSと同じく最小保存期間が90日
      • AWSと違って低レイテンシで取り出せる
    • 低冗長化(Durable Reduced Availability)も一応あるけど非推奨。
  • (参考)ライフサイクル

No.2 (会社のアーキテクチャは以下の図のように…)

  • 異なるリージョンからのアクセスが必要なので、Cloud Storageを選択
  • (参考)ストレージ比較
    • https://cloud.google.com/storage-options/?hl=ja
    • 非構造化データ
      • Cloud Storage for Firebase : Mobile用
      • Cluod Storage : AWSのS3/Glacier的なやつ
    • 分析用ストレージ
      • Cloud Bigtable : NoSQLのビッグデータデータベース。低レイテンシで高スループット。HadoopやCloud Dataflow、Dataprocと簡単に統合できる
      • Big Query : SQLでの分析が可能なデータウェアハウス。スケーラブルでフルマネージド。
    • RDB
      • Cloud Spanner : 水平スケーリングが可能なRDB。AWSでいうとAurora Serverlessが近い?
      • Cloud SQL : AWSのRDS的なやつ
    • NoSQL
      • Cloud Firestore for Firebase : Mobile用
      • Cloud Datastore : AWSのDynamoDB的なやつ

No.3 (大規模なウェブ アプリケーションを構築しています…)

  • 「独立したプロジェクト」「RFC1918」を満たすのは共有VPC
  • (参考)共有VPC
     - https://cloud.google.com/compute/docs/shared-vpc/?hl=ja

    • 同一アカウントの別プロジェクト間でVPCを共有
    • AWSと違ってGCPには「プロジェクト」という概念があって、プロジェクトごとにまるっと別の環境が作られるイメージ。
  • (参考)RFC1918
    • https://www.nic.ad.jp/ja/translation/rfc/1918.html
    • プライベートアドレス空間
      • 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
      • 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
      • 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
    • GCPのドキュメントにはわりとよく出てくる
  • (参考)VPC
    • https://cloud.google.com/compute/docs/vpc/?hl=ja
    • http://www.mpon.me/entry/2017/04/22/020428
    • AWSと違ってグローバルなプライベート通信スペースを提供
    • AWSと同じくVPC Peeringがある
    • 同一アカウントの別プロジェクト間でVPCを共有できる(共有VPC)
    • Cloud StorageなどのGCPサービスにもプライベートネットワークで接続可能
    • サブネットの予約IPアドレスは4個
      • ネットワーク アドレス(CIDR 範囲の最初のアドレス)
      • デフォルト ゲートウェイ アドレス(CIDR 範囲の 2 番目のアドレス)
      • 予約アドレス(CIDR 範囲の最後から 2 番目のアドレス)
      • ブロードキャスト アドレス(CIDR 範囲の最後のアドレス)
      • AWSは、0,1,2,3,255の5個が予約されている

No.4 (セキュリティカメラの映像を収集して…)

  • 最初の30日間は読み込み頻度が高そうなのでRegional
  • それ以降はほぼ使われなさそうなのでアーカイブとしてColdlineに移行
  • (参考)永続化ディスク

No.5 (以下に示す Google Cloud Deployment Manager…)

  • インスタンステンプレートに、具体的な永続化ディスクが指定されているのがダメ

    • 1台目はOKかもしれないが、2台目からはアタッチできなくてエラーになりそう
    • そもそもインスタンステンプレートに具体的なディスクの指定があるのがダメなのかも
  • (参考)オートスケーリング関連の名称
    • マネージドインスタンスグループ(AWSのAuto Scaling Group)
    • インスタンステンプレート(AWSのLaunch Config)
  • (参考)永続化ディスクのアタッチ
    • https://cloud.google.com/persistent-disk/?hl=ja
    • 読み書き可能にするのであれば、1台のインスタンスにしかアタッチできない
    • 読み込みのみであれば、AWSと違って複数台のインスタンスにアタッチできる

No.6 (次の図に示すような CI/CD パイプラインプロセスが…)

No.7 (本番環境トラフィックを提供するアプリケーションを…)

  • プロダクションでしかテストできないので、問題が起こっても一番傷が浅い選択肢を選ぶ
  • 選択肢1
    • 全アクセスが新アプリの方に行ってしまう。そもそもロールバックは緊急時の手段なので、ロールバックが計画手順に入ってるのは不適切
  • 選択肢2
    • 全アクセスが新アプリの方に行ってしまう。ちょっと大げさすぎる気も。オンボードの意味が調べても分からないけど向き先を変えるぐらいの意味かな。。
  • 選択肢3
    • 全アクセスが新しいアプリの方に行ってしまう。クライアントのサブセットってなんだろう。
  • 選択肢4
    • 一部のアクセスのみ新アプリに行くので、様子見するには最適。問題が起きたときに戻しやすくもある。
  • (参考)トラフィックの分割

No.8 (アプリケーションのマイクロサービスの1つに…)

  • 「問題が発生している間に、マシンをデバッグする必要があります」-> 問題の発生をリアルタイムで知りたい
  • 選択肢1
    • 日が暮れる
  • 選択肢2
    • 方法としては有効かもしれないが、設問とズレてる
  • 選択肢3
    • 方法としては有効かもしれないが、設問とズレてる
  • 選択肢4
    • 「特定のログ」が発生する分、ログの行数が増えるということ?いずれにせよこれでリアルタイムで問題発生を認識できる。
  • (参考)Stackdriver
    • https://cloud.google.com/stackdriver/?hl=ja
    • AWSのCloudWatch + α
    • もともと単体のサービスだったのをGCPが取り込んだらしく、GCPだけでなくAWSやその他オープンソースパッケージとも統合されているとか。
    • Stackdriver Error Reporting
      • 実行中のクラウドサービスで発生したクラッシュ数をカウントして、分析と集計を実施
    • Stackdriver Trace
      • VM,コンテナ,GAEなどのレイテンシ情報を集計、可視化
    • Stackdriver Logging
      • ログ管理、分析
    • Stackdriver Debugger
      • コードの任意の位置でのアプリケーションの状態をキャプチャ
    • Stackdriver Monitoring
      • ダッシュボード、モニタリング、アラート
      • Slackとも統合できるらしい

No.9 (Dress4Win は将来的に Google Cloud に…)

No.10 (Dress4Win の開発者は、Google Cloud Platform を評価…)

  • GAEがマネージドサービス
  • Google Cloud SDKを使うことで、プロビジョニングが自動化できる

No.11 (下のアーキテクチャ図は、ユーザーがアップロードした…)

  • 生画像はCloud Storageに入れるのが楽な気がする。GAEに送ってもデータが残らないし、作るの大変だし、コストかかるし、タグ付け処理への受渡しを頑張らなきゃだし。
  • タグ付けはCloud Function, 圧縮処理はデータ変換なのでCloud Dataflowを使う
  • Cloud StorageへのファイルアップロードをトリガーにCloud Functionを実行できるらしいけど、なんでPub/Subを間に入れてるんだろ。同時実行数の上限にかからないようにとか?
  • (参考)Google Cloud Function
  • (参考)Google Cloud Pub/Sub

No.12 (Dress4Win は、クラウドにデプロイされたテスト環境…)

  • ペネトレーションテストは自分でしなければならない(Googleに依頼できない)
  • 外部からの侵入テストなので、それに近い状況で試さねばならない
    • GCP内部にセキュリティスキャナを置くのは不適切。
    • VPNもプライベートネットワーク内でのアクセスになるので不適切。
  • (参考)ペネトレーションテスト

No.13 (Dress4Winのセキュリティチームは…)

  • Google Cloud Shellを使えば満たせる操作ばかり

No.14 (マルチペタバイトのデータセットをクラウドに…)

No.15 (米国中部リージョンにある本番環境 Linux 仮想マシンの…)

  • スナップショットはグローバルに使えるので、コピーするだけならCloud Storageに上げる必要は無いんだけど、「コピーの管理」のために上げてるのかな。。
  • (参考)イメージ VS スナップショット
    • https://www.apps-gcp.com/gce-snapshot-backup/
    • イメージはあくまでオートスケーリングのイメージテンプレート用らしく、AMIと同じように扱うのは違うかも。
    • スナップショットからのインスタンス起動は可能。
    • スナップショットは差分で保存されてくが、イメージは都度まるごと保存する。
    • スナップショットは取得したプロジェクト内でしか使えない。
    • どちらもリージョン間で共有可能

No.16 (お客様がストレージプロダクトを…)

No.17 (お客様が、会社のアプリケーションを Google Cloud Platform に移行…)

No.18 (最近のソフトウェア更新が原因となり…)

  • Cloud Storageの静的データはバージョニングしておけばロールバックできる
  • ローリング更新をすると引き返しやすい
  • Cloud Deployment Managerは関係ない
  • VMのスナップショットからの復元は、スナップショット取った後の更新が消える
  • (参考)ローリング更新

No.19 (企業のウェブ ホスティングプラットフォーム上で…)

  • green-blueデプロイモデルはロールバックしやすくするだけで減りはしないような気がするけど。。途中で引き返すのはロールバックにカウントされない?
  • カナリアリリースは本番環境でやらないと意味がない。
  • マイクロサービス化することで、サービス間が疎結合になってミスが減らせそう。
  • (参考)green-blueデプロイ
  • (参考)カナリアリリース
    • https://cloudplatform-jp.googleblog.com/2017/04/how-release-canaries-can-save-your-bacon-CRE-life-lessons.html
    • カナリアリリースのコンセプトは、1913 年に生理学者の John Scott Haldane 氏が、一酸化炭素を検出するためにカゴの中の鳥を炭鉱に連れて行ったことが始まりです。かよわい鳥は人間よりもこの無臭ガスに敏感で、ガス漏れが起きているとすぐに木から落ちてしまうため、それが炭鉱員にとってその場から離れるべきサインとなるのです。

No.20 (リードソフトウェアエンジニアは、新しいアプリケーションの…)

  • 問題がよくわからないけど、複数台構成のWebシステムで、サーバー間でセッションが共有されなくても大丈夫なようにするには?ってこと?
  • セッションアフィニティの話かな。

あわせて読みたい

まとめ

比較しながら見ていくと、それぞれの思想、生い立ちが垣間見えて面白いですね。
たとえばVPCについては、GCPはグローバル、AWSはリージョン毎だったりするわけですが、Googleのサービスはあまり地域性が無いのに対し、Amazonは各国内での商品販売がメインなのでこんな感じになってるのかなと思いを馳せてみたり。

需要がどれほどあるかは分かりませんが、同じ境遇の人の助けになれば幸いです!

続きを読む

シェルシェルのAWS運用術

この記事は「OpsJAWS Advent Calendar 2017」と「Shell Script Advent Calendar 2017」の 18日目の記事です。

あらすじ

時はまさに大AWS時代、この荒波をレガシー技術を駆使して乗りこなす、この世の全てはそこから持ってきた!

AWS-Makefile

AWS-Makefileは GNU Makeを使ってAWS Cloudformationのスタックと AWS EC2のAMIをビルドする為の共通定義インクルードファイルとユーティリティスクリプト群を一式にまとめたものです。複数の CloudformationスタックとカスタムAMIを組み合わせたビルドを下記の様な Makefileを記述するだけで定義できます。

Makefile
include AWS-Makefile.mk

KEY_NAME=your-key-name
CW_NAME_TAG=customweb:latest

all: MyCustomWeb

MyCustomWeb: MyNetwork MyCustomWebAmi MyCustomWebStack

MyCustomWebStack: CW_AMI_ID = $(shell ./amibake id $(CW_NAME_TAG))
MyCustomWebStack: CC_OPTS = --parameters ParameterKey=NetworkStackName,ParameterValue=MyNetwork ParameterKey=MyCustomWebAmiId,ParameterValue=$(CW_AMI_ID) ParameterKey=KeyName,ParameterValue=$(KEY_NAME)
MyCustomWebStack: $(DAM)/MyCustomWeb.stack

MyCustomWebAmi: AB_NAME_TAG = $(CW_NAME_TAG)
MyCustomWebAmi: $(DAM)/MyCustomWeb.ami

MyNetwork: $(DAM)/MyNetwork.stack

clean:
    @./cfn_delete.sh --wait MyCustomWeb
    ./amibake rmi $(CW_NAME_TAG)
    @./cfn_delete.sh --wait MyNetwork
    @rm -f $(DAM)/*.stack $(DAM)/*.ami

この Makefileが makeコマンドを実行すると MyNetwork MyCustomWebAmi MyCustomWebStackターゲットが順にビルドされ、構築されたWebサーバにアクセスしてみると。。。はい、拍手!

MA!.png

最初にインクルードしている AWS-Makefile.mkは現状こんな感じです。まだ幾つか不便な箇所は有るので、適宜修正&追加していく予定です。内容を簡単に説明しておくと .amwmakeディレクトリに指定の *.stackファイルが無い場合は CFnTemplate/*.yamlを create-stackして作成、*.amiファイルが無い場合は AMIBakefile/*.amibを amibake buildして作成、既にビルド済みの場合は前回ビルドしたものを削除してからビルドする等の定義を行っています。

AWS-Makefile.mk
DAM=.awsmake
CFNT=CFnTemplate
AMIB=AMIBakefile
TOOL_PATH=./
CFN_CREATE=cfn_create.sh
CFN_DELETE=cfn_delete.sh
AMIBAKE=amibake

$(DAM)/%.stack: $(CFNT)/%.yaml
    @if [[ -f $@ ]]; then \
        echo $* stack clean; \
        $(TOOL_PATH)$(CFN_DELETE) --wait $* $(CD_OPTS); \
    fi
    @echo $* stack build
    @$(TOOL_PATH)$(CFN_CREATE) --wait $* $< $(CC_OPTS)
    @if [[ ! -d $(DAM) ]]; then \
        mkdir -p $(DAM); \
    fi
    @touch $@

$(DAM)/%.ami: $(AMIB)/%.amib
    @if [[ -f $@ ]]; then \
        echo $* ami clean; \
        $(TOOL_PATH)$(AMIBAKE) rmi $(AB_NAME_TAG) $(AB_OPTS); \
    fi
    @echo $* ami build
    @$(TOOL_PATH)$(AMIBAKE) build -f $< $(AB_OPTS)
    @$(TOOL_PATH)$(AMIBAKE) push $(AB_NAME_TAG) $(AB_OPTS)
    @if [[ ! -d $(DAM) ]]; then \
        mkdir -p $(DAM); \
    fi
    @touch $@

cfn_create & cfn_delete

AWS-Makefile.mkで使用している cfn_create.shと cfn_delete.shは AWS CLIの cloudformationコマンドをラップして、作成するスタック名とテンプレートファイル名、削除するスタック名だけ指定すればよい様にしたスクリプト、–waitオプションで作成、削除の完了待ちをする機能を追加してます。現状 –parametersオプションの指定が冗長なので、近いうちに何とかしたいなと

$ ./cfn_create.sh [--profile <prof>] [--wait] <stack_name> <template_file> [more parameters]

$ ./cfn_delete.sh [--profile <prof>] [--wait] <stack_name> [more parameters]

cfn_create.shに渡している Cloudformationテンプレートは特に解説するつもりは無かったのですが、テンプレート間で作成リソースIDを受け渡しを行っているクロススタック参照部分はわりと調べたので記述例として上げておきます。

CFnTemplate/MyNetwork.yaml(一部抜粋)

Outputs:
  MyVPCId:
    Value: !Ref MyVPC
    Export:
      Name: !Sub ${AWS::StackName}-MyVPCId

CFnTemplate/MyCustomWeb.yaml(一部抜粋)

Resources:
  MyCustomWebSg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: MyCustomWebSg
      VpcId:
        Fn::ImportValue:
          !Sub ${NetworkStackName}-MyVPCId

Parameters:
  NetworkStackName:
    Type: String
    Default: MyNetwork

amibake

AWS-Makefile.mkで使用している amibakeは AWS CLIと packerコマンドをラップして、Dockerfile風の定義ファイルでカスタムAMIを作成したり、Dockerイメージ風の name:tag で AMIを管理出来る様にしたツールコマンドです。何でこんなものを作ったかというと Docker確かに便利で情報系のサービスや検証環境の構築には凄い良いのだけれど、ミッションクリティカルな用途にはインフラが多重化して障害点が増えるし、パフォチュー&トラシューのノウハウも必要やし、Dockerと同じ操作感でネイティブクラウドインフラが構築出来ればいいじゃね?という発想から、docker-compose相当の部分は Terraformと Cloudformationのどちらを採用するか一瞬検討したけど、管理情報を別建てで保持しなくていいのが楽そうだったので後者を採択

$ amibake tag ami-xxxxxxxx amazonlinux:201703
$ amibake id amazonlinux:201703
$ amibake search amazonlinux

$ # After writing AMIBakefile
$ amibake build
$ amibake push my_app:20170819
$ amibake run my_app:20170819
AMIBakefile/MyCustomWeb.amib
# AMIBakefile: MyCustomWeb
MAINTAINER nmrmsys
FROM ami-da9e2cbc # Amazon Linux AMI 2017.09.1 (HVM), SSD Volume Type
USER ec2-user
#BUILDER "vpc_id":"vpc-xxxxxxxx","subnet_id":"subnet-xxxxxxxx","associate_public_ip_address":true

COPY AMIBakefile/MyCustomWeb.sh ~/
COPY AMIBakefile/html/index.html ~/

RUN sudo sh ~/MyCustomWeb.sh
RUN sudo mv ~/index.html /var/www/html/

ami_backups & aws_stops

AWS-Makefileとは全く関係無いですが AWS CLIと jqの組み合わせで作ってみたシリーズのスクリプトで ami_backupsは EC2のタグにバックアップ指定を設定しておくと取ってくれるヤツ、売りはAMIでn世代バックアップとか出来る点と –dry-runオプションで動作確認出来る様にした所でしょうか、aws_stopsは RDSのSingleAZインスタンスが停止が可能になったけど 1週間で勝手に上がってくるぜって時に、なら毎日朝晩に開始停止をすればいいんじゃね? と RDS ついでに EC2も同じくタグ設定に基づいて所定の時刻に開始停止させるようにしたヤツです。

ami_backups [--profile <prof>] [--all | <inst>] [--dry-run] [--reboot] [--oneshot [suffix]] [--no-wait]

# Tag Setting Backup Mode
# Setting Backup Instance Tag 
# Backup:on, Generation:3, Reboot:off (optional) 
ami_backups --profile prof1 --all 

# Single Instance Backup Mode
ami_backups --profile prof1 inst1 

# First time running recommended to add --dry-run option.
aws_stops --profile <prof> [--dry-run <target_time>]

# Setting Stop/Start Instance Tag 
# Stop: 22:00, Start: 08:00 
aws_stops --profile prof1 

# First time running recommended to add --dry-run option.
# In actual operation, use cron

車輪の再発見とか

まーあれですよ『枯れた技術の水平思考』とでも言うと思ったかい、その態度、想定の範囲内だよ!
ルネサンスが古代ギリシャ・ローマの文化の再発見であったかのように歴史は何度でも繰り返される。
であるなら、最新のコジマ技術を獲得するのと同程度には、過去の用兵術にも目を向けるべきであろう

参考文献

続きを読む

動画を探して自動ツイートしてくれるPython製botをAWSに載せてみた(前編)

TL;DR

  • YouTubeから動画を拾ってTweetするbotをPythonで開発し、AWS Lambdaに載せてみました
  • 全2記事です。前編のこちらでは、主にPythonでの開発周りのトピックにフォーカスします
    • TwitterAPIを使ってプログラムからツイートしてみます
    • YouTubeのページを構文解析し、文字列操作を使って動画URLを抽出してみます

動機

新しい職場にて初めてAWSを触ることになったので、これを機にと個人アカウントを取ってみました。チュートリアルだけというのももったいないので、何か自分のためのサービスを作って載せると面白そうです。

で、Twitterのbot開発にはもともと興味があったので、これも前から興味を持ちつつ触ってなかったPythonでbotを作り、lambdaを使って運用してみようと思い立ちました。AWS lambdaは2017年4月からPython3系を扱えるようになったので、心置き無く最新バージョンで書けそうだなー、というのも狙いです。

ユーザーストーリー

毎日の退勤をもう少し楽しみにするために、定時になると自分が興味ありそうなYouTube動画をbotが勝手に検索して、自分のTwitterアカウントに届けてくれるようにしたい。
スクリーンショット 2017-12-06 23.30.33.png

前提

  • 開発にはMacを使用します
  • Pythonは3.6系を使用します
  • pyenvもvirtualenvも使用しません。議論はあろうかと思いますが、個人開発なので。。
  • で、開発環境構築はこちらの記事等を参照しました
  • bot化したいTwitterアカウントはあらかじめ用意してあるものとします

TwitterAPIを使ってプログラムに呟かせる

アクセスキーの取得

bot化したいアカウントでTwitter Application Managementにログインすると、アプリケーションの作成とConsumer Key、及びAccess Tokenの取得ができます。

なお、Appの作成にはTwitterアカウントが電話番号認証済みである必要があります。認証済みでないと怒られるので、エラーメッセージ中のリンクからさらっと済ませておきましょう。

  • Consumer Key
  • Consumer Key Secret
  • Access Token
  • Access Token Secret

以上の4パラメータがあればプログラムからのツイートができます。コピーしてこんな感じのファイルを作っておきましょう。

config.py
CONSUMER_KEY        = "xxxxxxxxxxxxxxxxx"
CONSUMER_SECRET     = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ACCESS_TOKEN        = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
ACCESS_TOKEN_SECRET = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

複数の外部ユーザーからアクセスがあるようなアプリケーションの場合(=「このアプリケーションとの連携を許可しますか?」など出るやつ)はそれぞれの役割についてもう少し説明が必要ですが、今回はある程度一緒くたに考えてしまっても実装に支障ありません。

PythonでOAuth認証

ライブラリの導入と管理

Pythonのライブラリは、パッケージ管理ツールであるpipでインストールできます。仮想環境がない場合、オプション無しで勝手にglobalに入るのがうーん、という感じですがまあそれは置いておいて。

PythonでHttp通信を行うライブラリとしては、requestsがポピュラーなようです。また、今回はTwitterAPIを使うための認証が必要なので、OAuth認証を扱えるライブラリも必須です。ここはrequestsと同じところが公開しているrequests_oauthlibを使用しました。

pip3 install requests requests_oauthlib

さて、インストールはできましたが、今度は開発するプロジェクトがこれらのライブラリに依存していることを表明しておくのがマナーです。js界隈で言うところのpackage.jsonですね。

Pythonでは依存関係を記したrequirements.txtなどを作っておくケースが多いようです。

requirements.txt
requests==2.18.4
requests-oauthlib==0.8.0

ちなみに、pip3 freeze > requirements.txtでインストールされた依存関係をrequirements.txtに吐き出せます。

逆に.txtファイルを元に一括インストールする場合は、-rオプションを用いてpip3 install -r requirements.txtなどと書けます。結構便利です。

つぶやいてみる

first_tweet.py
from requests_oauthlib import OAuth1Session
import config, json

twAuth = OAuth1Session(
  config.CONSUMER_KEY,
  config.CONSUMER_SECRET,
  config.ACCESS_TOKEN,
  config.ACCESS_TOKEN_SECRET)
apiURL = "https://api.twitter.com/1.1/statuses/update.json"
params = { "status": "プログラムにツイートさせてみるテスト" }

res = twAuth.post(apiURL, params = params)
print(json.loads(res.text))

先ほど作ったconfig.pyimportして、これだけ。思ったよりだいぶ手軽です。Twitterにアクセスして実際にツイートされたことを確認しましょう!

また、せっかくなのでレスポンスをjsonライブラリでロードして吐き出してみます。

{'created_at': 'Wed Dec 06 14:00:00 +0000 2017', 'id': 9384076800000000, 'id_str': '9384076800000000', 'text': 'プログラム
にツイートさせてみるテスト', 'truncated': False, 

...(中略)...

'retweeted': False, 'lang': 'ja'}

思ったよりいろんな属性があることがわかりますね。深掘りは公式のリファレンスにて。

YouTubeから動画のURLを拾ってくる

続いて、YouTubeから動画を探してくるパートです。

Webクローリング

この分野では、「クローリング」や「スクレイピング」と言った言葉が有名です。

クローリングとスクレイピング

クローリングはウェブサイトからHTMLや任意の情報を取得する技術・行為で、 スクレイピングは取得したHTMLから任意の情報を抽出する技術・行為のことです。

たとえば、あるブログの特徴を分析したい場合を考えてみましょう。
この場合、作業の流れは

  1. そのブログサイトをクローリングする。
  2. クローリングしたHTMLからタイトルや記事の本文をスクレイピングする。
  3. スクレイピングしたタイトルや記事の本文をテキスト解析する。

というようになります。

今回は、YouTubeをクローリングし、その中から動画のURLをスクレイピングすることになりますね。

Webページのクローリングとスクレイピングを行う際は、それがどんな目的のものであれ、HTMLを構文解析することが必須となります。Pythonでは、これを強力に支援するBeautifulSoupと言うライブラリがあります。執筆時点で最新のbeautifulsoup4を導入してみます。

pip3 install beautifulsoup4

早速使ってみましょう。Qiitaのトップページから<a>タグを探し、その中に含まれるhref属性の値を取得してみます。

crawling.py
import requests
from bs4 import BeautifulSoup

URL = "https://qiita.com/"
resp = requests.get(URL)

soup = BeautifulSoup(resp.text)

# aタグの取得
a_tags = soup.find_all("a", href=True)
for a in a_tags:
    print(a["href"])

結果

/about
https://qiita.com/sessions/forgot_password
https://oauth.qiita.com/auth/github?callback_action=login_or_signup
https://oauth.qiita.com/auth/twitter?callback_action=login_or_signup

・・・(中略)

https://qiita.com/api/v2/docs
https://teams.qiita.com/
http://kobito.qiita.com

いい感じです!

HTMLパーサーについて

さて、先のコードを実際に試すと、HTMLパーサーが明示されていないために警告が出ます。これは実際の解析時に使われるパーサーが実行時の環境に依存するためです。異なる環境下で同じ振る舞いを期待するには、使用するHTMLパーサーを明示してあげる必要があります。

デフォルトではhtml.parserが使われますが、lxmlかhtml5libを導入してこちらを明示してあげるのが無難なようです。このあたりの情報は下記の記事をだいぶ参考にさせていただきました。パーサーの選択だけでなくスクレイピング全般の情報が非常によくまとまっているエントリなので、オススメです。

PythonでWebスクレイピングする時の知見をまとめておく – Stimulator

パーサの良し悪しを考えるとlxmlでチャレンジしてダメならhtml5libを試すのが良さそう。

今回はこの1文に愚直に従ってみます。事前にpip3 install lxml html5libも忘れずに。


import requests
from bs4 import BeautifulSoup

URL = "https://qiita.com/"
resp = requests.get(URL)

+try:
+  soup = BeautifulSoup(resp.text, "lxml")
+except:
+  soup = BeautifulSoup(resp.text, "html5lib")
-soup = BeautifulSoup(resp.text)

# ...以下は先ほどと同様

Crawlerクラスを作ってみる

すでにPythonでオブジェクト指向な書き方を経験している方はこの辺りを飛ばしていただいて構いません。せっかくHTMLを解析してくれるコードができたので、クラスとして書き換えてみます。

crawler.py
import requests
from bs4 import BeautifulSoup

class Crawler:
    def hrefs_from(self, URL):
        a_tags = self.soup_from(URL).find_all("a", href=True)
        return set(map(lambda a:a["href"], a_tags))

    def soup_from(self, URL):
        res_text = requests.get(URL).text
        try:
            return BeautifulSoup(res_text, "lxml")
        except:
            return BeautifulSoup(res_text, "html5lib")

個人的にはインスタンスメソッドの第1引数が常にselfでなければならないのは書く量が増えるので少しもどかしいですね。ハマりポイントにもなりかねない…。

ちなみに、ここではラムダ式を使用し、hrefs_fromメソッドの戻り値の型をsetにしてみました。これは、今回のユースケースを鑑みてリンク先URLの重複を排除した方が便利と判断したためです。出現頻度など解析したい場合はまた改めて設計を考える必要があるでしょう。

継承と、YouTubeへのアクセス

YouTubeをクローリングするにあたって、「検索文字列を与えたら検索結果のページをクローリングし、動画を探してくる」などの機能があると便利そうです。先ほどのクラスを継承して、実装してみます。

tube_crawler.py
import random
import re
from crawler import Crawler

class TubeCrawler(Crawler):

    URLBase = "https://www.youtube.com"

    def hrefs_from_query(self, key_phrase):
        """
        検索文字列を与えると検索結果ページに含まれるhref属性の値を全て返す
        """
        return super().hrefs_from(self.URLBase + 
            "/results?search_query=" + key_phrase.replace(" ", "+"))



    def movies_from_query(self, key_phrase, max_count = 10):
        """
        検索文字列を与えると検索結果ページに含まれる動画のビデオIDを返す
        """
        return self.__select_movies(self.hrefs_from_query(key_phrase), max_count)



    def __select_movies(self, hrefs, max_count):
        """
        privateメソッド。href属性の値のsetからビデオIDのみを返す
        """
        filtered = [ re.sub( "^.*/watch?v=", "", re.sub( "&(list|index)=.*$", "", href )) 
            for href in hrefs if "/watch?v=" in href ]
        return filtered[:min(max_count, len(filtered))]



    def choose(self, movie_ids, prefix = "https://youtu.be/"):
        """
        渡した文字列のリスト(ビデオIDのリストを想定)から1つを選び、prefixをつけて返す
        """
        return prefix + random.choice(movie_ids)

文法的には継承とprivateメソッドの書き方あたりが新しい話題となります。この記事の主題ではないので特段の説明は省きます。

実際に試すとわかるのですが、検索結果のページにノイズとなるリンクが多いばかりか、再生リストへのリンクなど紛らわしいものも多く、その辺を適切に弾いていくのに手こずりました。おかげでfilter関数や正規表現に少し強くなれた気がします。

正規表現についてはこちらの記事をだいぶ参考にしました。

Pythonの正規表現の基本的な使い方

繋げてみる

準備が整ったので検索->ツイートの流れを試してみます。

main.py
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from tube_crawler import TubeCrawler
from tweeter import Tweeter
import config

def main():
    t = TubeCrawler()
    movies = t.movies_from_query("Hybrid Rudiments")
    chosen = t.choose(movies)

    # ツイートする部分をクラス化したもの
    tw = Tweeter()
    tw.reply(config.REPLY_TO, chosen)

if __name__ == '__main__':
    main()

エントリーポイントとなる関数が必要かなー、と思ったので何気なく(そう、本当に何気なく。これで良いと思っていたんですLambdaを使うまでは…)main関数を作成。

直接./main.pyでも呼べるようにこの辺からShebangを記述し始めました。また、末尾はファイル名で直接実行した場合にmain()を呼ぶためのおまじない。Rubyにも似たやつがありますね。あとはターミナルから呼んで動作確認するだけです。

$ ./main.py

実行したところ問題なく動きそうだったので、次回はAWS Lambdaに載せていきます。それなりの尺となったのでこのページはここまでです。お読みいただきありがとうございました。

リンク

続きを読む

G Suiteを利用してGAMでユーザーごとの利用できるAWSアカウントとロールを管理する

NIFTY Advent Calendar 2017 11日目の記事になります。

AWSのアカウント管理や認証をどうしていけばいいのか試行錯誤してました。
タイトルから個人的な結論が出ていますが、考えた順に書いていきます。

Microsoft ADで管理してMasterアカウントにログイン後、SubアカウントにSwitch Roleする

AWS_DirectoryService.png

IDaaSやADを自前で持っていない場合は、すべてがAWSで完結するからこれが綺麗だと思う。

AWS公式の提案手法

マルチアカウントにする意義と、そのためのアカウント間の構成を教えてくれるので、読んだことがない方は一度こちらを読んでおくことをオススメします。

上記の構成を実現するためのCloud Formationのサンプルなども提供されている。

OpenAMをIdpとしてAWSにSAML認証でログインする

OpenAM.png

すでにLDAPを持っていて、できるだけ内部で管理したい場合の構成。

OpenAMのグループに対して、利用できるアカウントとロールを付けていく管理がいいのだろうか。
OpenAMがまるで詳しくないので、次いきます。

G SuiteをIdpとしてAWSにSAML認証でログインする

IDaaSとしてはOneloginなど他にもありますが、G Suiteが試しやすかったので、こちらを採用。
G SuiteからSAML認証でAWSにログインするまでの手順は、こちらにまとまっているので、ここでは説明を割愛します。

MasterアカウントのIdpとして登録

G_Suite.png

OpenAMの代わりにIDaaSとしてG Suiteを利用した構成。

G SuiteのBusinessプラン以上でないと監査ログが取れないので、できればBussinessプランにしたい。Basicプランでも構成自体は実現できる。

SubアカウントのIdpとして登録

G_Suite2.png

G Suiteはひとつのアプリから、下記のどの構成もいけるのでMasterアカウントを経由する方法を取る必要はなさそう。

  • Single APP -> Single Account Single Role
  • Single APP -> Single Account Multi Role
  • Single APP -> Multi Account Multi Role

G SuiteユーザーのAWS Console Roleのrole属性に roleのarn,Idpのarn の形で記載する。roleは複数値入れられるように設定されているので、別のSubアカウントの権限も与えたい場合は、これを増やしていけばいい。

管理コンソール.png

G Suiteのアプリを選択すると、このようにSwith Roleの選択画面に飛ぶ。

Amazon Web Services Sign In.png

アカウントがIDなのはどうしようもなさそうだが、Role名を工夫すればどのサービスのアカウントか判別できそう。
Role名を統一したい場合は、Chromeの拡張機能とか作ってAWSアカウントIDと名前を置換するとか。
あとで困りそうだけどサービスごとにG Suiteのアプリを分けてしまう手もある。

各Subアカウントに対してIdpを設定する必要があるが、Cloud Formationでかなりの部分は吸収できるし、そもそもアカウントをそんなにぽんぽん増やすシーンも思いつかないので、その管理コストよりも利用者の日々の手間をワンステップ減らしたほうが利はあると思う。

GAMでG SuiteのユーザーにAWSの権限を与える

人が増えたり減ったり入れ替わりが起きるごとに、G SuiteのAWS Console Roleを変更するのは辛いので自動化を目指します。
GAMを使えばG Suite APIをCLIで簡単に操作できるので、これを使います。

インストールから基本的な使い方は、以下に詳しく書いてあるので割愛します。

今回修正がしたいのはCustom User Schema Fieldなのでマニュアルはこれ。

試しにさっきのユーザーを 54321 をなくして、 33333 をいうAWSアカウントIDに権限を付けてみます。
注意点としては追加削除という概念はなく、指定したものを上書きする形で指定します。

# gam update user username@example.com \
AWS_Console_Role.role multivalued arn:aws:iam::12345:role/CrossAccountManager-Administrator,arn:aws:iam::12345:saml-provider/G-Suite \
AWS_Console_Role.role multivalued arn:aws:iam::33333:role/CrossAccountManager-Developer,arn:aws:iam::33333:saml-provider/G-Suit
updating user username@example.com...

管理コンソール2.png

ちゃんと更新できてますね。

自動化について

ユーザーごとに管理するのは大変なので、グループごとにアカウントとロールを管理して、そのマスターが更新されるかグループのメンバーが更新されたら、functionが起動してグループ内ユーザーのroleを更新してくれる的なものまでいければ完璧ですが、まだ試していないので今回はここまで。

続きを読む