kube-awsでaws-ssm-agentとRunCommandを試してみる

kube-awsとは

AWS上にKubernetesクラスタをつくるツールです。GolangやCloudFormation、Container Linux(旧CoreOS)をベースにしています。

RunCommandとは

Amazon EC2 Systems Managerというサービスの一機能で、AWSのAPI経由でEC2インスタンスにざまざまなコマンドを実行させることができます。

aws-ssm-agentとは

RunCommandで送られたコマンドを実際に実行するデーモンです。

概要

kube-awsクラスタでRunCommandが利用できるようにします。

まず、aws-ssm-agentは公式にはrpmなどで配布されているのですが、基本的にContainer Linuxにはパッケージマネージャ的なものが存在しないのでそれが利用できません。そのため、Container Linux向けにビルドしたaws-ssm-agentをなんとかして手に入れる必要があります。

ありがたいことに、DailyHotelという会社さんがContainer Linux向けにaws-ssm-agentをビルド・公開してくださっているので、それを利用します。

https://github.com/DailyHotel/amazon-ssm-agent

また、kube-awsにはaws-ssm-agent対応があるので、ほぼcluster.yamlという設定ファイルの記述を少しいじるだけでセットアップは完了です。ただし、kube-aws 0.9.8の時点ではaws-ssm-agentの動作に必要なIAMポリシーがそのままだと付与されないというバグ?があるので、その点だけ手動でフォローする必要があります。

手順

kube-aws initkube-aws renderを実行して必要なファイルを生成したあと、cluster.yamlに以下を追記します。

amazonSsmAgent:
  enabled: true
  downloadUrl: https://github.com/DailyHotel/amazon-ssm-agent/releases/download/v2.0.805.1/ssm.linux-amd64.tar.gz
  sha1sum: a6fff8f9839d5905934e7e3df3123b54020a1f5e

clusterNamek8s1ということにします。
その場合、クラスタ名でIAMロールを検索すると下記のように3つでてきます。それぞれ、Controllerノード、Etcdノード、Workerノード用です。

image.png

それぞれに、AWSマネージドなIAMポリシー「AmazonEC2RoleforSSM」を追加します。

image.png

この時点でノードが起動完了してしまっている場合、aws-ssm-agentがエラーを吐き続けているかもしれません。(試した時は少なくともそう見えた)

その場合、aws-ssm-agentを再起動します。

sudo systemctl restart aws-ssm-agent

これで、あとはAWSコンソールから普段どおりRunCommandとAWS-RunShellScriptドキュメントを使ってクラスタ内の任意のEC2インスタンスに対して任意のシェルコマンドを実行することができます。

例えば、クラスタ内の全インスタンスを対象にしたい場合は、クラスタ名のタグが使えます。

image.png

コマンドの実行結果(時刻、成否、標準出力など)は以下のように記録されます。

image.png

CloudTrailなども使うと監査ログをとる目的ではかなり良さそうですね。

続きを読む

CloudFormationでクロスアカウントなVPCPeeringを構築する

概要

別アカウントのVPCとVPCピアリングを行う際のCFnの書き方と、ロールの設定方法について。
なお同一アカウント内でのVPCピアリングだとロール無しでスルッと行ける。

やり方

ざっくり

  1. 前提としてピアリングを行うVPCは各アカウントで用意してあること
  2. VPCPeeringを受け入れる(accepter)アカウントでクロスアカウント許可用ロールを用意
  3. VPCPeeringをリクエストする(requester)アカウントでVPCPeeringリクエストを送信
  4. 完了

ロールの用意

テンプレート

acceptrole.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Accept VPC Peering Role
Parameters:
  AcceptAccountId:
    Type: String

Resources:
  AcceptVpcPeeringRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: 'AcceptVpcPeeringRole'
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: 'sts:AssumeRole'
            Principal:
              # ここで複数アカウントを指定することも可能
              AWS: !Join
                - ''
                - - 'arn:aws:iam::'
                  - !Ref AcceptAccountId
                  - ':root'
      Policies:
        - PolicyName: 'AcceptVpcPeering'
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action: 'ec2:AcceptVpcPeeringConnection'
                # 適宜許可を行いたいVPCを設定
                Resource: '*'

実行コマンド

$ aws cloudformation create-stack 
    --stack-name accept-vpcpeering-role 
    --template-body file://acceptvpcpeeringrole.yaml 
    --parameters ParameterKey=AcceptAccountId,ParameterValue=000000 
    --capabilities CAPABILITY_NAMED_IAM 
    --profile=accepter # profileは適宜

VPCPeeringの作成

テンプレート

vpcpeering.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: VPC Peering
Parameters:
  VpcId: 
    Type: String
    Default: 'vpc-xxxxxxxxx'
  PeerOwnerId:
    Type: String
    Default: '000000000000000'
  PeerVpcId:
    Type: String
    Default: 'vpc-xxxxxxxxx'
  PeerRoleArn:
    Type: String
    Default: 'arn:aws:iam::000000000000000:role/AcceptVpcPeeringRole'

Resources:
  VpcPeering:
    Type: "AWS::EC2::VPCPeeringConnection"
    Properties:
      VpcId: !Ref VpcId
      PeerOwnerId: !Ref PeerOwnerId
      PeerVpcId: !Ref PeerVpcId
      PeerRoleArn: !Ref PeerRoleArn
      Tags:
        - Key: Name
          Value: 'TestPeering'

コマンド

$ aws cloudformation create-stack 
    --stack-name vpcpeering 
    --template-body file://vpcpeering.yaml 
    --profile=requester # profileは適宜

続きを読む

AWS Key Management Serviceでエンベロープ暗号化

はじめに

AWS Key Management Service (KMS) とは、データの暗号化に使用する暗号化キーを管理するためのマネージドサービスです。 KMS では、エンベロープ暗号化という暗号化方式を用いることでデータをセキュアに取り扱うことができます。

エンベロープ暗号化の Java での実装例について、日本語のドキュメントが少ないためまとめてみました。

エンベロープ暗号化とは

ざっくり説明すると、

  • データ
  • データを暗号化するための鍵(データキー)
  • 鍵を暗号化するための鍵(マスターキー)

を使い分けることでセキュアにしようよ、と言う仕組みのことを言います。

e076de0e-e74e-0f63-a339-f6d565b89743.png

一般的な暗号化方式では、平文のデータを何らかの鍵を使って暗号化することで保護します。このとき、この鍵がデータとセットで盗まれてしまうと、暗号を解かれてしまい、生のデータを見られてしまいます。そこで、データを暗号化した鍵を、別の鍵でさらに暗号化することでデータをより強固に保護しよう、というのがエンベロープ暗号化の考え方です。

KMS を使ったデータの暗号化は、以下の流れで実現できます。

  • KMS のコンソール画面でマスターキーを生成する
  • マスターキーからデータキーを生成する
  • データキーを使って平文のデータを暗号化する
  • データキーを暗号化する
  • 暗号化済みのデータキーと暗号化済みのデータを大切に保管する

実装例

エンベロープ暗号化の実現方法について、 Java での実装例を以下に示します。

なお、マスターキーは KMS のコンソール画面から事前に生成済み、という前提で話を進めます。マスターキーの生成手順については、AWS のマニュアルDevelopers.IO 等の記事を参考にしてください。

事前準備

KMS の利用にあたっては AWS から SDK が提供されています。ここでは Gradle を介してライブラリを使用する場合の例を示します。 build.gradle に以下の dependency を追加してください。

dependencies {
    compile "com.amazonaws:aws-java-sdk-kms:1.11.202"
}

暗号化

まずは、 AWS のアクセスキー、シークレットキーを使用して AWSKMSClient のインスタンスを生成します。

BasicAWSCredentials credentials = new BasicAWSCredentials("accessKey...", "SecretKey...");
AWSKMSClient client = (AWSKMSClient) AWSKMSClientBuilder.standard()
    .withRegion("ap-northeast-1")
    .withCredentials(new AWSStaticCredentialsProvider(credentials))
    .build();

生成したクライアントを使用して、データキーを生成します。 KeyId に KMS のコンソールから生成したマスターキーの ARN を指定して GenerateDataKeyRequest のインスタンスを生成し、 generateDataKey することでデータキーを取得できます。

GenerateDataKeyRequest generateDataKeyRequest = new GenerateDataKeyRequest()
    .withKeyId("arn:aws:kms:...")
    .withKeySpec(DataKeySpec.AES_128);
GenerateDataKeyResult dataKeyResult = client.generateDataKey(generateDataKeyRequest);

生成したデータキーを使って、平文のデータを暗号化します。ここでは AES を使って暗号化していますが、暗号化方式は任意のものをお使いください。

try {
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.ENCRYPT_MODE,
                new SecretKeySpec(dataKeyResult.getPlaintext().array(), "AES"));
    String decriptedText = Base64.getEncoder()
        .encodeToString(cipher.doFinal("plainText...".getBytes()));
} catch (Exception e) {
    // do something.
}

生成された decriptedText と 暗号化済みのデータキー(dataKeyResult.getCiphertextBlob() で暗号化済みのものを取得できます)は、 Base64 で encode して、 yaml ファイル等に書き写して保管しておきましょう。

復号化

続いて、暗号化済みデータの復号化の手順です。復号化は、暗号化済みのデータキーから decryptRequest を生成することで行います。

DecryptRequest decryptRequest = new DecryptRequest()
    .withCiphertextBlob(ByteBuffer.wrap(Base64.getDecoder().decode("decryptedDatakey...".getBytes())));
ByteBuffer plainTextKey = client.decrypt(decryptRequest).getPlaintext();

ここまでの手順で、暗号化済みのデータキーを平文のデータキーに戻すことができました。続いて、手に入れたデータキーを用いて暗号化済みのデータを復号化します。

byte[] decodeBase64src = Base64.getDecoder().decode("decryptedText...");
try {
    Cipher cipher = Cipher.getInstance("AES");
    cipher.init(Cipher.DECRYPT_MODE, plainTextKey);
    String plainText = new String(cipher.doFinal(decodeBase64src));
} catch (Exception e) {
    // do something.
}

取り出した平文のデータキー( plainTextKey )は速やかに破棄しましょう。

plainTextKey.clear();

参考資料

続きを読む

【AWS Batch】1コマンドでdockerを定期実行する

CloudWatch EventsでLambdaを定期実行し、LambdaからAWS Batch上でdockerが動くようにする。

scheduled-docker-batch.yaml
Parameters:
  SubnetIds:
    Description: Subnets For ComputeEnvironment
    Type: List<AWS::EC2::Subnet::Id>
  SecurityGroupIds:
    Description: SecurityGroups For ComputeEnvironment
    Type: List<AWS::EC2::SecurityGroup::Id>
Resources:
  BatchComputeSpotFleetRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: spotfleet.amazonaws.com
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2SpotFleetRole
  BatchComputeEnvironmentRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: batch.amazonaws.com
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSBatchServiceRole
  BatchComputeInstanceRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role
  BatchComputeInstanceProfile:
    Type: "AWS::IAM::InstanceProfile"
    Properties:
      Path: "/"
      Roles:
        - Ref: BatchComputeInstanceRole
  BatchJobQueue:
    Type: "AWS::Batch::JobQueue"
    Properties:
      JobQueueName: scheduled-job-queue
      Priority: 1
      ComputeEnvironmentOrder:
        - Order: 1
          ComputeEnvironment: !Ref BatchComputeEnvironment
  BatchComputeEnvironment:
    Type: AWS::Batch::ComputeEnvironment
    Properties:
      Type: MANAGED
      ServiceRole: !Ref BatchComputeEnvironmentRole
      ComputeEnvironmentName: scheduled-batch-compute-environment
      ComputeResources:
        Type: SPOT
        SecurityGroupIds: !Ref SecurityGroupIds
        Subnets: !Ref SubnetIds
        MaxvCpus: 128
        MinvCpus: 0
        DesiredvCpus: 0
        InstanceRole: !Ref BatchComputeInstanceProfile
        InstanceTypes:
          - optimal
        SpotIamFleetRole: !Ref BatchComputeSpotFleetRole
        BidPercentage: 50
      State: ENABLED
  JobDefinition:
    Type: AWS::Batch::JobDefinition
    Properties:
      Type: container
      JobDefinitionName: schedule-batch-example
      ContainerProperties:
        Command:
          - /hello
        Memory: 256
        Vcpus: 1
        Image: hello-world
      RetryStrategy:
        Attempts: 1
  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: CustomLocalPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - batch:SubmitJob
                Resource:
                  - "*"
  JobSubmitFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      # Handler: "index.handler"
      Handler: "index.handler"
      Role: !GetAtt [ LambdaExecutionRole, Arn ]
      Code:
        ZipFile: |
          import os
          import boto3
          def handler(event, context):
              params = {
                  'jobName': 'schedule-sample-job',
                  'jobQueue': os.environ.get('JOB_QUEUE'),
                  'jobDefinition': os.environ.get('JOB_DEFINITION')
              }
              print(params)
              client = boto3.client('batch')
              res = client.submit_job(**params)
              print(res)
      Runtime: "python3.6"
      Environment:
        Variables:
          JOB_QUEUE: !Ref BatchJobQueue
          JOB_DEFINITION: !Ref JobDefinition
  JobSubmitEventRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: rate(5 minutes)
      Targets:
        - Id: JobSubmitScheduler
          Arn:
            Fn::GetAtt:
              - JobSubmitFunction
              - Arn
  InvokeLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName:
        Fn::GetAtt:
          - JobSubmitFunction
          - Arn
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn:
        Fn::GetAtt:
          - JobSubmitEventRule
          - Arn

実行方法

$ aws cloudformation create-stack --stack-name schedule-docker-batch 
  --template-body file://./schedule-docker-batch.yaml 
  --capabilities CAPABILITY_IAM 
  --parameters ParameterKey=SubnetIds,ParameterValue="subnet-xxxxxxx" ParameterKey=SubnetIds,ParameterValue="subnet-yyyyyyy" ParameterKey=SecurityGroupIds,ParameterValue="sg-zzzzzzzz"

よしなにJobDefinitionを編集して好きなようにdockerを動かす。

続きを読む

DatadogでJavaのメトリクスを取得する

JavaアプリケーションのパフォーマンスをDatadogで可視化しようとしたときにハマったので、
誰かの役に立てばと思い、残しておきます。

やりたかったこと

  • JVMHeap使用状況などのメトリクスを取得したかった。

公式ドキュメントは以下にあるので、ドキュメントを参考にやってみます。
JMX Checks

JavaのIntegrationを有効にする

DatadogはデフォルトではJavaのメトリクスを取得するようになっていません。
なので明示的に設定をOnにする必要があります。
サイドバーのIntegrationから、Javaを検索し設定をOnにします。
Setup___Datadog.png

availableをクリックし詳細画面からinstall integrationをクリック

Setup___Datadog.png

jmx.yamlを追加

次にdatadog-agentをinstallしたサーバ内に入り、Javaが動いているサーバの情報をdatadog-agentに教えてあげる必要があります。

datadog-agentはデフォルトで様々なintegrationのコンフィグファイル例を用意してくれており、今回はそれをコピーしてそのまま使います。

$ cp /etc/dd-agent/conf.d/jmx.yaml.example  /etc/dd-agent/conf.d/jmx.yaml
$ vim /etc/dd-agent/conf.d/jmx.yaml
/etc/dd-agent/conf.d/jmx.yaml
init_config:
instances:
-   host: localhost
    port: 7199

port: 7199など気になる設定はありますが、そのまま使用します。

datadog-agentをrestartする

設定ファイルを変更した際はagentをrestartすると変更した設定が読み込まれます。

$ sudo /etc/init.d/datadog-agent restart

だが値は取れない

dashboardを作成したときに、jmx.hogehogeが候補として出てくることを期待するが出てこない。
Listenしてないportなどを指定していたので当たり前といえば当たり前です。

結論から言うとJavaアプリケーション起動時にJMXのポートを公開しておかないと値が取れません。
JMXとは何かという方は以下を参照してください。
JMX について

起動時に以下のオプションを追加する。

# jmxをonにする
-Dcom.sun.management.jmxremote
# jmxの情報を取得できるportを指定します。jmx.yamlで指定したportはここでの設定したportになります
-Dcom.sun.management.jmxremote.port=7199
# 通信時にsslをonにするかどうか。今回はlocalhostでの通信なのでOffにします。
-Dcom.sun.management.jmxremote.ssl=false
# password認証をするか
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.local.only=true

追加すると

jvmがプレフィックスにつくメトリクスが、Datadogのコンソール上に表示されるようになりました。
biz-prd-elb__created_via_Terraform____Datadog.png

続きを読む

AWS Lambdaをlocal環境で実行できるaws-sam-localを試してみる

インストール

npmでインストールすることができます

$ npm install -g aws-sam-local

プロジェクト作成

適当なディレクトリを作成しSAMの公式プロジェクトのサンプルを参考に
index.jsとtemplate.yml を作成していきます

template.yml
AWSTemplateFormatVersion : '2017-09-20'

Description: A hello world application.

Resources:
  HelloWorld:
    Type: AWS::Serverless::Function
    Properties:
      Handler: index.handler
      Runtime: nodejs6.10
index.js
'use strict';
console.log('Loading function');

exports.handler = (event, context, callback) => {
    callback(null, 'Hello World!');
};

実行

index.jsとtemplate.ymlが作成できたら実行してみます

※事前にDockerを起動しておく必要があります

$ echo '{}' | sam local invoke HelloWorld
2017/09/20 18:39:12 Successfully parsed template.yaml
2017/09/20 18:39:12 Connected to Docker 1.30
2017/09/20 18:39:12 Fetching lambci/lambda:nodejs6.10 image for nodejs6.10 runtime...
nodejs6.10: Pulling from lambci/lambda
Digest: sha256:b9a57b98dcfe226cac3fb0b8329594eefb62ba7089fa27cf8ac968e5736cc04a
Status: Image is up to date for lambci/lambda:nodejs6.10
2017/09/20 18:39:15 Reading invoke payload from stdin (you can also pass it from file with --event)
2017/09/20 18:39:15 Invoking index.handler (nodejs6.10)
START RequestId: feb31e2a-3748-1152-e1c1-85725b4021a7 Version: $LATEST
2017-09-20T09:39:16.722Z    feb31e2a-3748-1152-e1c1-85725b4021a7    Loading function
END RequestId: feb31e2a-3748-1152-e1c1-85725b4021a7
REPORT RequestId: feb31e2a-3748-1152-e1c1-85725b4021a7  Duration: 9.66 ms   Billed Duration: 0 ms   Memory Size: 0 MB   Max Memory Used: 29 MB

"Hello World!"


1回目の実行のときは Docker イメージの DLを行ってから実行されます

続きを読む

メールにパスワード付きzipを添付して「パスワードは別途お送りいたします」とする慣習がめんどくさいのでなんとかした

あの慣習

メールにパスワード付きzipを添付して「パスワードは別途お送りいたします」とする慣習、ありますよね。
自分からはやらないけど、相手に合わせてやらざるを得なかったりしてめんどくさい。

ここでは、このやり方の是非は問題にしません。
どんなに是非を説いても、この慣習があるという状況は変わらないので。

そして、この慣習を無くすことも考えません。
そういうのは巨大な力を持った何かにおまかせします。

昔のエラい人は言いました。「長いものには巻かれろ」と。
ただし、巻かれ方は考えたほうがいいと思うのです。

スマートな巻かれ方を考える

巻かれるにあたって、解決したいことはただ一つ。めんどくさくないこと。
このためにWebシステム作って、ブラウザ開いてどうのこうのなんてやってると本末転倒です。
可能な限り、普通のメール送信に近い形で実現したい。

というわけで、あれこれ考えた末、一部の制約を許容しつつ、AmazonSESを使ってサーバーレスな感じで解決してみました。

仕様

  1. 普通にメールを書く(新規・返信・転送問わず)
  2. ファイルをzipで固めずにそのまま放り込む
  3. SES宛のメールアドレスをToに、実際にファイルを送りたい相手をReply-Toに設定する。
  4. システムを信じて送信ボタンを押す
  5. 自分と相手に、パスワード付きzipが添付されたメールとパスワードのお知らせメールが届く

ただし、以下の制約があります。個人的には許容範囲です。

  • 結果的に相手方には全員Toで届く。Ccはできない(自分はBcc)
  • zipファイルの名前は日時(yymmddHHMMSS.zip)になる(中身のファイル名はそのまま)

システム構成

flow_01.png

  1. SESに宛ててメールを送る
  2. メールデータがS3に保存される
  3. それをトリガーにしてLambdaが起動する
  4. Lambdaがメールの内容を解析してパスワードとzipファイルを生成する
  5. いい感じにメールを送る(念のため自分にもBccで送る)

実装

Lambda

真面目にpython書いたの初めてだけどこんな感じでいいのかな?
大体メールと文字コードとファイルとの戦いです。

# -*- coding: utf-8 -*-

import os
import sys
import string
import random
import json
import urllib.parse
import boto3
import re
import smtplib
import email
import base64
from email                import encoders
from email.header         import decode_header
from email.mime.base      import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text      import MIMEText
from email.mime.image     import MIMEImage
from datetime             import datetime

sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'vendored'))
import pyminizip

s3 = boto3.client('s3')

class MailParser(object):
    """
    メール解析クラス
    (参考) http://qiita.com/sayamada/items/a42d344fa343cd80cf86
    """

    def __init__(self, email_string):
        """
        初期化
        """
        self.email_message    = email.message_from_string(email_string)
        self.subject          = None
        self.from_address     = None
        self.reply_to_address = None
        self.body             = ""
        self.attach_file_list = []

        # emlの解釈
        self._parse()

    def get_attr_data(self):
        """
        メールデータの取得
        """
        attr = {
                "from":         self.from_address,
                "reply_to":     self.reply_to_address,
                "subject":      self.subject,
                "body":         self.body,
                "attach_files": self.attach_file_list
                }
        return attr


    def _parse(self):
        """
        メールファイルの解析
        """

        # メッセージヘッダ部分の解析
        self.subject          = self._get_decoded_header("Subject")
        self.from_address     = self._get_decoded_header("From")
        self.reply_to_address = self._get_decoded_header("Reply-To")

        # メールアドレスの文字列だけ抽出する
        from_list =  re.findall(r"<(.*@.*)>", self.from_address)
        if from_list:
            self.from_address = from_list[0]
        reply_to_list =  re.findall(r"<(.*@.*)>", self.reply_to_address)
        if reply_to_list:
            self.reply_to_address = ','.join(reply_to_list)

        # メッセージ本文部分の解析
        for part in self.email_message.walk():
            # ContentTypeがmultipartの場合は実際のコンテンツはさらに
            # 中のpartにあるので読み飛ばす
            if part.get_content_maintype() == 'multipart':
                continue
            # ファイル名の取得
            attach_fname = part.get_filename()
            # ファイル名がない場合は本文のはず
            if not attach_fname:
                charset = str(part.get_content_charset())
                if charset != None:
                    if charset == 'utf-8':
                        self.body += part.get_payload()
                    else:
                        self.body += part.get_payload(decode=True).decode(charset, errors="replace")
                else:
                    self.body += part.get_payload(decode=True)
            else:
                # ファイル名があるならそれは添付ファイルなので
                # データを取得する
                self.attach_file_list.append({
                    "name": attach_fname,
                    "data": part.get_payload(decode=True)
                })

    def _get_decoded_header(self, key_name):
        """
        ヘッダーオブジェクトからデコード済の結果を取得する
        """
        ret = ""

        # 該当項目がないkeyは空文字を戻す
        raw_obj = self.email_message.get(key_name)
        if raw_obj is None:
            return ""
        # デコードした結果をunicodeにする
        for fragment, encoding in decode_header(raw_obj):
            if not hasattr(fragment, "decode"):
                ret += fragment
                continue
            # encodeがなければとりあえずUTF-8でデコードする
            if encoding:
                ret += fragment.decode(encoding)
            else:
                ret += fragment.decode("UTF-8")
        return ret

class MailForwarder(object):

    def __init__(self, email_attr):
        """
        初期化
        """
        self.email_attr = email_attr
        self.encode     = 'utf-8'

    def send(self):
        """
        添付ファイルにパスワード付き圧縮を行い転送、さらにパスワード通知メールを送信
        """

        # パスワード生成
        password = self._generate_password()

        # zipデータ生成
        zip_name = datetime.now().strftime('%Y%m%d%H%M%S')
        zip_data = self._generate_zip(zip_name, password)

        # zipデータを送信
        self._forward_with_zip(zip_name, zip_data)

        # パスワードを送信
        self._send_password(zip_name, password)

    def _generate_password(self):
        """
        パスワード生成
        記号、英字、数字からそれぞれ4文字ずつ取ってシャッフル
        """
        password_chars = ''.join(random.sample(string.punctuation, 4)) + 
                         ''.join(random.sample(string.ascii_letters, 4)) + 
                         ''.join(random.sample(string.digits, 4))

        return ''.join(random.sample(password_chars, len(password_chars)))

    def _generate_zip(self, zip_name, password):
        """
        パスワード付きZipファイルのデータを生成
        """
        tmp_dir  = "/tmp/" + zip_name
        os.mkdir(tmp_dir)

        # 一旦ローカルにファイルを保存
        for attach_file in self.email_attr['attach_files']:
            f = open(tmp_dir + "/" + attach_file['name'], 'wb')
            f.write(attach_file['data'])
            f.flush()
            f.close()

        # パスワード付きzipに
        dst_file_path = "/tmp/%s.zip" % zip_name
        src_file_names = ["%s/%s" % (tmp_dir, name) for name in os.listdir(tmp_dir)]

        pyminizip.compress_multiple(src_file_names, dst_file_path, password, 4)

        # # 生成したzipファイルを読み込み
        r = open(dst_file_path, 'rb')
        zip_data = r.read()
        r.close()

        return zip_data

    def _forward_with_zip(self, zip_name, zip_data):
        """
        パスワード付きZipファイルのデータを生成
        """
        self._send_message(
                self.email_attr['subject'],
                self.email_attr["body"].encode(self.encode),
                zip_name,
                zip_data
                )
        return

    def _send_password(self, zip_name, password):
        """
        zipファイルのパスワードを送信
        """

        subject = self.email_attr['subject']

        message = """
先ほどお送りしたファイルのパスワードのお知らせです。

[件名] {}
[ファイル名] {}.zip
[パスワード] {}
        """.format(subject, zip_name, password)

        self._send_message(
                '[password]%s' % subject,
                message,
                None,
                None
                )
        return

    def _send_message(self, subject, message, attach_name, attach_data):
        """
        メール送信
        """

        msg = MIMEMultipart()

        # ヘッダ
        msg['Subject'] = subject
        msg['From']    = self.email_attr['from']
        msg['To']      = self.email_attr['reply_to']
        msg['Bcc']     = self.email_attr['from']

        # 本文
        body = MIMEText(message, 'plain', self.encode)
        msg.attach(body)

        # 添付ファイル
        if attach_data:
            file_name = "%s.zip" % attach_name
            attachment = MIMEBase('application', 'zip')
            attachment.set_param('name', file_name)
            attachment.set_payload(attach_data)
            encoders.encode_base64(attachment)
            attachment.add_header("Content-Dispositon", "attachment", filename=file_name)
            msg.attach(attachment)

        # 送信
        smtp_server   = self._get_decrypted_environ("SMTP_SERVER")
        smtp_port     = self._get_decrypted_environ("SMTP_PORT")
        smtp_user     = self._get_decrypted_environ("SMTP_USER")
        smtp_password = self._get_decrypted_environ("SMTP_PASSWORD")
        smtp = smtplib.SMTP(smtp_server, smtp_port)
        smtp.ehlo()
        smtp.starttls()
        smtp.ehlo()
        smtp.login(smtp_user, smtp_password)
        smtp.send_message(msg)
        smtp.quit()
        print("Successfully sent email")

        return

    def _get_decrypted_environ(self, key):
        """
        暗号化された環境変数を復号化
        """

        client = boto3.client('kms')
        encrypted_data = os.environ[key]
        return client.decrypt(CiphertextBlob=base64.b64decode(encrypted_data))['Plaintext'].decode('utf-8')

def lambda_handler(event, context):

    # イベントからバケット名、キー名を取得
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'])

    try:
        # S3からファイルの中身を読み込む
        s3_object = s3.get_object(Bucket=bucket, Key=key)
        email_string = s3_object['Body'].read().decode('utf-8')

        # メールを解析
        parser = MailParser(email_string)

        # メール転送
        forwarder = MailForwarder(parser.get_attr_data())
        forwarder.send()
        return

    except Exception as e:
        print(e)
        raise e

pyminizip

パスワード付きzipは標準のライブラリじゃできないっぽい。
ということで、ここだけpyminizipという外部ライブラリに頼りました。
ただこれ、インストール時にビルドしてバイナリ作る系のライブラリだったので、Lambdaで動かすためにローカルでAmazonLinuxのDockerコンテナ立ててバイナリを作りました。何かほかにいい方法あるのかな。。

AWS SAM

ちなみに、これはAWS SAMを使ってローカルテストしてみました。
SMTPサーバーの情報を直書きして試してたところまでは良かったけど、それを環境変数に移すとうまく動かなくて挫折しました。修正はされてるけどリリースされてないっぽい。

導入方法

せっかくなので公開してみます。コードネームzaru
かなり設定方法が泥臭いままですがご容赦ください。。
https://github.com/Kta-M/zaru

自分の環境(Mac, Thunderbird)でしか試してないので、メーラーやその他環境によってはうまくいかないかも?自己責任でお願いします。

SES

SESはまだ東京リージョンで使えないので、オレゴンリージョン(us-west-2)で構築します。

ドメイン検証

まずはSESに向けてメールが送れるように、ドメインの検証を行います。
やり方はいろいろなので、このあたりは割愛。
たとえばこのあたりとか参考になるかも -> RailsでAmazon SES・Route53を用いてドメインメールを送信する

Rule作成

ドメインの検証ができたら、Ruleを作成します。

メニュー右側のRule Setsから、View Active Rule Setをクリック。
ses_rule_01.png

Create Ruleをクリック。
ses_rule_02.png

受信するメールアドレスを登録。検証を行なったドメインのメールアドレスを入力して、Add Recipientをクリック。
ses_rule_03.png

メール受信時のアクションを登録。
アクションタイプとしてS3を選択し、受信したメールデータを保存するバケットを指定します。このとき、Create S3 bucketでバケットを作成してあげると、必要なバケットポリシーが自動で登録されて便利。
SESからバケットへのファイルアップロードを許可するポリシーが設定されます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowSESPuts-XXXXXXXXXXXX",
            "Effect": "Allow",
            "Principal": {
                "Service": "ses.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::<ses-bucket-name>/*",
            "Condition": {
                "StringEquals": {
                    "aws:Referer": "XXXXXXXXXXXX"
                }
            }
        }
    ]
}

また、バケットに保存されたメールデータは、貯めておいても仕方ないので、ライフサイクルを設定して一定期間経過後削除されるようにしておくといいかも。
ses_rule_04.png

ルールに名前を付けます。あとはデフォルトで。
ses_rule_05.png

登録内容を確認して、登録!
ses_rule_06.png

Lambda

デプロイ

SESと同じくオレゴンリージョンにデプロイします。
CloudFormationを利用するので、データをアップロードするS3バケットを作っておいてください。

# git clone git@github.com:Kta-M/zaru.git
# cd zaru
# aws cloudformation package --template-file template.yaml --s3-bucket <cfn-bucket-name> --output-template-file packaged.yaml
# aws cloudformation deploy --template-file packaged.yaml --stack-name zaru-stack --capabilities CAPABILITY_IAM --region us-west-2

Lambdaのコンソールに行くと、関数が作成されています。
また、この関数の実行に必要なIAMロールも作成されています。
lambda_01.png

トリガー設定

バケットにメールデータが入るのをトリガーにして、Lambdaが動くように設定します。

関数の詳細画面のトリガータブに移動します。
lambda_02.png

トリガーを追加をクリックし、S3のイベントを作成します。
SESからデータが来るバケット、イベントタイプはPutです。それ以外はデフォルト。
バケットはlambda_03.png

暗号化キーを作成

このLambda関数内では、暗号化された環境変数からSMTP関連の情報を取得しています。
その暗号化に使用するキーを作成します。

IAMコンソールから、左下にある暗号化キーをクリックします。
リージョンをオレゴンに変更し、キーを作成してください。
lambda_04.png

設定内容は、任意のエイリアスを設定するだけで、残りはデフォルトでOKです。
lambda_05.png

環境変数数設定

Lambdaに戻って、関数内で使用する環境変数を設定します。
コードタブの下のほうに、環境変数を設定するフォームがあります。
暗号化ヘルパーを有効にするにチェックを入れ、先ほど作成した暗号化キーを指定します。
環境変数は、変数名と値(平文)を入力し、暗号化ボタンを押します。すると、指定した暗号化キーで暗号化してくれます。
設定する環境変数は以下の4つです。

変数名 説明
SMTP_SERVER smtpサーバー smtp.example.com
SMTP_PORT smtpポート 587
SMTP_USER smtpサーバーにログインするユーザー名 test@example.com
SMTP_PASSWORD SMTP_USERのパスワード

lambda_06.png

ロール設定

最後に、このLambda関数を実行するロールに必要な権限を付けます。
– メールデータを保存するS3バケットからデータを取得する権限
– 暗号化キーを使って環境変数を復号する権限

まず、IAMコンソールのポリシーに行き、ポリシーの作成->独自のポリシーを作成で以下の2つのポリシーを作成します。
lambda_07.png

ポリシー:s3-get-object-zaru
<ses-bucket-name>には、SESからメールデータを受け取るバケット名を指定してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1505586008000",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::<ses-bucket-name>/*"
            ]
        }
    ]
}

ポリシー;kms-decrypt-zaru
<kms-arn>には、暗号化キーのARNを指定してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1448696327000",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "<kms-arn>"
            ]
        }
    ]
}

最後に、この2つのポリシーを、Lambda関数実行ロールにアタッチします。
まず、IAMコンソールのロールに行き、ロールを選択し、ポリシーのアタッチからアタッチします。
lambda_08.png

動作確認

これで動くようになったはずです。
ToにSES向けに設定したメールアドレス、Reply-Toに相手のメールアドレスを設定し、適当なファイルを添付して送ってみてください。どうでしょう?

まとめ

どんとこいzip添付!

続きを読む

忙しい人のための Use AWS WAF to Mitigate OWASP’s Top 10 Web Application Vulnerabilities メモ

こんにちは、ひろかずです。

2017年7月に「Use AWS WAF to Mitigate OWASP’s Top 10 Web Application Vulnerabilities」が公開されました。
日本語翻訳されていないので、なかなか読めていない方もいると思います。
リリースから少し時間が経ってしまいましたが、読了したので一筆書きます。

どんなドキュメントか

OWASP Top10 で上っている各項目についての解説と、各項目についてAWS WAFでどのような対応ができるかを記載したもの。
Conditionを作る上でのアドバイスも記載されている。

参照ドキュメント

Use AWS WAF to Mitigate OWASP’s Top 10 Web Application Vulnerabilities

A1 – Injection

SQLインジェクション攻撃は、比較的簡単に検出できるが、バックエンドの構造によって複雑さが変わるため、アプリケーション側での対応も必要。
cookieやカスタムヘッダをデータベース参照に使う場合は、Conditionで検査対象とするように設定すること。
リクエスト中にクエリを流す事を是としているサービスの場合、対象URLを検知除外(バイパス)するような設定を検討すること。

A2 – Broken Authentication and Session Management

String Matchを活用して、盗まれた(怪しい)トークンを「安全のために」ブラックリストに登録する
デバイスのロケーションが異なるトークンを検出する等、ログから悪性利用をしているトークンを分析して、ブラックリストに登録するような使い方。
Rate-basedルールを用いて、認証URLを総当り攻撃を防ぐ施策も有効。

A3 – Cross-Site Scripting (XSS)

XSS攻撃は、HTTPリクエストで特定のキーHTMLタグ名を必要とするため、一般的なシナリオで比較的簡単に軽減できる。
BODYやQUERY_STRING, HEADER: Cookieを検査するのが推奨される。
URLを検査対象とすることは一般的ではないが、アプリケーションが短縮URL使っていると、パラメータはURLパスセグメントの一部として表示され、クエリ文字列には表示されない(後でサーバー側で書き換えられる)
CMSエディタ等、たくさんHTMLを生成するサービスには効果は薄い。
対象URLを検知除外(バイパス)する場合は、別途対策を検討すること。
加えて、<script>タグを多用するイメージ(svgグラフィックフォーマット)やカスタムデータも誤検知率が高いので調整が必要。

A4 – Broken Access Control

2017年から新たに登場
認証ユーザーの制限が適切に動作せず、必要以上に内部アプリケーションオブジェクト(不正なデータの公開、内部Webアプリケーション状態の操作、パストラバーサル、リモート/ローカルファイルの呼び出し)を操作できるというもの。
機能レベルのアクセス制御の欠陥は、一般的に後で追加されたアプリケーションで発生する。
アクセスレベルは、呼び出し時に一度検証されるが、その後に呼び出される様々なサブルーチンについては、都度検証されない。
呼び出し元のコードが、ユーザーの代わりに他のモジュールやコンポーネントを呼び出す暗黙的な信頼ができてしまう。
アプリケーションが、アクセスレベルやサブスクリプションレベルに応じてコンポーネントへアクセスさせる場合は、呼び出しの都度、検証する必要がある。

AWS WAFとしては、「../」や「://」をQUERY_STRINGやURIに含むかを検証することで、リモート/ローカルファイルの呼び出しを検出できる。
これも、リクエスト中に「../」や「://」を含む事を是とするアプリケーションには効果は薄い。
管理モジュール、コンポーネント、プラグイン、または関数へのアクセスが既知の特権ユーザーのセットに限定されている場合、Byte_MatchとIPSetの組み合わせでアクセスを制限することができる。

認証要求がHTTPリクエストの一部として送信され、JWTトークン(のようなもの)でカプセル化されている場合

  • Lambda@Edgeファンクションを用いて、関連リクエストパラメータがトークン内のアサーションおよび承認と一致することを確認することで、権限不適合リクエストはバックエンドに届く前に拒否することができる。

A5 – Security Misconfiguration

セキュリティの影響を受けるパラメータの設定ミスは、アプリケーションだけではなく、OSやミドルウェア、フレームワーク全てにおいて発生し得る。
セキュリティの影響を受けるパラメータの設定ミス例

  • ApacheのServerTokens Full(デフォルト)構成
  • 本番Webサーバーでデフォルトのディレクトリ一覧を有効にしたまま
  • エラーのスタックトレースをユーザーに戻すアプリケーション
  • PHPの脆弱なバージョンと組み合わせて、HTTPリクエストを介して内部サーバ変数を上書き

AWS WAFとしては、パラメータの設定ミスを悪用するHTTPリクエストパターンが認識可能な場合に限り対応可能。

  • A4 – Broken Access Controlと同様に、特定パスやコンポーネントに対してByte_MatchとIPSetの組み合わせでアクセスを制限を行う。(Wordpress管理画面 等)
  • 脆弱なPHPを利用している場合、QUERY_STRING中の“_SERVER[“を遮断する。

その他に考慮できること

  • Amazon Inspectorでの設定ミスの捜査(rootログイン許可や脆弱なミドルウェアバージョン、脆弱性の対応状況)
  • Amazon InspectorでのCISベンチマークとの適合性チェック
  • AWS ConfigやAmazon EC2 Systems Managerを用いた、システム構成変更の検出

A6 – Sensitive Data Exposure

アプリケーションの欠陥による機密情報の暴露をWAFで緩和するのは難しい。
この欠陥には、一般に不完全に実装された暗号化が含まれる(弱い暗号の利用を許容していること)

AWS WAFとしては、HTTPリクエスト中の機密情報を識別するパターンをString-Match Conditionで検出することで、機密情報へのアクセスを検出することができる。(ホストするアプリケーションに対する深い知識が必要)
アプリケーションが、アップロードを許容する場合、Base64を示す文字列の一部を検出するString-Match Conditionが有効(BodyはBase64でエンコードされているから)
一般的ではない手法

その他に考慮できること

  • AWS内の機能を使って、接続ポイントにて、ELBで強い暗号化suiteを使用するように制限する
  • クラシックロードバランサの場合、事前定義・カスタムセキュリティポリシーで制御できる。
  • アプリケーションロードバランサの場合、事前定義セキュリティポリシーで制御できる。
  • Cloud Frontでも利用するSSLプロトコルを制限できる。

A7 – Insufficient Attack Protection

2017年から新設
このカテゴリは、新しく発見された攻撃経路や異常なリクエストパターン、または発見されたアプリケーションの欠陥にタイムリーに対応する能力に重点を置いている。
幅広い攻撃ベクトルが含まれるので、他のカテゴリと重複する部分もある。

確認ポイント

  • アプリケーションに対して、異常なリクエストパターンや大量通信を検出できるか?
  • その検出を自動化できる仕組みはあるか?
  • 不要な通信に対して、ブロックできる仕組みはあるか?
  • 悪意のあるユーザーの攻撃開始を検出できるか?
  • アプリケーションの脆弱性に対するパッチ適用までの時間はどの程度かかる?
  • パッチ適用後の有効性を確認する仕組みはあるか?

AWS WAFで何ができるか

  • Size-Content Conditionを使うことで、アプリケーションで利用するサイズ以上の通信を検出・遮断できる。
  • URIやクエリ文字列のサイズを、アプリケーションにとって意味のある値に制限すること。
  • RESTful API用のAPIキーなどの特定のヘッダーが必要になるようにすることもできる。
  • 特定IPアドレスからの5分間隔での要求レートに対する閾値設定。(例えば、UserAgentとの組み合わせでのカウントも可能)
  • Web ACLはプログラマブルで反映が早いのが利点(CloudFrontに対しても約1分で反映)
  • ログの出力傾向から分析して、Web ACLを自動調整する機能も構築できる。

AWS WAFのセキュリティオートメーション機能の活用

  • Lambdaを使って、4xxエラーを多く出しているIPアドレスを特定してIPブラックリストに登録する。
  • 既知の攻撃者に対しては、外部ソースのIPブラックリストを活用する。
  • ボットとスクレイバに対しては、robots.txtファイルの ‘disallow’セクションにリストされている特定URLへのアクセスを行ったIPを、IPブラックリストに登録する。(ハニーポットと表現されています)

A8 – Cross-Site Request Forgery

CSRFは、WebアプリケーションのStateを変更する機能を対象としている。
State変更に係るURLやフォーム送信などのHTTPリクエストを考慮する。
「ユーザーがその行動を取っている」ということを必ず証明する機構がなければ、悪意のあるユーザーによるリクエスト偽造を判断する方法はない。

  • セッショントークンや送信元IPは偽造できるので、クライアント側の属性に頼るのは有効ではない。
  • CSRFは、特定のアクションのすべての詳細が予測可能であるという事実(フォームフィールド、クエリ文字列パラメータ)を利用する。
  • 攻撃は、クロスサイトスクリプティングやファイルインクルージョンなどの他の脆弱性を利用する方法で実行される。

CSRF攻撃への対処

  • アクションをトリガーするHTTPリクエストに予測困難なトークンを含める
  • ユーザー認証プロンプトの要求
  • アクション要求時のキャプチャの提示

AWS WAFで何ができるか

  • 固有トークンの存在確認
  • 例えば、UUIDv4を活用して、x-csrf-tokenという名前のカスタムHTTPヘッダーの値を期待する場合は、Byte-Size Conditionが使える。
  • ブロックルールには、POST HTTPリクエスト条件に加える

その他にできること

  • 上記のようなルールでは、古い/無効な/盗まれた トークンの検出については、アプリケーションでの対応が必要
  • 例えばの仕組み
  • サーバが、一意のトークンを隠しフィールドとしてブラウザに送信し、ユーザーのフォーム送信時の期待値にする。
  • 期待したトークンが含まれないPOSTリクエストを安全に破棄できる。
  • 処理後のセッションストアからトークンは削除しておくことで、再利用がされないことを保証。

A9 – Using Components with Known Vulnerabilities

既知の脆弱性を持つコンポーネントの利用
ソースや、商用/オープンソースのコンポーネント、フレームワークを最新に保つことが重要
最も簡単な攻撃ベクトルで、その他の攻撃手法の突破口にもなる。
脆弱なサブコンポーネントに依存するコンポーネントの利用しているケース。
コンポーネントの脆弱性は、CVEにて管理/追跡されないので、緩和が難しい。

  • アプリケーション開発者は、それぞれのベンダー、作成者、またはプロバイダとのコンポーネントのステータスを個別に追跡する責任を負う。
  • 脆弱性は、既存のバージョンを修正するのではなく、新機能を含む新しいバージョンのコンポーネントで解決される。
  • そのため、アプリケーション開発者は、新バージョンの実装、テスト、デプロイの工数を負担する。

基本的な対処に

  • アプリケーションの依存関係と基礎となるコンポーネントの依存関係の識別と追跡
  • コンポーネントのセキュリティを追跡するための監視プロセス
  • コンポーネントのパッチやリリース頻度、許容されるライセンスモデルを考慮したソフトウェア開発プロセスとポリシーの確立
  • これらにより、コンポーネントプロバイダーがコード内の脆弱性に対処する際に、迅速に対応できる。

AWS WAFで何ができるか

  • アプリケーションで使用していないコンポーネントの機能に対するHTTPリクエストをフィルタリングおよびブロックによる、攻撃面の削減
  • 例)HTTPリクエストを直接/間接的にアセンブルするコードへのアクセスのブロックする
  • String-matchコンディションを用いたURI検査(例えば、”/includes/”の制限)
  • アプリケーションでサードパーティのコンポーネントを使用しているが機能のサブセットのみを使用する場合は、同様のAWS WAF条件を使って、使用しないコンポーネントの機能への公開URLパスをブロックする。

その他にできること

  • ペネトレーションテスト。マーケットプレイスで買える。申請が必要だが、一部事前承認を受けているサービスでは申請が不要なこともある。(マーケットプレイス上のマークで識別可能)
  • 展開とテストのプロセスに統合して、潜在的な脆弱性を検出するとともに、展開されたパッチが対象となるアプリケーションの問題を適切に緩和する。

A10 – Underprotected APIs

2017年版の新カテゴリ
特定アプリケーションの欠陥パターンではなく、潜在的な攻撃ターゲットを示す

  • UIを持たないアプリケーションは増えてきている。(UI/API両方使えるサービスも同様)
  • 攻撃ベクトルは、A1-A9までと変わらないことが多い。
  • APIはプログラムからのアクセスのために設計されているので、セキュリティテストでは、追加の考慮事項がある。
  • APIは、(Rest,SOAP問わず)複雑なデータ構造での動作とより広い範囲の要求頻度と入力値を使用するように設計される事が多い。

AWS WAFでできること

  • A1-A9での対応と変わらないが、対APIとして追加でできる事がある。
  • 強化が必要な主要コンポーネントは、プロトコルパーサ自体(XML,YAML,JSON 等)
  • パーサーの欠陥を悪用しようとする特定の入力パターンをString-match Condition、またはByte-Size Conditionを使用して、そのような要求パターンをブロックする。

旧OWASP Top10 A10 – Unvalidated Redirects and Forwards

リダイレクトや転送要求を検証しない場合、悪意のある当事者が正当なドメインを使用してユーザーを不要な宛先に誘導する可能性がある。

  • 例)短縮URLを生成する機能がある場合、URLジェネレータがターゲットドメインを検証しないことで、悪意を持ったユーザーが正式サービス上から悪意のあるサイトへの短縮URLを発行できる。

AWS WAFでできること

  • まず、アプリケーションでリダイレクトとフォワードがどこで発生するのかを理解する(リクエストパターン等)
  • アプリケーションが使用する、サードパーティコンポーネントも同様にチェックする。
  • エンドユーザからのHTTPリクエストに応じてリダイレクトと転送が生成された場合は、AWS WAFでリクエストをQUERY_STRINGやURIに対するString-Match Conditionでフィルタリングする(リダイレクト/転送の目的で信頼されているドメインのホワイトリストを維持する)

Cloud Formationテンプレート

Web ACLと、このドキュメントで推奨されている条件タイプとルールを含むAWS CloudFormationテンプレートが用意されています。
今回は、解説を割愛します。

所感

AWS WAFの設定は基本的にDIYですが、実装のポイントをOWASP Top10になぞらえて、その対応を解説する良いヒント集だと思いました。
AWS WAFを設定する人は、一読するのがいいですね。
外部にAWS WAFの設定を依頼する人は、アプリケーションの特性をよく理解している人を立てて、設定する人とよくコミュニケーションを取ることが重要だとわかります。
セキュリティ製品の真価を発揮するには、保護する対象のサービスへの理解とサービスへの適合化が必要なのは、どんなソリューションにも共通しますね。

今日はここまでです。
お疲れ様でした。

続きを読む

CloudFormationウォークスルーにYAMLで入門してみる

AWSリソースの構築を自動化できるCloud Formationについて学んで見ます.

まずは既存のVPC内にSG切ってインスタンス立てて…ぐらいができればいいんだけど…と思っていたのですが、

ウォークスルー: スタックの更新

がある程度まとまってたので、とりあえず走らせて見て、何が起きてるかを学んでくことにしようと思います.

下準備: JSON => YAMLへの変換

このウォークスルーではスタック(CloudFormationで構築されるひとかたまり)を作成したのち、それを更新していく…というチュートリアルですが、一番上のセクション「単純なアプリケーション」に下地となるテンプレートが記載されているのでそれをコピーして進める…

前にこれをJSONからYAMLへ変換します (JSONしんどいため&コメントつけられるようにするため)

AWS CloudFormation Template FlipというツールがAWSから公開されているので、pipでインストールします.

pip install cfn-flip

次に下地となる、「単純なアプリケーション」セクションの一番下にある、長ーいJSONをwalkthrough01.jsonなど適当な名前でローカルに保存して、下記のコマンドを実行します.

cfn-flip -c walkthrough01.json walkthrough01.yaml

するとYAMLへ変換されたファイルが生成されます.

この際、通常のJSON => YAMLの変換だけでなく、組み込み関数宣言部Fn::が短縮形!に変換されます. (3文字減るだけですが…ドキュメントをでは短縮形が併記されています)
また、-c, --cleanオプションをつけると、Fn::Join からFn::Subへと書き換えてくれます. 文字列を連結する際[]で括らなくても良くなるようで、見やすかったのでこちらで進めていきます.

(最初は関数って何してんのと思ったのですが、ユーザーの入力受け取ったり文字列を結合したりできる、といった感じなのでひとまずそのまま進めれば大丈夫です. )

生成されたテンプレートはこんな感じです(長いですねー…)

walkthrough01.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS CloudFormation Sample Template: Sample template that can be used
  to test EC2 updates. **WARNING** This template creates an Amazon Ec2 Instance. You
  will be billed for the AWS resources used if you create a stack from this template.'
Parameters:
  InstanceType:
    Description: WebServer EC2 instance type
    Type: String
    Default: m1.small
    AllowedValues:
      - t1.micro
      - t2.micro
      - t2.small
      - t2.medium
      - m1.small
      - m1.medium
      - m1.large
      - m1.xlarge
      - m2.xlarge
      - m2.2xlarge
      - m2.4xlarge
      - m3.medium
      - m3.large
      - m3.xlarge
      - m3.2xlarge
      - c1.medium
      - c1.xlarge
      - c3.large
      - c3.xlarge
      - c3.2xlarge
      - c3.4xlarge
      - c3.8xlarge
      - g2.2xlarge
      - r3.large
      - r3.xlarge
      - r3.2xlarge
      - r3.4xlarge
      - r3.8xlarge
      - i2.xlarge
      - i2.2xlarge
      - i2.4xlarge
      - i2.8xlarge
      - hi1.4xlarge
      - hs1.8xlarge
      - cr1.8xlarge
      - cc2.8xlarge
      - cg1.4xlarge
    ConstraintDescription: must be a valid EC2 instance type.
Mappings:
  AWSInstanceType2Arch:
    t1.micro:
      Arch: PV64
    t2.micro:
      Arch: HVM64
    t2.small:
      Arch: HVM64
    t2.medium:
      Arch: HVM64
    m1.small:
      Arch: PV64
    m1.medium:
      Arch: PV64
    m1.large:
      Arch: PV64
    m1.xlarge:
      Arch: PV64
    m2.xlarge:
      Arch: PV64
    m2.2xlarge:
      Arch: PV64
    m2.4xlarge:
      Arch: PV64
    m3.medium:
      Arch: HVM64
    m3.large:
      Arch: HVM64
    m3.xlarge:
      Arch: HVM64
    m3.2xlarge:
      Arch: HVM64
    c1.medium:
      Arch: PV64
    c1.xlarge:
      Arch: PV64
    c3.large:
      Arch: HVM64
    c3.xlarge:
      Arch: HVM64
    c3.2xlarge:
      Arch: HVM64
    c3.4xlarge:
      Arch: HVM64
    c3.8xlarge:
      Arch: HVM64
    g2.2xlarge:
      Arch: HVMG2
    r3.large:
      Arch: HVM64
    r3.xlarge:
      Arch: HVM64
    r3.2xlarge:
      Arch: HVM64
    r3.4xlarge:
      Arch: HVM64
    r3.8xlarge:
      Arch: HVM64
    i2.xlarge:
      Arch: HVM64
    i2.2xlarge:
      Arch: HVM64
    i2.4xlarge:
      Arch: HVM64
    i2.8xlarge:
      Arch: HVM64
    hi1.4xlarge:
      Arch: HVM64
    hs1.8xlarge:
      Arch: HVM64
    cr1.8xlarge:
      Arch: HVM64
    cc2.8xlarge:
      Arch: HVM64
  AWSRegionArch2AMI:
    us-east-1:
      PV64: ami-50842d38
      HVM64: ami-08842d60
      HVMG2: ami-3a329952
    us-west-2:
      PV64: ami-af86c69f
      HVM64: ami-8786c6b7
      HVMG2: ami-47296a77
    us-west-1:
      PV64: ami-c7a8a182
      HVM64: ami-cfa8a18a
      HVMG2: ami-331b1376
    eu-west-1:
      PV64: ami-aa8f28dd
      HVM64: ami-748e2903
      HVMG2: ami-00913777
    ap-southeast-1:
      PV64: ami-20e1c572
      HVM64: ami-d6e1c584
      HVMG2: ami-fabe9aa8
    ap-northeast-1:
      PV64: ami-21072820
      HVM64: ami-35072834
      HVMG2: ami-5dd1ff5c
    ap-southeast-2:
      PV64: ami-8b4724b1
      HVM64: ami-fd4724c7
      HVMG2: ami-e98ae9d3
    sa-east-1:
      PV64: ami-9d6cc680
      HVM64: ami-956cc688
      HVMG2: NOT_SUPPORTED
    cn-north-1:
      PV64: ami-a857c591
      HVM64: ami-ac57c595
      HVMG2: NOT_SUPPORTED
    eu-central-1:
      PV64: ami-a03503bd
      HVM64: ami-b43503a9
      HVMG2: ami-b03503ad
Resources:
  WebServerInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Install a simple PHP application
      AWS::CloudFormation::Init:
        config:
          packages:
            yum:
              httpd: []
              php: []
          files:
            /var/www/html/index.php:
              content: '<?php

                echo ''<h1>AWS CloudFormation sample PHP application</h1>'';

                ?>

                '
              mode: '000644'
              owner: apache
              group: apache
            /etc/cfn/cfn-hup.conf:
              content: !Sub '[main]

                stack=${AWS::StackId}

                region=${AWS::Region}

                '
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Sub '[cfn-auto-reloader-hook]

                triggers=post.update

                path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init

                action=/opt/aws/bin/cfn-init -s ${AWS::StackId} -r WebServerInstance  --region     ${AWS::Region}

                runas=root

                '
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
    Properties:
      ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
          !Ref 'InstanceType', Arch]]
      InstanceType: !Ref 'InstanceType'
      SecurityGroups:
        - !Ref 'WebServerSecurityGroup'
      UserData: !Base64
        Fn::Sub: '#!/bin/bash -xe

          yum install -y aws-cfn-bootstrap

          # Install the files and packages from the metadata

          /opt/aws/bin/cfn-init -v          --stack ${AWS::StackName}         --resource
          WebServerInstance          --region ${AWS::Region}

          # Start up the cfn-hup daemon to listen for changes to the Web Server metadata

          /opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''

          # Signal the status from cfn-init

          /opt/aws/bin/cfn-signal -e $?          --stack ${AWS::StackName}         --resource
          WebServerInstance          --region ${AWS::Region}

          '
    CreationPolicy:
      ResourceSignal:
        Timeout: PT5M
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable HTTP access via port 80
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0
Outputs:
  WebsiteURL:
    Description: Application URL
    Value: !Sub 'http://${WebServerInstance.PublicDnsName}'

起動

ともあれ、生成されたテンプレートを元に「初期スタックの作成」を参考にしてCloudFormationをマネジメントコンソールから起動して見ます.
するとデフォルトVPC内にインスタンスが立ち上がっていることが確認できるかと思います.
CloudFormationコンソールの[Output]タブに表示されたアドレスへアクセスすると、AWS CloudFormation sample PHP applicationというメッセージが表示されているかと思います.

テンプレートを読む

一旦起動し終わったところで、どんな構造になっているか読んでいきたいと思います.
長いようでやってることはシンプルなテンプレートなので、そんに項目数は多くないかと思います.

YAMLの最上位には

  • AWSTemplateFormatVersion
  • Description
  • Parameters
  • Mappings
  • Resources
  • Outputs

が並んでいました. それぞれ、

項目 内容
AWSTemplateFormatVersion テンプレートフォーマットのバージョン、基本固定?
Description テンプレートの説明文
Parameters CloudFormation実行時にユーザーが与えるパラメーター
Mappings 与えらた条件に応じて変更される値の定義
Resources 起動されるAWSリソースの設定
Outputs 起動完了後に出力されるメッセージ

といった感じです. Version, Description, Outputは簡単そうなので脇に置いて、残りについて見ていきたいと思います.

Parameters

実行するユーザーが与えるパラメータです.
テンプレートでは下記の通り.

Parameters:
  InstanceType:                               # 項目名
    Description: WebServer EC2 instance type  # 説明
    Type: String                              # 入力される値の型
    Default: m1.small                         # デフォルト値
    AllowedValues:                            # 受け付ける値
      - t1.micro
      # ...
      # 中略
      # ...
    ConstraintDescription: must be ...        # 入力値違反時のエラーメッセージ

これが反映されるとCloudFormation起動画面ではこんな感じになります.

Screen Shot 2017-09-07 at 11.42.15.png

Prameters 以下にセレクトボックスができているのがわかります.
AllowedValueの他にも、AllowedPatternで正規表現で入力値を規定することができます(SSHアクセスを許可するIPアドレスレンジを入力させたいときなど)

Mappings

CloudFormationが実行される条件に応じて、この次のResourcesを変更したいときに利用するための値のマッピングです.

Mappings:
  AWSInstanceType2Arch:
    t1.micro:
      Arch: PV64
      # ...
      # 中略
      # ...
  AWSRegionArch2AMI:
    us-east-1:
      PV64: ami-50842d38
      HVM64: ami-08842d60
      HVMG2: ami-3a329952
      # ...
      # 後略
      # ...

今回はParameterでユーザーから与えられたインスタンスタイプに応じて仮想化の方式を決定したり、実行されたリージョン(+インスタンスタイプ)に応じて選択するAMIを決定しています.

(ParameterとMappingがこのサンプルテンプレートを長くしちゃってる原因、ですね…)

Resources

CloudFormationの本体部分になる、起動されるリソースの設定です.

Resources:
  WebServerInstance:
  # ...
  # 中略
  # ...
  WebServerSecurityGroup:
  # ...
  # 後略
  # ...

と今回はEC2インスタンスとセキュリティグループの2つになります.
ここではEC2の設定について、読んでいこうと思います.
(VPCとサブネットは指定しないとデフォルトが選択される模様)

EC2

ここが一番重たい…気がします.

  WebServerInstance:          # 項目名
    Type: AWS::EC2::Instance    # リソースタイプ
    Metadata:                   # 追加コメントの記載や起動スクリプトの定義や
      # ...
      # 中略
      # ...
    Properties:                 # EC2自身の起動パラメータ
      # ...
      # 中略
      # ...
    CreationPolicy:             # CloudFormation側とのやりとりの設定
      # ...
      # 後略
      # ...

と大きく分けて4つ、要素があります.
Typeはそのままであり、CreationPolicyはこのテンプレートではシンプルなので、ここではMetadataとPropatiesの2つについて見ていきます.

Metadata
    Metadata:
      Comment: Install a simple PHP application
      AWS::CloudFormation::Init:  # 起動スクリプトの設定
        config:
          packages:  # 利用するパッケージ、今回はyumでapacheとphpをインストール
            yum:
              httpd: []
              php: []
          files:     # 設置する設定ファイル類
            /var/www/html/index.php:  # デモ用phpファイル
              content:  # 略
              mode: '000644'
              owner: apache
              group: apache
            /etc/cfn/cfn-hup.conf:  # Stackを通じてEC2のアップデートをするための設定
              content: !Sub '[main]  # !Subで文字列の結合

                stack=${AWS::StackId}  # Stack IDの取得

                region=${AWS::Region}  # リージョンの取得

                '
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:  # 同じくアップデートのための設定
              content:  # 略
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf

ドキュメントにあるように、実装の詳細をコメントとして含めたり、AWS::CloudFormation::Initを利用した起動スクリプトの設定ができる箇所になります。

AWS::CloudFormation::Initではyumなどによるインストール、設定ファイルの配置、起動するサービスの選択ができます. 今回はApacheと、CloudFormationスタックを通してEC2の方に変更を反映するためのエージェント(cfn-hup)の設定をしている形になります. ウォークスルーを進めると早速index.phpファイルを変更しスタックをアップデートするのですが、その際CloudFormation側からインスタンス内のindex.phpファイルを触りに行くのにこれを通じて行う、ようです.

参考: cfn-hup

また!Subという組み込み関数を用いて文字列の連結を行なっています.

Properties

EC2本体の設定値やスクリプトの実行を行ないます.

    Properties:
      ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
          !Ref 'InstanceType', Arch]]  # AMIのID取得、Mappingsで設定した値を利用
      InstanceType: !Ref 'InstanceType'
      SecurityGroups:
        - !Ref 'WebServerSecurityGroup'
      UserData: !Base64  # スクリプト実行
        Fn::Sub: '#!/bin/bash -xe

          yum install -y aws-cfn-bootstrap

          # Metadataからのファイルとパッケージのインストール

          /opt/aws/bin/cfn-init -v          --stack ${AWS::StackName}         --resource
          WebServerInstance          --region ${AWS::Region}

          # cfn-hup daemon の起動とStackの変更のリッスン開始

          /opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''

          # インスタンスの状態をCloudFormation側へ通知

          /opt/aws/bin/cfn-signal -e $?          --stack ${AWS::StackName}         --resource
          WebServerInstance          --region ${AWS::Region}

          '

!RefでParameterで指定された値を受け取り、!FindInMapでMappingで設定した値を取得し、EC2の設定パラメータを指定しています. (こうすることでどのリージョンでも起動可能)

また、UserDataにCloudFormation起動時に実行するスクリプトを渡しています. Metadataで記載したファイルやパッケージもここで読み込まれ、インストールが実行されます.

(UserData側に全てインストール項目記載しても良さそうですが、後からスタックを通しての変更ができない、テンプレートの見通しが悪くなる等々デメリットがあるのだと思います.

(追記)改行など整えたもの

自動変換の際、改行を表現するのに1行あける、ということがされていましたが、YAMLでは|が置かれた後の属性は改行を評価する、ということになっているそうです.
これを用いるともう少しスッキリ書けます

リソースのプロパティの追加のSSHによるアクセス追加の部分を加えてあります.

AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS CloudFormation Sample Template: Sample template that can be used
  to test EC2 updates. **WARNING** This template creates an Amazon Ec2 Instance. You
  will be billed for the AWS resources used if you create a stack from this template.'
Parameters:
  InstanceType:
    Description: WebServer EC2 instance type
    Type: String
    Default: m1.small
    AllowedValues:
      - t1.micro
      - t2.micro
      - t2.small
      - t2.medium
      - m1.small
      - m1.medium
      - m1.large
      - m1.xlarge
      - m2.xlarge
      - m2.2xlarge
      - m2.4xlarge
      - m3.medium
      - m3.large
      - m3.xlarge
      - m3.2xlarge
      - c1.medium
      - c1.xlarge
      - c3.large
      - c3.xlarge
      - c3.2xlarge
      - c3.4xlarge
      - c3.8xlarge
      - g2.2xlarge
      - r3.large
      - r3.xlarge
      - r3.2xlarge
      - r3.4xlarge
      - r3.8xlarge
      - i2.xlarge
      - i2.2xlarge
      - i2.4xlarge
      - i2.8xlarge
      - hi1.4xlarge
      - hs1.8xlarge
      - cr1.8xlarge
      - cc2.8xlarge
      - cg1.4xlarge
    ConstraintDescription: must be a valid EC2 instance type.
  KeyName: 
    Description: Name of an existing Amazon EC2 key pair for SSH access
    Type: AWS::EC2::KeyPair::KeyName
  SSHLocation:
    Description: The IP address range that can be used to SSH to the EC2 instances
    Type: String
    MinLength: 9
    MaxLength: 18
    Default: 192.168.0.1/32
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
    ConstraintDescription: must be a valid IP CIDR range of the form x.x.x.x/x.  
Mappings:
  AWSInstanceType2Arch:
    t1.micro:
      Arch: PV64
    t2.micro:
      Arch: HVM64
    t2.small:
      Arch: HVM64
    t2.medium:
      Arch: HVM64
    m1.small:
      Arch: PV64
    m1.medium:
      Arch: PV64
    m1.large:
      Arch: PV64
    m1.xlarge:
      Arch: PV64
    m2.xlarge:
      Arch: PV64
    m2.2xlarge:
      Arch: PV64
    m2.4xlarge:
      Arch: PV64
    m3.medium:
      Arch: HVM64
    m3.large:
      Arch: HVM64
    m3.xlarge:
      Arch: HVM64
    m3.2xlarge:
      Arch: HVM64
    c1.medium:
      Arch: PV64
    c1.xlarge:
      Arch: PV64
    c3.large:
      Arch: HVM64
    c3.xlarge:
      Arch: HVM64
    c3.2xlarge:
      Arch: HVM64
    c3.4xlarge:
      Arch: HVM64
    c3.8xlarge:
      Arch: HVM64
    g2.2xlarge:
      Arch: HVMG2
    r3.large:
      Arch: HVM64
    r3.xlarge:
      Arch: HVM64
    r3.2xlarge:
      Arch: HVM64
    r3.4xlarge:
      Arch: HVM64
    r3.8xlarge:
      Arch: HVM64
    i2.xlarge:
      Arch: HVM64
    i2.2xlarge:
      Arch: HVM64
    i2.4xlarge:
      Arch: HVM64
    i2.8xlarge:
      Arch: HVM64
    hi1.4xlarge:
      Arch: HVM64
    hs1.8xlarge:
      Arch: HVM64
    cr1.8xlarge:
      Arch: HVM64
    cc2.8xlarge:
      Arch: HVM64
  AWSRegionArch2AMI:
    us-east-1:
      PV64: ami-50842d38
      HVM64: ami-08842d60
      HVMG2: ami-3a329952
    us-west-2:
      PV64: ami-af86c69f
      HVM64: ami-8786c6b7
      HVMG2: ami-47296a77
    us-west-1:
      PV64: ami-c7a8a182
      HVM64: ami-cfa8a18a
      HVMG2: ami-331b1376
    eu-west-1:
      PV64: ami-aa8f28dd
      HVM64: ami-748e2903
      HVMG2: ami-00913777
    ap-southeast-1:
      PV64: ami-20e1c572
      HVM64: ami-d6e1c584
      HVMG2: ami-fabe9aa8
    ap-northeast-1:
      PV64: ami-21072820
      HVM64: ami-35072834
      HVMG2: ami-5dd1ff5c
    ap-southeast-2:
      PV64: ami-8b4724b1
      HVM64: ami-fd4724c7
      HVMG2: ami-e98ae9d3
    sa-east-1:
      PV64: ami-9d6cc680
      HVM64: ami-956cc688
      HVMG2: NOT_SUPPORTED
    cn-north-1:
      PV64: ami-a857c591
      HVM64: ami-ac57c595
      HVMG2: NOT_SUPPORTED
    eu-central-1:
      PV64: ami-a03503bd
      HVM64: ami-b43503a9
      HVMG2: ami-b03503ad
Resources:
  WebServerInstance:
    Type: AWS::EC2::Instance
    Metadata:
      Comment: Install a simple PHP application
      AWS::CloudFormation::Init:
        config:
          packages:
            yum:
              httpd: []
              php: []
          files:
            /var/www/html/index.php:
              content: |
                <?php
                echo '<h1>AWS CloudFormation sample PHP application</h1>';
                echo 'Updated virsions via UpdateStack';
                ?>
              mode: '000644'
              owner: apache
              group: apache
            /etc/cfn/cfn-hup.conf:
              content: !Sub |
                [main]
                stack=${AWS::StackId}
                region=${AWS::Region}
              mode: '000400'
              owner: root
              group: root
            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.WebServerInstance.Metadata.AWS::CloudFormation::Init
                action=/opt/aws/bin/cfn-init -s ${AWS::StackId} -r WebServerInstance --region ${AWS::Region}
                runas=root
          services:
            sysvinit:
              httpd:
                enabled: 'true'
                ensureRunning: 'true'
              cfn-hup:
                enabled: 'true'
                ensureRunning: 'true'
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
    Properties:
      ImageId: !FindInMap [AWSRegionArch2AMI, !Ref 'AWS::Region', !FindInMap [AWSInstanceType2Arch,
          !Ref 'InstanceType', Arch]]
      InstanceType: !Ref 'InstanceType'
      SecurityGroups:
        - !Ref 'WebServerSecurityGroup'
      KeyName: !Ref 'KeyName'
      UserData: !Base64
        Fn::Sub: |
          #!/bin/bash -xe
          yum install -y aws-cfn-bootstrap
          # Install the files and packages from the metadata
          /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource WebServerInstance --region ${AWS::Region}
          # Start up the cfn-hup daemon to listen for changes to the Web Server metadata
          /opt/aws/bin/cfn-hup || error_exit ''Failed to start cfn-hup''
          # Signal the status from cfn-init
          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerInstance --region ${AWS::Region}
    CreationPolicy:
      ResourceSignal:
        Timeout: PT5M
  WebServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable HTTP access via port 80
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '80'
          ToPort: '80'
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: !Ref 'SSHLocation'
Outputs:
  WebsiteURL:
    Description: Application URL
    Value: !Sub 'http://${WebServerInstance.PublicDnsName}'

phpのところで、''で括られていた箇所を'に直していますが、これもYAMLの規定で、文字列中でシングルクォーテーションのエスケープには2つ連ねる、というのを変換のスクリプトが利用していたからでした.


なかなか手が出せなかったCloudFormationだったのですが、ウォークスルーの一番最初の部分について、ドキュメント読み読み進めて見ました.

ウォークスルーでいきなりこの長さはちょっとしんどいですね…

続きを読む

AnsibleでALBを作成してみる 〜その①〜

はじめに

Ansibleを使ってALBを起動する方法について調べたので、備忘録的に記事にまとめたいと思います。今回はALBの作成とターゲットグループの設定まで行いたいと思います。

1. 環境セットアップ

はじめにAWS関連のモジュール実行に必要なパッケージ、boto3をインストールします。

sudo pip install boto3

2. プレイブックの作成

下記のディレクトリ構成で、プレイブックを作成していきます。なお、ALBに指定するVPC及びサブネット、セキュリティグループは作成済みとします。

ansible
├── roles
│   └── aws
│       ├── tasks
│       │   ├── main.yml
│       │   ├── create_alb.yml
│       │   ├── create_tg.yml
│       └── vars
│           └── main.yml
├── hosts
├── alb.yml

 ①. ALB作成タスクの定義

ansible/roles/aws/tasks配下に、AWSに対してALB作成を指示するタスクを記載したYamlファイル(create_alb.yml)を作成します。各タスクで実行する内容は下記の通りです。

  1. 分散先サブネットIDの取得
  2. セキュリティグループIDの取得
  3. ALBの起動
ansible/roles/alb/tasks/create_alb.yml
---
# サブネットIDの取得タスク
## ec2_vpc_subnet_factsモジュールを使用
- name: "Get Subnet ID"
  ec2_vpc_subnet_facts:
    region: "{{ alb.aws_region }}"
    filters:
      "tag:Name": "{{ item.1 }}"
  with_subelements:
    - "{{ alb.subnet_grp }}"
    - subnets
  register: subnet_fact

# セキュリティグループIDの取得
## ec2_group_factsモジュールを使用
- name: "Get Sec Grp ID"
  ec2_group_facts:
    region: "{{ alb.aws_region }}"
    filters:
      group-name: "{{ alb.security_groups }}"
  register: group_fact

# ALBの起動
## AWS-CLIで起動
- name: "Create ALB"
  command: aws elbv2 create-load-balancer --name "{{ alb.alb_name }}" --subnets "{{ subnet_fact.results[0].subnets[0].id }}" "{{ subnet_fact.results[1].subnets[0].id }}" --security-groups "{{ group_fact.security_groups[0].group_id }}"

 ②. ターゲットグループ作成タスクの定義

ansible/roles/aws/tasks配下に、AWSに対してターゲットグループ作成を指示するタスクを記載したYamlファイル(create_tg.yml)を作成します。各タスクで実行する内容は下記の通りです。

  1. VPC IDの取得
  2. ターゲットグループの作成
ansible/roles/alb/tasks/create_tg.yml
---
# VPC IDの取得タスク
## ec2_vpc_net_factsモジュールを使用
- name: "Get VPC ID"
  ec2_vpc_net_facts:
    region: "{{ alb.aws_region }}"
    filters:
      "tag:Name": "{{ alb.vpc_name }}"
  register: vpc_net_fact

# ターゲットグループの作成
## AWS-CLIで起動
- name: "Create Target Group"
  command: aws elbv2 create-target-group --name "{{ alb.tg_name }}" --protocol HTTP --port 80 --vpc-id "{{ vpc_net_fact.vpcs[0].id }}"

 ③. main.yamlを作成

ansible/roles/alb/tasks/create_tg.yml
---
###  ALB作成タスクを指定
- name: "create alb"
  include: create_alb.yml

###  ターゲットグループ作成タスクを指定
- name: "create target-group"
  include: create_tg.yml

 ④. 変数の定義

ansible/roles/aws/vars配下に、タスク実行に必要な各種パラメータを変数指定したyamlファイル(main.yml)を作成します。

ansible/roles/aws/vars/main.yml
---
alb:
  aws_region: "ap-northeast-1"
  vpc_name: "AnsibleVPC"
  subnet_grp:
    - name: "alb-subnet-group"
      subnets:
        - subnet-1
        - subnet-2
  security_groups: "alb-sec-grp"
  alb_name: "Ansible-ALB"
  tg_name: "Ansible-TG"
  tg_grp:
    - name: "ec2-target-group"

 ⑤. ホストファイルの作成

AWSに対してタスクを実行するモジュールは全てローカル上で実行するため、localhostを指定したhostsファイルを用意します。

ansible/hosts
[localhost]
127.0.0.1

 ④. Playbookの作成

最後にホスト情報、ロールを指定したPlaybookをansible/配下に作成します。

ansible/alb.yml
- hosts: localhost
  roles:
    - aws

3. Playbookの実行

下記のコマンドでPlaybook(rds.yml)を指定し、実行します。

$ ansible-playbook alb.yml
 [WARNING]: Host file not found: /usr/local/etc/ansible/hosts

 [WARNING]: provided hosts list is empty, only localhost is available


PLAY [localhost] ***************************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [aws : Get Subnet ID] *****************************************************
ok: [localhost] => (item=({u'name': u'alb-subnet-group'}, u'subnet-1'))
ok: [localhost] => (item=({u'name': u'alb-subnet-group'}, u'subnet-2'))

TASK [aws : Get Sec Grp ID] ****************************************************
ok: [localhost]

TASK [aws : Create ALB] ********************************************************
changed: [localhost]

TASK [aws : Get VPC ID] ********************************************************
ok: [localhost]

TASK [aws : Create Target Group] ***********************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=6    changed=2    unreachable=0    failed=0

4. まとめ

必須となるパラメータを全てAnsibleで取得し、ALB及びターゲットグループの作成を行いました。現状ではALBの作成に必要なAnsibleモジュールがリリースされていないため、 AWS CLIをPlaybookに書き込む形となります。次回はターゲットグループへのインスタンス追加及びALBへのターゲットグループ設定を試してみたいと思います。

続きを読む