AWSの部屋

AWS学習者向けのブログです

SAML による AWS へのシングルサインオン

はじめに

AWS アカウントに IAM ユーザーを作成しログインする代わりに、ID プロバイダー(IdP)を使⽤しシングルサインオンすることができます。これは、組織に独⾃のID 基盤がある場合や、複数の AWS アカウントを使⽤している場合に便利です。今回は AWS 認定ソリューションアーキテクト - プロフェッショナル試験に向けて、SAML を使った AWS へのシングルサインオンに挑戦してみようと思います。

シングルサインオンとは

1度のユーザー認証によって複数のシステム(業務アプリケーションやクラウドサービスなど)の利用が可能になる仕組みを指します。

シングルサインオンに必要な要素

要素 説明 本エントリーで活用するサービス
アイデンティティストア IDプロバイダー(IdP)のユーザ管理 GMOトラスト・ログイン
IDプロバイダー(IdP) ユーザー・アカウントを管理するサービス GMOトラスト・ログイン
サービスプロバイダー なんらかのサービスを提供する企業または組織 AWS

AWSにおけるシングルサインオンのイメージ

手順

  1. IDプロバイダーの設定1
  2. サービスプロバイダーの設定
  3. IAM ロールの作成
  4. IDプロバイダーの設定2
  5. アイデンティティストアのユーザの作成
  6. 動作確認

1. IDプロバイダーの設定1

アカウントの登録後、ID プロバイダー情報のメタデータをダウンロードします。アプリの登録はせずにこのまま次の手順に進みます。

管理者画面に遷移します。

アプリの追加ボタンを押します。

AWS IAM(SAML) をクリックします。

ID プロバイダー情報のメタデータをダウンロードします。

2. サービスプロバイダーの設定

IAM管理画面で「プロバイダを追加」をクリックします。

前工程でダウンロードしたメタデータを選択し、プロバイダーの追加をします。

プロバイダのARNを控えておきます。

3. IAM ロールの作成

先ほど作成したプロバイダを選択し、以下のようにロールを作成します(アタッチするポリシーやロール名は適当でOK)。

ロールの ARN を控えておきます。

4. IDプロバイダーの設定2

「サービスプロバイダーの設定」の「SAML属性の設定」に控えておいた「ロールの ARN」と「プロバイダの ARN」を「,」で区切った「ロールのARN,プロバイダのARN」の形式で入力し、アプリの登録を行います。

例:arn:aws:iam::000000000000:role/saml-role, arn:aws:iam::000000000000:saml-provider/sanvarie_test

5. アイデンティティストアのユーザの作成

このようにユーザを二つ作成しました。

6. 動作確認

ログインができている場合、画面の上部にロール名/ユーザー名@アカウントIDが表記されます。

一人目のユーザでログイン

二人目のユーザでログイン

さいごに

何となくでしかシングルサインオンの仕組みを理解していなかったのですが、実現に必要なものを整理し実際に手を動かすことによって理解が深まった気がします。次回は OpenID Connect を使ったシングルサインオンにも挑戦してみようと思います。

CloudFormation でインフラ環境を自動構築する

はじめに

マネジメントコンソールで手作業でインフラ環境を構築していくのは、最初は直感的でわかりやすいかと思います。ただ、同じ環境を複数用意する、複数の環境に同じ修正を横展開するといった作業を人力でやり続けることは効率が悪く、設定ミスも発生しやすくなります。今回はそのような時に役立つ CloudFormation を使ってみたいと思います。

使用するサービス

  • AWS CloudFormation・・・AWS リソースを自動構築するためのサービス

CloudFormation の利用の流れ

  1. CloudFormation テンプレートを作成する
  2. テンプレートを適用する
  3. CloudFormation スタックが作成され、それに紐づく形で AWS リソースが自動構築される

スタックとは

CloudFormation で構築された AWS リソースはスタックという集合にまとめられます。テンプレートを修正し、スタックを指定して再度適用することで、スタック上の AWS リソースの設定を変更したり、リソースを削除することができます。

テンプレートとは

スタックの設計図です。JSONYAML 形式で記述します。テンプレートに関する詳細はこちらをご参照ください。

今回のゴール

CloudFormation のテンプレート(yml)を作成し、それを使用して以下のようなインフラ環境を構築する。

作成する AWS リソース

  • VPC
  • サブネット(パブリック×1、プライベート×1)
  • ルートテーブル(パブリック×1、プライベート×1)
  • インターネットゲートウェイ
  • セキュリティグループ(パブリック×1、プライベート×1)
  • EC2(パブリック×1、プライベート×1)

CloudFormation テンプレートの解説

CloudFormation テンプレートはいくつかのセクションに分かれています。本エントリーではよく使われる以下3つのセクションについて説明します。

Resources セクション

構築する AWS リソースの設計を記述するセクション。例えば、VPC の設定は以下のように行います。このテンプレートでは「fjVpc」が VPC リソースを表す論理 ID で、この ID を使ってリソース間の紐づけを行います。

# Resources Section
Resources:
  # VPCの設定
  fjVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: fjVpc
Parameters セクション

実行時に値を選択(入力)するセクション。例えば、以下のようにインスタンスタイプを変数として定義し、実行時に選択する形式にすることができます。

# Parameters Section
Parameters:
  InstanceType:
    Type: String
    Default: t2.micro
    AllowedValues:
      - t2.micro
      - t2.small
      - t2.medium
    Desctiption: Select EC2 instance type.
  KeyPair:
    Description: Select KeyPair Name.
    Type: AWS::EC2::KeyPair::KeyName
Mappings セクション

変数を Map 形式に定義できるセクション(実行環境によって変わる値を定義するのに用いられることが多い)。例えば、AMI ID はリージョンによって変わるため、以下のように AMI ID を定義します。

# Mappings Section
Mappings:
  RegionMap:
    us-east-1:
      hvm: 'ami-a4c7edb2'
    ap-northeast-1:
      hvm: 'ami-3bd3c45c'

CloudFormation テンプレートの作成

以下のようなテンプレートを作成しました。

AWSTemplateFormatVersion: "2010-09-09"
Description: Network and server resource template

# Parameters Section
Parameters:
  InstanceType:
    Type: String
    Default: t2.micro
    AllowedValues:
      - t2.micro
      - t2.small
      - t2.medium
    Description: Select EC2 instance type.
  KeyPair:
    Description: Select KeyPair Name.
    Type: AWS::EC2::KeyPair::KeyName
    
# Mappings Section
Mappings:
  RegionMap:
    us-east-1:
      hvm: 'ami-a4c7edb2'
    ap-northeast-1:
      hvm: 'ami-3bd3c45c'
      
# Resources Section
Resources:
  ############### VPC ###############
  fjVpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: fjVpc
  
  ############### Subnet, RouteTable, IGW ###############
  
  ## パブリックサブネット
  fjSubnetPublic:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.0.0/24
      VpcId:
        Ref: fjVpc
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: fj-subnet-public
        - Key: Type
          Value: Public
          
  ## プライベートサブネット
  fjSubnetPrivate:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: 10.0.1.0/24
      VpcId:
        Ref: fjVpc
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: fj-subnet-private
        - Key: Type
          Value: Isolated
  
  ## パブリックサブネット用のルートテーブル
  fjRoutePublic:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: fjVpc
      Tags:
        - Key: Name
          Value: fj-route-public
          
  ## パブリックサブネットへルート紐付け
  fjRoutePublicAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: fjRoutePublic
      SubnetId:
        Ref: fjSubnetPublic

  ## パブリックサブネット用ルートテーブルのデフォルトルート
  fjRoutePublicDefault:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: fjRoutePublic
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId:
        Ref: fjIgw
    DependsOn:
      - fjVpcgwAttachment

  ## プライベートサブネット用のルートテーブル
  fjRoutePrivate:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: fjVpc
      Tags:
        - Key: Name
          Value: fj-route-private
          
  ## プライベートサブネットへルート紐付け
  fjRoutePrivateAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: fjRoutePrivate
      SubnetId:
        Ref: fjSubnetPrivate
  
  # インターネットへ通信するためのゲートウェイの作成
  fjIgw:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: fj-igw
  fjVpcgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: fjVpc
      InternetGatewayId:
        Ref: fjIgw
  
  ############### Security groups ###############
  ## インターネット公開のセキュリティグループの生成
  fjSgPublic:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for public
      GroupName: public
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          Description: from 0.0.0.0/0:80
          FromPort: 80
          IpProtocol: tcp
          ToPort: 80
        - CidrIp: 0.0.0.0/0
          Description: from 0.0.0.0/0:22
          FromPort: 22
          IpProtocol: tcp
          ToPort: 22
      Tags:
        - Key: Name
          Value: fj-sg-public
      VpcId:
        Ref: fjVpc
  
  ## プライベートサブネットインスタンス用のセキュリティグループの生成
  fjSgPrivate:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security Group of private
      GroupName: private
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      SecurityGroupIngress:
        - CidrIp: 0.0.0.0/0
          Description: from 0.0.0.0/0:22
          FromPort: 22
          IpProtocol: tcp
          ToPort: 22
        - CidrIp: 0.0.0.0/0
          Description: from 0.0.0.0/0:22
          FromPort: -1
          IpProtocol: icmp
          ToPort: -1
      Tags:
        - Key: Name
          Value: fj-sg-private
      VpcId:
        Ref: fjVpc
        
  ############### Server ###############
  
  ## EC2(パブリックサブネット)
  fjEC2Public:
    Type: AWS::EC2::Instance
    Properties: 
      ## Mappingsセクションの値をFindMap関数で取得
      ImageId: !FindInMap [ RegionMap, !Ref 'AWS::Region', hvm]
      ## Parametersセクションの値をRef関数で取得
      InstanceType: !Ref InstanceType
      SubnetId: !Ref fjSubnetPublic
      Tags:
        - Key: Name
          Value: fj-ec2-public
      SecurityGroupIds:
        - !Ref fjSgPublic
      KeyName: !Ref KeyPair
  
  ## EC2(プライベートサブネット)
  fjEC2Private:
    Type: AWS::EC2::Instance
    Properties: 
      ## Mappingsセクションの値をFindMap関数で取得
      ImageId: !FindInMap [ RegionMap, !Ref 'AWS::Region', hvm]
      ## Parametersセクションの値をRef関数で取得
      InstanceType: !Ref InstanceType
      SubnetId: !Ref fjSubnetPrivate
      Tags:
        - Key: Name
          Value: fj-ec2-private
      SecurityGroupIds:
        - !Ref fjSgPrivate
      KeyName: !Ref KeyPair

CloudFormation スタックの作成

作成した CloudFormation テンプレートを使用して CloudFormation スタックを作成します。

Parameters セクションで設定した項目に対して値を入力します。この後はすべてデフォルト設定でOKです。

スタックが作成されました。

AWS リソースの確認

以下のように AWS リソースが作成されていることが確認できました。

VPC

サブネット

ルートテーブル

インターネットゲートウェイ

セキュリティグループ

EC2

動作確認

EC2 の動作確認を行います。

パブリックサブネット用の EC2

SSH で EC2 にログインします。

EC2 へのログインを確認しました。

プライベートサブネット用の EC2

パブリックサブネット用の EC2 から SSH で プライベートサブネット用の EC2 へのログインをすることができました。

また、ping も通ることがわかりました。

さいごに

CloudFormation テンプレートを実際に作成したのは今回が初めてでした。ただ、AWS のネットワークやサーバー構築に関して基本的な知識があればすぐに理解できると感じました(AWS に関する基本的な理解がない場合はかなり厳しそう)。今回はシンプルな構成のインフラ環境でしたが、次は少し複雑な環境構築にも挑戦してみようと思います。

CloudWatch のログを Kinesis Data Firehose 経由で S3 に保存する

はじめに

CloudWatch でログを収集することができますが、収集したログを S3 に自動転送したいことがあるかと思います。そんな時は Kinesis Data Firehose が役に立つということで、実際に自分で手を動かして試してみました。

使用するサービス

  • Amazon CloudWatch・・・AWS リソースと AWS で実行するアプリケーションのモニタリングサービス
  • Amazon Simple Storage Service (Amazon S3)・・・AWSが提供するオブジェクトストレージサービス
  • Amazon Kinesis Data Firehose・・・ユーザからの入力やアクセスログなどで生成されたデータソースを、指定したAWSのサービスにストリーミングデータとして転送するサービス

アーキテクチャ

手順

  1. S3
    1. バケットを作成
  2. Kinesis
    1. IAMロールを作成
    2. 配信ストリームを作成
  3. CloudWatch
    1. IAMロールを作成
    2. ロググループ作成
    3. ログストリーム作成
    4. サブスクリプションフィルタ設定
  4. 動作確認
    1. AWS CLI で CloudWatch にログを出力
    2. S3 のバケットを確認

1. S3

1. バケットを作成

以下のようにバケットを作成します。設定はすべてデフォルトでOKです。

2. Kinesis

1. IAMロールを作成

Kinesis Data Firehose に S3バケットへデータを出力する権限を与えます。今回は以下のようなロールを作成しました。

2. 配信ストリームを作成

以下のように値を設定して配信ストリームを作成します。

項目
Source Direct PUT
Destination Amazon S3
Delivery stream name 配信ストリーム名
S3 bucket s3://<作成したバケット名>

このように配信ストリームが作成されました。

3. CloudWatch

1. IAMロールを作成

CloudWatch に Kinesis Data Firehose へデータを出力する権限を与えます。今回は以下のようなロールを作成しました。(CloudWatchのロールに関しては作り方が少しトリッキーなため、こちらをご参照ください)

2. ロググループを作成

以下のようにロググループを作成します。

3. ログストリームを作成

作成したロググループにログストリームを作成します。

以下のようにログストリームを作成しました。

4. サブスクリプションフィルタを作成

作成したロググループにサブスクリプションフィルタを作成します。

以下のように値を設定します。

項目
Destination account 現在のアカウント
Kinesis Firehose delivery stream 作成した配信ストリーム名
既存のロールを選択 CloudWatch用に作成したIAMロール
ログの形式 その他
サブスクリプションフィルター名 サブスクリプションフィルターの名前
テストするログデータを選択 作成したログストリーム名

4. 動作確認

1. AWS CLI で CloudWatch にログを出力

AWS CLI で 以下のようにしてログを出力します。

aws logs put-log-events --log-group-name log-to-kinesis --log-stream-name log-stream-to-kinesis --log-events timestamp=1653457849182,message="test"
2. S3 のバケットを確認

作成したバケットに以下のようなデータが格納されていることを確認します。

データは圧縮されているので、ダウンロードして解凍するとログが格納されていることが確認できます。

{"messageType":"CONTROL_MESSAGE","owner":"CloudwatchLogs","logGroup":"","logStream":"","subscriptionFilters":[],"logEvents":[{"id":"","timestamp":1653552879998,"message":"CWL CONTROL MESSAGE: Checking health of destination Firehose."}]}{"messageType":"DATA_MESSAGE","owner":"370560102364","logGroup":"log-to-kinesis","logStream":"log-stream-to-kinesis","subscriptionFilters":["log-to-kinesis-filter"],"logEvents":[{"id":"36873342191118257728364867749675187185934849327962587136","timestamp":1653457849182,"message":"test"}]}

さいごに

Kinesis Data Firehose は扱うのが難しいイメージでしたが、設定自体は非常に簡単でしたね。今回試したことはログ収集の初歩的なことだと思うので、次はもう一歩踏み込んだ形のログ収集にチャレンジしてみようと思います。

Cognito を使ったユーザ認証で S3 にアクセスしてみる

はじめに

アプリケーションの開発時、認証部分の開発は必須かと思います。しかし、認証まわりの開発は地味ですが意外と手間と時間がかかります。Cognito は認証基盤を短期間でアプリケーションに実装することができるサービスで認証部分の開発工数削減が見込めます。

今回のゴール

Cognito でユーザ認証を行い S3 にアクセスしてみようと思います。

使用するサービス

サービス 説明
Amazon Cognito Web アプリケーションやモバイルアプリケーションに対する安全な認証を提供するサービス
Amazon Simple Storage Service (Amazon S3) データを格納・管理できるオブジェクトストレージサービス

Amazon Cognito の主な 2 つのコンポーネント

Amazon Cognito にはユーザープールと ID プールというコンポーネントがあります。

ユーザープール

認証基盤を開発しなくてもウェブおよびモバイルアプリケーションからのサインアップ、サインインに使用することができます。ユーザーの ID やパスワードの認証情報をアプリ内部の「ユーザーディレクトリ」という領域に保存し、その情報を利用してアプリの「認証」を行います(ユーザープールはアクセスしてきたユーザーが誰であるかを確認します)。また、Cognito ユーザープールのみで認証することもできますが、SNS など外部の認証も使用することができます。

ID プール

AWS の他のサービスへのアクセス権をユーザーに付与する AWS 認証情報を提供します。例えば、Amazon S3バケットAmazon DynamoDB テーブルなどへのアクセスをユーザーに許可します。また、ID プールには認証プロバイダーを設定できます。認証プロバイダーには、SNS などの Web ID フェデレーションや Cognito ユーザープールが設定できます。

ユーザープールと ID プールの違い

「ユーザープール」は「認証(アクセスしてきたユーザーが誰か)」を処理しますが、「ID プール」は「認可(そのユーザーが利用できるサービスであるか)」を処理するという違いがあります。
また、「ユーザープール」の認証処理の対象は「アプリ」ですが、「ID プール」は「AWS のサービス」が対象となってるところも違いの一つです。

ユーザープールと ID プールをどのように活用すべきか

ユーザープールと ID プールの活用のポイントは、両方ともセットで使うことです(「認証(誰が)」と「認可(何のサービスを)」を組み合わせて利用することではじめて認証サービスとして機能するため)。

アーキテクチャ

S3 に対する読み取りだけ許可をするように Cognito で制御します。

実行環境

macOS 12.3.1
Python 3.9.6
Boto3 1.20.53

手順

  1. ユーザープールの作成
  2. ID プールの作成
  3. ユーザ登録
  4. S3 にアクセス

1. ユーザープールの作成

以下の設定でユーザープールを作成します。

項目
プロバイダーのタイプ Cognito ユーザープール
Cognito ユーザープールのサインインオプション E メール
パスワードポリシーモード Cognito のデフォルト
多要素認証 MFAなし
ユーザーアカウントの復旧 デフォルト
属性検証とユーザーアカウントの確認 デフォルト
属性変更の確認 デフォルト
必須の属性 email
属性変更の確認 デフォルト
E メール Cognito で E メールを送信
ユーザープール名 任意のユーザープール名
ホストされた認証ページ デフォルト
アプリケーションタイプ 秘密クライアント
ホストされた認証ページ デフォルト
アプリケーションクライアント名 任意のアプリケーションクライアント名
クライアントのシークレット クライアントのシークレットを生成しない
高度なアプリケーションクライアントの設定 デフォルト
属性の読み取りおよび書き込み許可 デフォルト

2. ID プールの作成

以下の設定で ID プールを作成します。

項目
ID プール名 任意のID プール名
認証されていない ID デフォルト
認証フローの設定 デフォルト
認証プロバイダー Cognito
ユーザープール ID 作成したユーザープールのユーザープール ID
アプリクライアント ID 作成したユーザープールのクライアント ID

ID プールの作成後にロールの作成を求められます。

ポリシーを以下のように設定します。

認証ユーザに付与するロール
"Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*",
        "cognito-identity:*",
        "s3:Get*",
         "s3:List*",
         "s3-object-lambda:Get*",
         "s3-object-lambda:List*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}
非認証ユーザに付与するロール
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "mobileanalytics:PutEvents",
        "cognito-sync:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]

3. ユーザ登録

ユーザープール内にユーザが未登録なため、ユーザの登録をします。

本来は認証画面を作りたいところなのですが、時間的に難しいので以下のコードでユーザ登録を行います。

import boto3

idp_client = boto3.client('cognito-idp')

aws_result = idp_client.admin_create_user(
    # ユーザープールID
    UserPoolId='xxxxx',
    Username='xxx@gmail.com', # なぜかUsernameにメールアドレスを求められる・・・
    UserAttributes=[
        {
            'Name': 'email',
            'Value': 'xxx@gmail.com'
        },
        {
            'Name': 'email_verified',
            'Value': 'true'
        }
    ],
    TemporaryPassword = 'xxx'
)

ユーザが作成されました。コードの詳細はこちらこちらでご確認ください。

4. S3 にアクセス

S3 にある以下のバケットのオブジェクトを取得します。

import boto3

# CognitoIdentityProviderクライアント(Using Amazon Cognito user pools API)
idp_client = boto3.client('cognito-idp')

# CognitoIdentityクライアント(Using Amazon Cognito Federated Identities)
identity_client = boto3.client('cognito-identity')

# 設定情報
account_id = 'xxx'
region = 'ap-northeast-1'
user_pool_id = 'xxx'
identity_pool_id = 'xxx'
client_id = 'xxx'
auth_flow = "ADMIN_NO_SRP_AUTH"
user_name = 'xxx'
password = 'xxx'
logins_key = f"cognito-idp.{region}.amazonaws.com/{user_pool_id}"

def admin_initiate_auth():
    response = idp_client.admin_initiate_auth(
                UserPoolId = user_pool_id,
                ClientId = client_id,
                AuthFlow = auth_flow,
                AuthParameters={
                    "USERNAME": user_name,
                    "PASSWORD": password,
                }
            )
    return response

def get_id(id_token):
    response = identity_client.get_id(
        AccountId = account_id,
        IdentityPoolId = identity_pool_id,
        Logins={
            logins_key: id_token
        }
    )
    return response

def get_credentials_for_identity(identity_id, id_token):
    response = identity_client.get_credentials_for_identity(
        IdentityId = identity_id,
        Logins = {
            logins_key: id_token
        }
    )
    return response

# ログイン
auth_response = admin_initiate_auth()

# IDトークンを取得
id_token = auth_response['AuthenticationResult']['IdToken']

# アイデンティティIDを取得
id_response = get_id(id_token)
identity_id = id_response['IdentityId']

# S3にアクセスするクレデンシャルを取得
credentials_response = get_credentials_for_identity(identity_id, id_token)
Credentials = credentials_response['Credentials']
access_key_id = Credentials['AccessKeyId']
secret_access_key = Credentials['SecretKey']
session_token = Credentials['SessionToken']

# S3にアクセス
s3_client = boto3.client('s3',
            region_name="ap-northeast-1",
            aws_access_key_id=access_key_id,
            aws_secret_access_key=secret_access_key,
            aws_session_token=session_token
        )

# バケットにあるアイテムを検索
object_list = s3_client.list_objects_v2(Bucket='cf-templates-lxrja6wfp65n-ap-northeast-1')
for obj in object_list['Contents']:
    print(obj['Key'])

以下のようにバケット内のアイテムを取得できました。

20221507Y9-network_and_server.yml
2022150G7w-network_and_server.yml
2022150HcQ-network_and_server.yml
2022150KyE-network_and_server.yml
2022150clO-network_and_server.yml
2022150mbX-network_and_server.yml
2022150nGG-network_and_server.yml
2022150yTO-network_and_server.yml

バケットにオブジェクトを PUT すると Access Denied されます(想定通りの動き)

s3_client.put_object(Body = 'IMG_0687.jpeg', Bucket='cf-templates-lxrja6wfp65n-ap-northeast-1', Key = 'test.jpeg')

さいごに

少し長くなってしまいましたが、Cognito を使って S3 にアクセスすることができました。ユーザープールや ID プールなどややっこしくて理解するのに時間がかかりましたが、いい勉強になりました。次は認証画面も作ってみようと思います。

KMS を使ってデータの暗号化と復号化をしてみる

はじめに

機密性の高いデータを運用するには、暗号化の施策が必要になります。その際に重要になるのが、暗号化や復号化のための鍵の管理です。今回は KMS を使ってデータの暗号化と復号化をしてみようと思います。

KMS とは

AWS Key Management Service のことで CMK(カスタマーマスターキー)を管理して、データキーを生成・暗号化・復号するなど、暗号化に必要なキー管理、キーオペレーションを提供するマネージドサービスです。

対称暗号化とは

KMS は対称暗号化と非対称暗号化をサポートしており、今回は対称暗号化について書きます。対称暗号化では一つのデータキーを使った暗号化と復号を行います。

KMS の機能

機能 用途
Encrypt データの暗号化
Decrypt データの復号化
GenerateDataKey ユーザがデータを暗号化するための CMK を生成

CMK(カスタマーマスターキー)と CDK(カスタマーデータキー)

CMK は CDK を暗号化するための鍵で、CDK はデータを暗号化するための鍵です。KMS では CDK でデータを暗号化してから、CDK を CMK で暗号化する手法をとっています。これは CDK の保護のためで、この手法はエンベロープ暗号化と言われています。

KMS を使った暗号化

以下の順番でデータの暗号化を行います。
1. CMK を使って暗号化されている CDK と暗号化されていない CDK を作成する
2. 暗号化されていない CDK を使ってデータを暗号化する
3. 暗号化されていない CDK を削除する

KMS を使った復号化

以下の順番でデータの復号化を行います。
1. CMK を使って暗号化されている CDK を復号化する
2. 復号化された CDK を使って暗号化されているデータを復号化する

手順

  1. CMK の作成
  2. 暗号化
    1. CMK を使って暗号化されている CDK と暗号化されていない CDK を作成する
    2. 暗号化されていない CDK を使ってデータを暗号化する
    3. 暗号化されていない CDK を削除する
  3. 復号化
    1. CMK を使って暗号化されている CDK を復号化する
    2. 復号化された CDK を使って暗号化されているデータを復号化する

1. CMK の作成

キータイプを対称とします。

エイリアスを設定します。

キー管理者を設定します。

CMK が作成されました。

暗号化

1. CMK を使って暗号化されている CDK と暗号化されていない CDK を作成する

以下のように CMK に紐づく CDK を作成します。

aws kms generate-data-key --key-id arn:aws:kms:ap-northeast-1:xxxxxxxxxxxx --key-spec AES_256 

このような結果が得られます。

{
    "CiphertextBlob": "AQIDAH...",
    "Plaintext": "f6xFX4p...",
    "KeyId": "arn:aws:kms:ap-northeast-1:xxxxxxxxxxxxxxx:key/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
  • Plaintext・・・暗号化されていない CDK
  • CiphertextBlob・・・暗号化されている CDK

作成した CDK を保存します。

echo [Plaintext] > plain-data-key
echo [CiphertextBlob] | base64 --decode > encrypted-data-key
2. 暗号化されていない CDK を使ってデータを暗号化する

以下のデータを暗号化してみます。

[ec2-user@ip-10-0-1-245 ~]$ cat test-document
Welcome to Kamata!

暗号化されていない CDK を用いてデータを暗号化する。

openssl aes-256-cbc -e -in test-document -out encrypted-document -pass file:plain-data-key

このようにデータが暗号化されたことがわかります。

[ec2-user@ip-10-0-1-245 ~]$ cat encrypted-document
Salted__ク^・_~%?ステ黑゚鮱゚~ネ_マW-ヨ・@!蘋
3. 暗号化されていない CDK を削除する

暗号化されていない CDK (plain-data-key)を削除してください。

rm plain-data-key

3. 復号化

1. CMK を使って暗号化されている CDK を復号化する

暗号化されている CDK を復号化します。

aws kms decrypt --ciphertext-blob fileb://encrypted-data-key --output text --query Plaintext > decrypted-datakey
2. 復号化された CDK を使って暗号化されているデータを復号化する

暗号化されているデータを復号化します。

openssl aes-256-cbc -d -in encrypted-document -out decrypted-document -pass file:decrypted-data-key

このようにデータが復号化されたことがわかります。

[ec2-user@ip-10-0-1-245 ~]$ cat decrypted-document
Welcome to Kamata!

さいごに

KMS は AWS 認定試験において必ず出題されるサービスの一つで何となく理解したつもりでいたのですが、自分の中でモヤモヤする部分もありました。今回、実際に KMS を使うことによってサービスに対するイメージがより具体的になりました。次回は KMS とその他のサービスを連携させてみようと思います。

CloudWatch のログで特定の文字列を検出した際に Lambda と SNS でメール送信を行う

はじめに

システム運用時にエラーはつきものかと思いますが、そのエラーをいかに早く検知し対処を行うかが安定したシステム運用につながります。本日は CloudWatch に蓄積されたログから特定の文字列(エラーログに出力されている内容等)を自動で検出し、それをメールで通知するということをやってみます。

使用するサービス

  • Amazon CloudWatch・・・AWS リソースと AWS で実行するアプリケーションのモニタリングサービス
  • AWS Lambda・・・サーバーをプロビジョニングしたり管理しなくてもコードを実行できるコンピューティングサービス
  • Amazon Simple Notification Service (Amazon SNS)・・・モバイルプッシュ通知やSMS送信など様々なメディアに対応した分散型フルマネージド通知サービス

アーキテクチャ

本エントリーではログ内の「error」という文字列を検出しようと思います。

手順

  1. メール通知用 SNS トピックの作成
  2. Lambda 関数に割り当てるロールを作成
  3. SNS でメール送信を行う Lambda 関数の作成
  4. CloudWatch でサブスクリプションフィルタを作成
  5. 動作確認

1. メール通知用 SNS トピックの作成

こちらのエントリーを参照してください。

aws-room.hatenablog.com

2. Lambda 関数に付与するロールを作成

Lambda に対して以下のポリシーを割り当てたロールを作成します。

・CloudWatchLogsFullAccess
・AmazonSNSFullAccess

このようにロールを作成しました。

3. SNS でメール送信を行う Lambda 関数の作成

このような関数を作成します。(前工程で作成したロールを割り当てます)

以下のコードをデプロイします。

import base64
import json
import zlib
import datetime
import os
import boto3
from botocore.exceptions import ClientError

print('Loading function')


def lambda_handler(event, context):
    data = zlib.decompress(base64.b64decode(event['awslogs']['data']), 16+zlib.MAX_WBITS)
    data_json = json.loads(data)
    log_entire_json = json.loads(json.dumps(data_json["logEvents"], ensure_ascii=False))
    log_entire_len = len(log_entire_json)

    print(log_entire_json)

    for i in range(log_entire_len): 
        log_json = json.loads(json.dumps(data_json["logEvents"][i], ensure_ascii=False))

        try:
            sns = boto3.client('sns')
    
            #SNS Publish
            publishResponse = sns.publish(
                TopicArn = os.environ['SNS_TOPIC_ARN'],
                Message = log_json['message'],
                Subject = os.environ['NOTIFICATION_SUBJECT']
            )
    
        except Exception as e:
            print(e)

デプロイが完了したら環境変数を設定します。

キー
SNS_TOPIC_ARN SNSのトピックの ARN
NOTIFICATION_SUBJECT SNS で通知される際の件名

4. CloudWatch でサブスクリプションフィルタを作成

まず CloudWatch でこのようにロググループを作成します。

ログストリームも作成します。

Lambda サブスクリプションフィルターを作成します。

項目
Lambda 関数 前工程で作成したLambda 関数
ログの形式 JSON
ログの形式 検出する文字列・パターン

5. 動作確認

AWS CLI で CloudWatch にログを出力することができるので、以下のようにしてログを出力しました。

aws logs put-log-events --log-group-name Error --log-stream-name Error-log-stream --log-events timestamp=1653457849182,message="System error"

ログ出力後、このように SNS からメールが送信されることが確認できました。

さいごに

同じことを自分で実装しようと思ったら非常に大変な作業になると思いますが、AWS のようなクラウドのサービスを活用するとメールでのアラート機能を簡単に作ることができました。CloudWatch を初めてしっかりと使ってみたのですが、やはり自分で手を動かすことでサービスに対する理解が深まりますね。これからもどんどん色々なサービスを使っていこうと思います。

EC2 を使ってプロキシサーバを構築してみる

はじめに

本日は EC2 をプロキシサーバとして構築し、プライベートサブネット内のサーバからパブリックサブネットのプロキシサーバを経由してインターネットに接続してみようと思います。

使用するサービス

  • Amazon EC2・・・AWS が提供するコンピューティングプラットフォーム

アーキテクチャ

パブリックサブネットにプロキシサーバ(Squid をインストール)、プライベートサブネットに DBサーバを置く構成にします。

Squid とは

プロキシサーバ、ウェブキャッシュサーバなどに利用されているオープンソースソフトウェア。本日はプロキシサーバとして使用します。

手順

  1. ネットワーク・サーバ構築
  2. Squid のインストール
  3. Squid の設定
  4. DBサーバからプロキシサーバを経由するための設定
  5. 動作確認

1. ネットワーク・サーバ構築

以下のようにサブネット、ルートテーブルなどを作成しネットワーク・サーバを構築します。詳細は割愛しますが、ネットワーク・サーバ構築に関してはこちらの書籍がおすすめなのでぜひ読んでみてください。

<ルートテーブル>

ルートテーブル名 設定
public-route-table 10.0.0.0/16, 0.0.0.0/0
private-route-table 10.0.0.0/16

<サブネット>

サブネット名 ルートテーブル
public-subnet public-route-table
private-subnet private-route-table

<セキュリティグループ>

セキュリティグループ名 インバウンドルール
public-security-group ポート22,3128,80,443を許可(SSH,Squid,HTTP,HTTPSのポート)
private-security-group ポート22を許可(SSHのポート)

<EC2>

ホスト名 OS セキュリティグループ
proxy-server Amazon Linux 2 public-security-group
db-server Amazon Linux 2 private-security-group

2. Squid のインストール

以下コマンドで Squid をインストールします。

sudo yum install squid -y

Squid自動起動を有効にします。

sudo systemctl enable squid

3. Squid の設定

squid.conf(Squid の設定ファイル)を開きます。

sudo vi /etc/squid/squid.conf

設定ファイルに以下を追加します。

acl private-subnet src 10.0.2.0/24 # ADD for private-subnet
http_access allow private-subnet   # ADD access from private-subnet

Squid を再起動をします。

sudo service squid restart

4. DBサーバからプロキシサーバを経由するための設定

yumwget の設定ファイルにプロキシの設定をします(プロキシサーバを経由して、DBサーバから yumwget でインターネットにアクセスするために)。

まずはDBサーバの秘密鍵ファイルをプロキシサーバにアップロードし、以下のようにDBサーバに SSH 接続します。

ssh -i <秘密鍵ファイル名> ec2-user@DBサーバのプライベートIPアドレス
yum の設定

yum.conf を開きます。

sudo vi /etc/yum.conf

プロキシ設定を追加します。(Squid が使うポートを設定します)

proxy=http://<プロキシサーバのプライベートIPアドレス>:3128
wget の設定

wget の設定ファイルを開きます。

sudo vi /etc/wgetrc

プロキシ設定を追加します。(Squid が使うポートを設定します)

http_proxy=http://<プロキシサーバのプライベートIPアドレス>:3128/
https_proxy=http://<プロキシサーバのプライベートIPアドレス>:3128/
ftp_proxy=http://<プロキシサーバのプライベートIPアドレス>:3128/

5. 動作確認

yum

MariaDB をインストールしてみます。

sudo yum -y install mariadb-server

インストールすることができました。

wget

以下のWebサイトをダウンロードしてみます。

wget https://www.hatena.ne.jp/

Webサイトのダウンロードできました。

さいごに

パブリックサブネットに NATゲートウェイを構築すれば同じことをもっと簡単にできたとは思いますが、今回は勉強のためにあえて EC2 をプロキシサーバとして構築してみました(やはり手を動かして何かを作るのは楽しいですね)。AWS認定ソリューションアーキテクト-プロフェッショナル合格に向けて今後も色々なサービスを楽しみながら使っていこうと思います。