EPPlusで作成したOpenXmlの構成

前回はOpenXmlをラップするEPPlusでExcel作成しました。 今回は作成されたOpenXMLの中身を確認してみます。

tech.sanwasystem.com

フォルダ構成~セル値

作成されたダウンロードファイル名.xlsxの拡張子を変更しダウンロードファイル名.zipにし解凍します。
フォルダ構成は以下の通りになっている事がわかります。

ダウンロードファイル名
├─[Content_Types].xml
├─xl
│ │ sharedStrings.xml
│ │ styles.xml
│ │ workbook.xml
│ ├─charts
│ │ chart1.xml
│ ├─drawings
│ │ │ drawing1.xml
│ │ └─rels
│ │ drawing1.xml.rels
│ ├─media
│ │ 1sanwa.png
│ ├─worksheets
│ │ │ sheet1.xml
│ │ └─
rels
│ │ sheet1.xml.rels
│ └─rels
│ workbook.xml.rels
└─
rels
.rels

[Content_Types].xmlは本Excelドキュメントを構成するファイルのContentTypeとパスが指定されています。
それ以外のxml

workboox.xmlワークブックの定義。ワークブックを構成するシートの定義
sheet1.xmlシートの各カラムの値や計算式、ページフッタヘッダの定義
chart1.xmlグラフ・画像の定義
sharedStrings.xmlセル値の定義
style.xml書式設定の定義

となっています。

ワークシートワークシート名XMLsheet1.xmlなのでこのファイルにセル情報がどのように定義されているのか確認します。
以下ワークシートの5行目と6行目の抜粋です。
f:id:ihisa:20151112111400p:plain

    <row r="5" >
      <c r="A5" s="4" t="s">
        <v>1</v>
      </c>
      <c r="B5" s="4" t="s">
        <v>2</v>
      </c>
    </row>
    <row r="6" >
      <c r="A6" s="3" t="s">
        <v>5</v>
      </c>
      <c r="B6" s="5" >
        <v>100</v>
      </c>
      <c r="C6" s="5" >
        <v>25</v>
      </c>
      <c r="D6" s="3">
        <f>B6 * C6</f>
      </c>
      <c r="E6" s="1" />
    </row>

A5のセルに着目してみていきます。
A5には<c r="A5" s="4" t="s"><v>1</v></c>と定義されています。
sはstyle.xmlへの参照を表し、tはsharedStrings.xmlへの参照、vはセル値を表します。
ワークシート上のA5には商品という文字と、背景が青色・文字が白色というスタイルが定義されています。
style.xmlは以下の通りになっています。

<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
  <numFmts count="0" />
  <fonts count="4">
    <font>
      <sz val="11" />
      <name val="Calibri" />
    </font>
    <font>
      <sz val="10" />
      <name val="MS Pゴシック" />
    </font>
    <font>
      <b />
      <sz val="10" />
      <color rgb="FFFF0000" tint="0" />
      <name val="MS Pゴシック" />
    </font>
    <font>
      <sz val="10" />
      <color rgb="FFFFFFFF" tint="0" />
      <name val="MS Pゴシック" />
    </font>
  </fonts>
  <fills count="3">
    <fill>
      <patternFill patternType="none" />
    </fill>
    <fill>
      <patternFill patternType="gray125" />
    </fill>
    <fill>
      <patternFill patternType="solid">
        <fgColor rgb="FF0000FF" tint="0" />
      </patternFill>
    </fill>
  </fills>
  <borders count="2">
    <border>
      <left />
      <right />
      <top />
      <bottom />
      <diagonal />
    </border>
    <border>
      <left style="thin" />
      <right style="thin" />
      <top style="thin" />
      <bottom style="thin" />
      <diagonal />
    </border>
  </borders>
  <cellStyleXfs count="1">
    <xf fontId="0" />
  </cellStyleXfs>
  <cellXfs count="6">
    <xf fontId="0" applyFont="1" xfId="0" />
    <xf fontId="1" applyFont="1" xfId="0" />
    <xf fontId="2" applyFont="1" xfId="0">
      <alignment horizontal="center" />
    </xf>
    <xf fontId="1" applyFont="1" borderId="1" applyBorder="1" xfId="0" />
    <xf fontId="3" applyFont="1" fillId="2" applyFill="1" borderId="1" applyBorder="1" xfId="0">
      <alignment horizontal="center" />
    </xf>
    <xf numFmtId="2" applyNumberFormat="1" fontId="1" applyFont="1" borderId="1" applyBorder="1" xfId="0" />
  </cellXfs>
  <cellStyles count="1">
    <cellStyle name="Normal" xfId="0" builtinId="0" />
  </cellStyles>
  <dxfs count="0" />
</styleSheet>

<c r="A5" s="4" t="s"><v>1</v></c>s="4"style.xml<cellXfs count="6">タグ配下のxfタグの5番目を指します(0始まり)
つまりここです。

<xf fontId="3" applyFont="1" fillId="2" applyFill="1" borderId="1" applyBorder="1" xfId="0">
  <alignment horizontal="center" />
</xf>

fontId="3"は同じstyle.xml<fonts count="4">タグ配下の<font>タグの4番目を指します。 つまりここです。

<font>
  <sz val="10" />
  <color rgb="FFFFFFFF" tint="0" />
  <name val="MS Pゴシック" />
</font>

同様にfillId="2"<fills count="3">タグ配下の<fill>タグの3番目

<fill>
  <patternFill patternType="solid">
    <fgColor rgb="FF0000FF" tint="0" />
  </patternFill>
</fill>

※fgColorの指定はARGB

borderId="1"<borders count="2">タグ配下の<border>タグの2番目

<border>
  <left style="thin">
    <color auto="1"/>
  </left>
  <right style="thin">
    <color auto="1"/>
  </right>
  <top style="thin">
    <color auto="1"/>
  </top>
  <bottom style="thin">
    <color auto="1"/>
  </bottom>
  <diagonal/>
</border>

となります。 Excelをみたまんまの定義がされています。わかりやすい!

肝心のセルの値は<c r="A5" s="4" t="s"><v>1</v></c><v>1</v>の部分なので、sharedStrings.xmlの2つめの値になります。

<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="8" uniqueCount="8">
  <si>
    <t>今日の売上!</t>
  </si>
  <si>
    <t>商品</t>
  </si>
  <si>
    <t>単価</t>
  </si>
  <si>
    <t>売上数</t>
  </si>
  <si>
    <t>合計</t>
  </si>
  <si>
    <t>りんご</t>
  </si>
  <si>
    <t>みかん</t>
  </si>
  <si>
    <t>ばなな</t>
  </si>
</sst>

.relsファイル

拡張子が.relsのファイルはファイルとXMLスキーマのリレーションが記載されています。
ワークシートワークシート名には画像とグラフが挿入されています。sheet1.xml内には

<drawing r:id="rId1" />

とidが記載されています。 sheet1.xmlと同一フォルダにある_rels/sheet1.xml.relsをみるとそのidで

<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing1.xml"/></Relationships>

という記載があることががわかります。

../drawings/drawing1.xmlを見ると画像とグラフの位置が指定されていることがわかります。またidが指定されています。

<c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:id="rId1" />
<a:blip xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:embed="rId2" cstate="print" />

またまたdrawing1.xmlと同一フォルダにある_rels/drawing1.xml.relsをみると

<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" Target="../charts/chart1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/1sanwa.png"/>

ようやくグラフと画像の本体にたどりつきました。
もちろん直接xmlを編集して保存すればExcelの内容も変わります。
EPPlusで作成したOpenXmlも通常のOpenXml同様の中身となっていることがわかりました。

なおOpenXMLでゴリゴリ書くには以下よりライブラリを作成し、参照設定すればいいです。
github.com

こちらが参考になります。
Creating basic Excel workbook with Open XML - CodeProject

実際にxlsxファイルの中身を逐一見ることは業務としてほぼないと思いますが、構成を知っておくとOpen-XML-SDKを用いる場合理解しやすいので覚えておいて損はないかと思います。
なんかよくわからんがファイルサイズが大きいときとかってありますよね、そういう場合は直接中見るほうが早い場合もありそうです。

StatusCakeでサイト監視してSlackへ通知する

こんにちは。ジョジョ4部アニメ化決定に歓喜しているおいかわです。
弊社で提供しているWebサービスは殆どがAWSへ移行しています。その場合は監視もCloudWatchで行いSlackへ通知しています。

tech.sanwasystem.com

ですが、他のサーバーで運用しているものや弊社で管理しているクライアントのサイトなどはそういう訳にも行きません。

そこで今回はサイト監視サービスのStatusCakeとSlackを連携させてサイトがダウンしてる場合などに通知させる方法をご紹介します。今回もノンプログラミングです!

StatusCakeとは

StatusCakeはWebサイトの死活監視やパフォーマンス監視なども行える無料のWebサービスです。1アカウントで複数のサイトを監視出来ます。f:id:h_oikawa2:20151104105604j:plain

では連携の準備を進めていきましょう。

StatusCakeの準備

  1. アカウント作成後、画面の「CREATE A TEST」をクリックします。StatusCakeでは監視タスクをTestと呼んでいます。

  2. まずはTestの名前と監視対象のURLを設定します。Test Typeは「HTTP/HTTP Test」を選択して下さい。 f:id:h_oikawa2:20151104114532p:plain

  3. 続けて詳細設定をします。なるべく監視間隔を短くするためにCheck Rateを5 Minsに変更しましょう。それ以外はデフォルトでOKです。 f:id:h_oikawa2:20151104170426p:plain

以上でStatusCakeの準備は完了です。StatusCakeは既に監視を始めているッ。

連携の設定(zapier)

StatusCakeとSlackの連携にはzapierを使います。zapierとはWebサービス同士をサイト上から簡単に連携させることが出来るWebサービスです。

  1. アカウント作成後、ヘッダーの「MAKE A ZAP!」をクリックします。

  2. 連携させるサービスとトリガーとなるアクションを設定します。 f:id:h_oikawa2:20151104184309p:plain
    左側が起点となるサービスです。StatusCakeを選択しActionにはNew Alertを選択します。右側に連携先のサービスとしてSlackを選択します。ActionにはSend New Messageを選択します。

  3. StatusCakeとSlackのアカウントを選択します。 f:id:h_oikawa2:20151105105423p:plain
    zapierと連携する許可を求められますので許可します。

  4. StatusCakeからの通知フィルターを作成します。 f:id:h_oikawa2:20151105110213p:plain
    ここではステータスコードが400以上の場合を対象としたフィルターを作成します。

5.Slackへの通知メッセージを設定します。
f:id:h_oikawa2:20151105110735p:plain
Insert fieldsボタンをクリックするとStatusCakeからのステータスコードやステータス、URL等を選択しメッセージに含めることが出来ます。

6.あとは名前をつけて登録完了です!

動作確認

設定した監視対象のサイトのindex.htmlを削除するなどして404や403が発生するようにしておきます。

DASHBOARDの該当のZAPのメニューから「Run」を実行します。 f:id:h_oikawa2:20151105111846p:plain

実行中ダイアログが表示されます。連携が成功したようです!
f:id:h_oikawa2:20151105120716p:plain

Slackの方を見てみましょう。
f:id:h_oikawa2:20151105121206p:plain
おおおおおおおおおお!しっかり通知されています!

今回もノンプログラミングで出来ました。StatusCake自体も色々な通知機能を備えていますが、このように簡単に連携出来るのであればSlackを使うのもありなのではないでしょうか。弊社の場合は様々なツール類からの通知をSlackにまとめています。

TOTP: Time-Based One-Time Password AlgorithmのRFC 6238を読んだ

こんにちは、AWS担当のwakです。

弊社のAWSのIAMユーザーアカウントは(もちろん)スマホアプリを使った2段階認証で保護しています。また、ルートアカウントは(もちろん)ハードウェアMFAデバイスを使った2段階認証で保護しています。しかしこのMFAデバイス、割と頻繁に認証に失敗して再同期が必要となってしまいます。月に1回~2回程度は使っているためか、こちらの記事のように

qiita.com

「再同期が出来ず、Amazonに電話で連絡」するような状態に陥ったことはないのですが、それにしてもなんでこうなるのか、また再同期とは何をしているのかとは少し疑問に思っていました。

……という前振りのもと、時刻ベースのワンタイムパスワードアルゴリズム(TOTP: Time-based One-time Password Algorithm)について定めたRFC 6238を読んだのでネタにします。

f:id:nurenezumi:20151105105639j:plain

ちゃんと我慢ができるお利口な猫

TOTPの簡単な説明

最初にシステムで決めておくものは次の通りです。これは固定値となります。

  • 何秒おきにOTP(One-Time Password)が変わるかを決めた数値X(30秒が推奨されています。またこの値をtime stepと呼びます)
  • OTPの桁数(6桁が多い)
  • ハッシュ関数アルゴリズム

そして認証者(verifier: サーバーとか)と証明者(prover: スマホアプリとかハードウェアMFAデバイスとかのトークンを持っている人)は、これに加えて

  • 最初に決めた共通鍵(ただの乱数列)。QRコードに含まれていたり、物理MFAデバイスのラベルに書いてあったりする。最低128ビットの長さが必要

をともに持っておきます。ただ、この共通鍵を認証時にそのまま送信してしまうとただの「2個目のパスワード」と同じことになってしまうので、時刻という要素を混ぜてハッシュを取り、さらに扱いやすいように情報量を適当なところまで減らして、仮に盗聴されても鍵そのものを推測されることを不可能にするわけです。

TOTPはこの4つの要素+現在時刻だけで決まります。たとえば現在時刻が2015/11/6 9:32:59だとしますと、

  1. 現在時刻をUNIX timeで取得 (1,446,769,979)
  2. 1をXで割る(48,225,665)
  3. 2と共通鍵を合わせてハッシュ関数に突っ込む
  4. 出てきた値を非負の整数だとみなして、1,000,000で割った余りを計算する*1

で6桁のOTPが出てきます。これら4つの要素と時計は認証者も証明者も同じものを持っているはずですから、同じように計算すれば必ず同じ結果が出てくるはずです。認証者はこれを付き合わせて、一致していれば共通鍵を知っている相手、つまり本物の証明者だとみなすという形で認証を行います。

許容する幅

このようにOTPを算出するには正確な時計が必須です。しかしスマートフォンは時計のズレを簡単に補正して常に正しい時刻を保つことができますが、ハードウェアMFAデバイスではそうはいきません。「一般的なクォーツ時計の誤差は1ヶ月で15-30秒程度」だそうなので(Wikipediaより)、デバイスに内蔵された時計もこの程度はズレてしまう可能性があります。

そこでRFC 6238ではOTPの許容幅と自動再同期についても触れられています。たとえばあるシステムで、18時頃のOTPが

  • 18:00:00~18:00:29 : 111111
  • 18:00:30~18:00:59 : 222222
  • 18:01:00~18:01:29 : 333333
  • 18:01:30~18:01:59 : 444444
  • 18:02:00~18:02:29 : 555555
  • 18:03:30~18:03:59 : 666666

となっていたとしましょう。そして時刻18:01:41(つまり正しいOTPは444444)の時点で、証明者から222222が送られてきたとします。

認証者は、あらかじめ「2回分前のOTPと、1回分先のOTPまでは受け付ける」のようなポリシーを定めておきます。つまり18:01:41の時点で認証が要求された場合、

  • 2回分前のOTP(222222
  • 1回分前のOTP(333333
  • 現在のOTP(444444
  • 1回分先のOTP(555555

の4個のOTPを計算し、どれか一つとでも一致すればOKとみなすわけです*2。また、その範囲から外れた111111666666は誤ったOTPだとして認証を拒否することになります。

tech.sanwasystem.com

こちらの記事の最終行にあるVerificationWindowはこの幅を指定しているわけですね。

自動再同期

さてこの場合、証明者からは2回分前の(つまり1分前の)OTPが送られてきました。これはおそらく証明者が持つトークンの時計が1分程度遅れていることを意味していると思われます。そこで認証者は、今後このトークンを使った認証を行う場合は1分前の時刻を基準にOTPを算出するように記録しておくことができます。これが自動再同期です。この仕組みが実装されているシステムでは、ポンコツな時計を内蔵したデバイスでも、前回認証成功時からのズレが一定範囲におさまっていれば問題なく利用し続けることができます。

眠らせておくと面倒かも

上述の仕組みを実装しているシステムであっても、このように認証を行うたびに同期が行われて時計のズレが補正されてゆく仕組みになっているため、トークンを長時間使わないでいると時計のズレは次第に広がり、最終的には許容幅から外れてしまいます。また自動再同期がなければ*3、いかに頻繁に利用していようとも、トークンの時計が正確な時刻から一定以上ズレた時点で認証に失敗します。こうなると再同期が必要になったり、あるいはそもそも認証が全く通らなくなったりしてしまいます。私が頻繁にAWSで再同期を求められるのはこれが理由でした。

これを防ぐには、時計が正確な(そう期待される)タイプのデバイスを選んで購入する、そもそもハードウェアMFAデバイスを諦めてスマートフォンを使う、頻繁にログインしておく*4、といった対策があり、会社ごとのポリシーによっていずれかを選ぶことになるでしょう。使わないまま金庫に長時間保存しておいたりすると、いざ必要なときに使えないという事態に陥りかねませんので、ここはAWSに限らず、ハードウェアMFAデバイスを使う上での注意点となると思いました。

*1:OTPが6桁の場合。また最上位ビットを無視を無視することとRFC 4226に書いてあります

*2:むやみに広げてはいけません。この場合だと、第3者が適当に入力したとしても100万分の5の確率でヒットしてしまうことになります

*3:この仕組みは必須とはされていません。上で触れた記事で利用したOTP-Sharpでも実装されていません

*4:自動再同期が実装されているシステムでのみ有効

新規AWSアカウント作成時にまずやること

こんにちは、AWS担当のwakです。先日社内でAWSアカウントを新規に作成してリソースの引っ越しをする機会がありました。良い機会ですので、AWS新規アカウントを作成して最初にすべきことをまとめます。

なお、現時点ではAWSの日本語にはあちこち若干怪しい箇所があるので画面キャプチャでは言語設定を英語にしています。操作を行う際はもちろん日本語で構いません。

f:id:nurenezumi:20151102152539j:plain

猫スリバチ の中でご満悦のかわいい猫*1


2段階認証

まずルートアカウントを2段階認証で保護しましょう。ルートアカウントは全ての権限を持っていますから、万が一乗っ取られたりすると

  • 勝手にAWSの契約を解約されて全てのリソースを失った
  • 勝手にインスタンスを山ほど起動されてビットコインに採掘に利用され、高額な課金だけが残った
  • 個人情報が全て流出した

といった悲劇につながります。2段階認証を導入するとこの危険を著しく軽減することができます。公式ドキュメントはこちらです。 docs.aws.amazon.com

アプリかトークンを用意する

スマホに対応アプリを入れるか、2段階認証用のハードウェアMFAデバイスを用意します(Amazon.comで注文したらデバイスは1週間程度で届きました)。スマホを使う場合は壊れる可能性を考えて2台用意しておくと良いでしょう。対応しているアプリ、デバイスの購入方法については次のドキュメントを参照してください。

IAM - Multi-factor Authentication

ルートアカウントで2段階認証を有効にする

画面右上のアカウント名をクリックし、「Security Credentials」をクリックするとこちらの画面が開きます。「MFA」をクリックします。

f:id:nurenezumi:20151030150234p:plain
スマホアプリで2段階認証を行う場合

「A virtual MFA device」を選択すると、画面にQRコードが表示されます。アプリで「コードを追加」メニューを選択するとQRコードリーダーが起動するので(QRコードリーダーを普通に直接起動して読み込ませてもダメです)、画面のQRコードを読ませましょう。スマホを2台使う場合は、2台目のスマホでまったく同じ操作を繰り返して同一のQRコードを読ませます*2QRコードを読ませると、アプリが6桁の数字からなるOTP(ワンタイムパスワード。6桁の数字)を表示してくれるようになります*3QRコードの下のテキストボックスに、まず表示されているOTPを、続けてその次に表示されるOTPを入力します。「連続するOTPを2つ入れる」のがルールです。

ハードウェアMFAデバイスで2段階認証を行う場合

バイスを使う場合は「A hardware MFA device」を選択して先へ進むと、デバイスのIDを入力するように指示されます。デバイスの裏側に書いてあるIDを入力しましょう(つまり、このIDはとても大切なコードだということになります)。同じように連続する2つのコードを入力します。


ログ格納用S3バケットを作る

AWSでは日々大量のログデータ(AWSの操作ログ、ELBのアクセスログ、請求データなど)が発生します。これらはもちろんきちんと保存して役立てたいものですが、それはどこに蓄積されていくのでしょうか。容量制限はないのでしょうか。

その答えがS3です(そして容量制限はありません)。まず事前にバケットを作成し、各サービスでそのバケットをログ出力先として登録します。すると日が経つにつれログがどんどんバケットに出力されてゆくという仕組みです。いずれ使うことになるはずなので、次のS3バケットを作成しておきましょう。後ろに書いてある名前は参考例です。

なお、S3では空のバケットを作成しただけでは料金はかからず、データが入った時点で初めて課金が始まります*4。ケチなあなたも安心です。


請求関連

ルートアカウントでログインし、アカウント名→Billing & Cost Management(請求とコスト管理)→Preferences(設定)とメニューを辿りましょう。

f:id:nurenezumi:20151030153158p:plain

こちらのチェックボックスを全てチェックして設定を有効にします。これはそれぞれ次のような意味です。

  • Receive PDF Invoice By Email(電子メールで PDF 版請求書を受け取る) 文字通り、毎月3日~5日頃にPDFの請求書がメール送信されてくるようになります。(これがなくてもコンソールから過去の請求額を参照することはできます)

  • Receive Billing Alerts(請求アラートを受け取る) これを有効にすると、CloudWatchのメトリクスに「料金」が追加されます。するとサーバーの負荷などと同じように、当月の利用料金が事前に設定しておいた金額を超えるたびにアラートを発生させることができます(この金額はもちろん複数設定できます)。きちんと監視をすれば、利用料金が不自然に増加していること、あるいは想定通りの推移をたどっていることを確認できるので安心できます。

  • 請求レポートを受け取る 請求データをCSV形式で受け取ることができます。次の項に書きます。

請求レポートを有効にする

請求レポートは、AWSの請求額の詳細な内訳が出力されたCSVファイルです。「何がこんなに高いのか?」「先週から増強したインスタンスにはいくらかかっているのか?」など、料金に関する情報を最も詳細かつタイムリーに得ることができます。最初に説明したように、このデータはS3に保存されてゆきます。

まずこの赤枠内のリンクをクリックすると、S3に設定すべきポリシー(JSON形式で記述されています)がポップアップ表示されます。

f:id:nurenezumi:20151030153504p:plain

S3のコンソールへ移動し、事前に作成しておいた請求レポート用バケットを右クリックしてプロパティを開きます。Permissions(アクセス許可)の中に「Edit bucket policy(バケットポリシーの追加)」メニューがあるので、これをクリックして開くテキストボックスに、上で表示されたポリシーを全文そのままコピペします。これでAWSのサービスがこのバケットへのアクセス権限を得て、権限テスト用のaws-programmatic-access-test-objectというファイルが1個自動的に作成されます。

請求レポートの内容

請求レポートは4種類あり、最も細かいものではリソース単位・種別単位・1時間単位で1レコードになっています。たとえばEC2インスタンスを作成して1時間動かしておくだけで、少なくとも

  • EC2稼働時間
  • データ転送(IN) *5
  • データ転送(OUT)
  • EBS保持時間(※SSDの場合)

という4種類の料金が発生しますから、1ヶ月では4件×24時間×30日分、約3000レコードが出力されます。こちらについてはまた稿を改めて書きたいと思います。

CloudTrailのログを保存できるようにする

CloudTrailのメニューを開き、

  • Logging(ログ記録)を有効にする
  • Configuration(設定)でログをS3へ保存するように設定する

を行っておきます。これでAWSに対するあらゆる操作を記録したログ(JSON形式)がS3にたまるようになります。

Configのログを保存できるようにする

AWS Configのメニューを開き、同様に

  • Recordingを有効にする
  • Set up(歯車アイコン)でS3に保存するように設定する

を行っておきます。これでAWSリソースに対する変更がすべてS3にたまるようになります。


IAMユーザー・グループを作成する

現時点ではAWSを操作できるユーザーアカウントはルートアカウントだけですが、これは権限が強力すぎて日常的な作業を行う用途には不向きです。必要な権限だけを持ったアカウントを作成しましょう。

IAMユーザーを作成する

画面右上のアカウント名→Security Credentials(認証情報)→Users(ユーザー)とたどると、IAMユーザーの管理画面が表示されます。ここでまずユーザーを作成します。

f:id:nurenezumi:20151030171608p:plain

アカウント名を入力して作成をするだけです。IAMはディレクトリサービス的な機能は持っておらず、IAMユーザーに氏名やメールアドレスを持たせることはできません。

IAMユーザーにパスワードを設定する

IAMユーザーは作成直後の段階ではパスワードが未設定のためコンソールにログインすることすらできません。作成したユーザーをクリックし、Security Credentials(認証情報)タブからパスワードを設定してあげます。

f:id:nurenezumi:20151030172026p:plain

また、ルートアカウントに2段階認証を設定したときと同様、IAMユーザーでも2段階認証を導入することができます。しかし2段階認証設定時にはスマートホンまたはトークンが必要になるため、IAMユーザー本人に設定してもらう方がスムーズかもしれません。

IAMユーザーに直接権限を付与する

次は権限です。ユーザーをクリックし、

f:id:nurenezumi:20151030174049p:plain
  • Attach Policy(ポリシーのアタッチ)をクリックすると、AWS側で用意された権限セットのテンプレート(パワーユーザー、EC2の状態読み取りのみ可能など)を付与することができます。
  • Inline Policies(インラインポリシー)からメニューを開くと、全ての種類の権限を条件付きで詳細に設定して付与することができます。

このどちらでも構いません。また、一般ユーザーに与える権限としては前回の記事を参考にしてください。

tech.sanwasystem.com

IAMグループに権限を付与する

上述のようにユーザーに直接権限を与えることもできますが、本来は

  1. まずユーザーをグループに追加し、
  2. そのグループに対して権限を付与する

というやり方が推奨されています(一人のユーザーが複数のグループに所属することもできます)。このボタンを押してグループ名を入力すれば即座にグループが作成されます。

f:id:nurenezumi:20151030175621p:plain

ここにユーザーと同じように権限を割り当て、さらにユーザーを追加すれば終わりです。


おわり

これで、

  • 権限を正しく設定し、不用意な操作を避ける
  • 操作ログを事後チェックすることができる
  • 料金を詳細に分析できる

ことができる状態、いわば「ちゃんと」AWSを使うための準備が整いました。AWSはお手軽に使うことができますが、お気軽に使い続けて構わないかというとまた別の検討が必要でしょう。後になって事情や気分が変わってもいいように、最初の下準備は大切だと思う次第です。それでは。

*1:ASCII.jp:ハマる猫はハマる! 魅惑の「猫スリバチ」とは!? (1/2)|荻窪圭の“這いつくばって猫に近づけ” より見つけて購入

*2:このとき、別に急ぐ必要はありません

*3:OTPは30秒ごとに変わります。スマホを2台使った場合は同じOTPが表示されるはずです。タイミングにはある程度ズレが出ますが気にしなくて大丈夫です

*4:それでもログデータ程度なら微々たるものですが

*5:通常は無料ですが、それでもレコードは出力されます

ASP.NETでExcelファイルを作成・ダウンロードさせる

こんにちは井上です。

ASP.NETでのEXCEL作成はネット上で検索すると結構ヒットするのですが、今回用いるパッケージについては情報量が比較的少なく、比較的頻度の高いプロパティ等に触れられていないケースが多かったので備忘録がてら纏ました。

パッケージはEPPlusです。

NuGet Gallery | EPPlus 5.8.6

OpenXMLを作成する他パッケージとしてClosedXML等もありますが、EPPlusを選んだ理由は安定版のなかでもよりDL数が多く更新頻度の高いパッケージであるためです。
尚、ClosedXMLは私の環境では少し長いコードを書いていると例外が発生して残念ながら使えませんでした。
Microsoft.Office.Interop.Excelを用いてExcel作成も可能ですが、こちらはおなじみのリソース解放が大変なのと(解放が漏れるとタスクマネージャーにExcelが続々現れる)、そもそもMSが推奨していないので採用できません。

Archived MSDN and TechNet Blogs | Microsoft Docs

EEPlusはOpenXMLを作成するだけですのでそこらへんは気にしなくても良いので楽チンです。

環境

.NET Framework 4.5.1
ASP.NET MVC
C#
EPPlus 4.0.4

Nugetでゲットする

f:id:ihisa:20151009132438p:plain

Excel作成処理

using OfficeOpenXml;
using System;
using System.IO;
using System.Web.Mvc;

~

[AcceptVerbs(HttpVerbs.Post)]
[ActionName("Index")]
public ActionResult Download()
{
    // excel
    using (var excel = new ExcelPackage())
    using (var worksheet = excel.Workbook.Worksheets.Add("ワークシート名"))
    {
        // フォント指定(列:1から5列、行1から10列なので、A1:E10を指す)
        worksheet.Cells[1, 1, 10, 5].Style.Font.Name = "MS Pゴシック";
        
        // フォントサイズ指定
        worksheet.Cells[1, 1, 10, 5].Style.Font.Size = 10;

        // 文字設定(A2セルを指す)
        worksheet.Cells[2, 1].Value = "今日の売上!";

        // セル結合(A2とB2,C2を結合)
        worksheet.Cells[2, 1, 2, 3].Merge = true;

        // センタリング
        worksheet.Cells[2, 1].Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Center;

        // 太字
        worksheet.Cells[2, 1].Style.Font.Bold = true;
        
        // 文字色指定
        worksheet.Cells[2, 1].Style.Font.Color.SetColor(System.Drawing.Color.Red);
        
        // 罫線
        using (var list = worksheet.Cells[5, 1, 8, 4])
        {
            list.Style.Border.Top.Style = OfficeOpenXml.Style.ExcelBorderStyle.Thin;
            list.Style.Border.Right.Style = OfficeOpenXml.Style.ExcelBorderStyle.Thin;
            list.Style.Border.Bottom.Style = OfficeOpenXml.Style.ExcelBorderStyle.Thin;
            list.Style.Border.Left.Style = OfficeOpenXml.Style.ExcelBorderStyle.Thin;
        }
        
        using (var listheader = worksheet.Cells[5, 1, 5, 4])
        {
            // セル背景色
            listheader.Style.Fill.PatternType = OfficeOpenXml.Style.ExcelFillStyle.Solid;
            listheader.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.Blue);
            listheader.Style.Font.Color.SetColor(System.Drawing.Color.White);

            // 中央寄せ
            listheader.Style.HorizontalAlignment = OfficeOpenXml.Style.ExcelHorizontalAlignment.Center;
        }

        // 書式(Valueはobject型。数値をそのままセットすると書式も数値となる)
        worksheet.Cells[5, 1].Value = "商品";
        worksheet.Cells[5, 2].Value = "単価";
        worksheet.Cells[5, 3].Value = "売上数";
        worksheet.Cells[5, 4].Value = "合計";

        worksheet.Cells[6, 1].Value = "りんご";
        worksheet.Cells[6, 2].Value = 100;
        worksheet.Cells[6, 3].Value = 25;

        worksheet.Cells[7, 1].Value = "みかん";
        worksheet.Cells[7, 2].Value = 90;
        worksheet.Cells[7, 3].Value = 10;

        worksheet.Cells[8, 1].Value = "ばなな";
        worksheet.Cells[8, 2].Value = 120;
        worksheet.Cells[8, 3].Value = 20;

        worksheet.Cells[6, 2, 8, 3].Style.Numberformat.Format = "0.00";

        // 計算式
        worksheet.Cells[6, 4].Formula = "B6 * C6";
        worksheet.Cells[7, 4].Formula = "B7 * C7";
        worksheet.Cells[8, 4].Formula = "B8 * C8";

        // 印刷設定            
        worksheet.PrinterSettings.Orientation = eOrientation.Landscape;
        worksheet.PrinterSettings.FitToPage = true;
        worksheet.PrinterSettings.FitToWidth = 1;
        worksheet.PrinterSettings.FitToHeight = 0;
        
        // 幅調整
        worksheet.Cells.AutoFitColumns();
        worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
        worksheet.Cells[1, 1].AutoFitColumns(10);
        worksheet.Cells[1, 2].AutoFitColumns(20);
        worksheet.Cells[1, 3].AutoFitColumns(10);
        worksheet.Cells[1, 4].AutoFitColumns(10);

        // グラフ
        using (var chart = worksheet.Drawings.AddChart("グラフ", OfficeOpenXml.Drawing.Chart.eChartType.BarClustered))
        {
            chart.SetPosition(10, 0, 0, 0);
            chart.SetSize(400, 400);
            chart.Series.Add("D6:D8", "A6:A8");
        }

        // 画像設定
        using (var picture = worksheet.Drawings.AddPicture("画像", new FileInfo(Server.MapPath("~/Content/Images/sanwa.png"))))
        {
            picture.SetPosition(33, 0, 0, 0);
        }

        // ページヘッダ設定
        worksheet.HeaderFooter.OddHeader.LeftAlignedText = "Excel出力";

        // ページフッタ設定(ページ番号/総ページ数)
        worksheet.HeaderFooter.OddFooter.RightAlignedText = String.Format("{0} / {1}", ExcelHeaderFooter.PageNumber, ExcelHeaderFooter.NumberOfPages);

        // ページフッタ幅
        worksheet.PrinterSettings.FooterMargin = (decimal)(0.8 / 2.54);
        worksheet.PrinterSettings.BottomMargin = (decimal)(1.5 / 2.54);

        // ページサイズ
        worksheet.PrinterSettings.PaperSize = ePaperSize.A4;

        // 用紙内にサイズ調整
        worksheet.PrinterSettings.FitToHeight = 1;
        worksheet.PrinterSettings.FitToWidth = 1;

        // 出力
        using (MemoryStream ms = new MemoryStream())
        {
            excel.SaveAs(ms);
            return File(ms.ToArray(), "application/msexcel", "ダウンロードファイル名.xlsx");
        }
    }
}

出力ファイル

f:id:ihisa:20151027223717p:plain

あっという間にできました。
このパッケージのプロパティはExcel VBAプロパティ/メソッド名と近しい名前となっていますので、一度でもExcelをマクロでいじったことがあるような方でしたら直ぐにわかるかと思います。
同様にMicrosoft.Office.Interop.Excel名前空間内のプロパティ/メソッド名とも近いですね。
Excelはなんだかんだで重宝される形式ではありますので覚えておいて損はないかと思います。

PowerShellでhashを書いてさくっとJSONを生成する

こんにちは、PowerShellが好きなwakです。

kimoto.hatenablog.com

こんな記事を読んだので、さくっとPowerShell版も書いておきます。

f:id:nurenezumi:20151023091935j:plain

ねこのひらき(を再度閉じたやつ)

やってみる

何も考えずにPowerShellを立ち上げて、以下のブロックをコピペしてください。*1

# とりあえずハッシュを用意する
$hash = @{
   Number = 123;
   String = "ABCあいう";
   Object = @{ a = "A"; b = 1; };
}
# 後からハッシュに要素を追加してみる
$hash.Array = @(1, 2, 3)
$hash | ConvertTo-Json

実行結果はこうなります。

f:id:nurenezumi:20151022175708p:plain

インデントが邪魔なときは -Compress オプションを付けると1行に圧縮してくれます。

f:id:nurenezumi:20151022175753p:plain

簡単な説明

PowerShellでは、

  • ハッシュは@{Key1=1; Key2=2; Key=3;} の書式で記述します。最後のセミコロンはあってもなくても構いません。
  • 配列は @("value1", "value2", "value3") の書式で記述します。残念ながら余計なカンマは許容されません。

結果をクリップボードにコピーする

末尾に | clip と付ければ結果をクリップボードへコピーすることができます。これが一番簡単です。

$hash | ConvertTo-Json | clip

ただ、結果の中に非ASCII文字が入っていると文字化けしてしまいます(上述の「あいう」など)。ちょっとだけ面倒ですが対応は可能です。まず次のブロックをコピペして(最初の1回だけでOKです)、テキストをクリップボードにコピーするための関数を定義しておきましょう。

function Out-ClipBoard
{
    param([parameter(Mandatory=$true,
            ValueFromPipeline=$true)][object[]]$InputObject)
    begin
    {
        Add-Type -AssemblyName System.Windows.Forms
        $tb = New-Object System.Windows.Forms.TextBox
        $tb.Multiline = $true
        $tb.MaxLength = 0
        $out=@()
    }
    process
    {
        $out+=$InputObject
    }
    end
    {
        $tb.Text = $out -join "`r`n"
        $tb.SelectAll()
        $tb.Copy()
    }
}

これは内部的にWindows Formのテキストボックスを生成し、テキストをセット・全選択・コピーするという力業で実現しています。この準備をしておいた上で、末尾に| Out-ClipBoardを付ければ結果をクリップボードへ正しくコピーすることができます。

$hash | ConvertTo-Json | Out-ClipBoard

結果をファイルに出力する

\記号のエスケープは必要ありません。

$hash | ConvertTo-Json | Out-File "c:\temp\hoge.txt" -Encoding UTF8

こんなところです。

*1:なお、PowerShell 3.0以降が必要です。普通の環境では特に気にする必要はありませんが、Windows Server 2008 R2を更新せずに使っているような人は諦めてください。

AWSで全ユーザーに割り当てる権限

こんにちは、AWS担当のwakです。AWSで新しくIAMユーザーを作ったときに割り当てるべきポリシーを整理したので書くことにします。

f:id:nurenezumi:20151002173222j:plain

大好物の高級かりかり欲しさに芸をするかわいい猫

ポリシーの内容

まずポリシーの内容を記載します。3つのパートに分けています。

1. 各種読み取り権限

弊社でよく利用しているサービスで状態読み取りを許可しています。この権限だけで、稼働中の各種インスタンスについてはどのような構成になっているかまでは分かることになります。またrds:DownloadDBLogFilePortionを指定しているのでRDSのログファイルの取得も可能です。スロークエリをファイルに保存するようにしている場合にはファイルを取得するためにこの権限が必要です。

{
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:Describe*",
                "autoscaling:Describe*",
                "elasticloadbalancing:Describe*",
                "cloudwatch:List*",
                "cloudwatch:Get*",
                "cloudwatch:Describe*",
                "autoscaling:Describe*",
                "rds:Describe*",
                "rds:ListTagsForResource",
                "logs:Get*",
                "logs:Describe*",
                "logs:TestMetricFilter",
                "sns:Get*",
                "sns:List*"
            ],
            "Resource": "*"
        }
    ]
}

2. 認証情報

アカウント上に存在する全IAMユーザー一覧取得、自分自身のパスワード変更、自分自身の仮想MFAデバイス(要するにスマートホンのアプリ)を使った2段階認証追加・再同期を許可しています。ここでは無効化は許可していないので、別の端末に2段階認証を移動したい場合(機種変したときなど)には管理者が一度2段階認証を削除し、ユーザーが再度追加する必要があります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iam:ChangePassword",
                "iam:EnableMFADevice",
                "iam:ListMFADevices",
                "iam:ResyncMFADevice"
            ],
            "Resource": "arn:aws:iam::999999999999:user/${aws:username}"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:CreateVirtualMFADevice",
                "iam:DeleteVirtualMFADevice"
            ],
            "Resource": "arn:aws:iam::999999999999:mfa/${aws:username}"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:ListUsers",
                "iam:ListVirtualMFADevices"
            ],
            "Resource": "*"
        }
    ]
}
  • 999999999999AWSのアカウントIDに置き換えてください。ダッシュボードや毎月の請求書(PDFファイル)で確認できます。
  • ${aws:username}はIAMユーザー名に置換される変数です。つまりarn:aws:iam::999999999999:user/${aws:username}は「IAMユーザー自分自身のARN*1」を示します。
  • 仮想MFAデバイスの無効化権限を与えない場合でも、iam:DeleteVirtualMFADeviceは必須です。
    • 「MFAデバイスの管理」→「仮想MFAデバイス」をクリックした時点で内部的には仮想MFAデバイスが作成されているようで、ここでキャンセルしてしまうと未アクティベーションの仮想MFAデバイスが残ってしまいます。これは次に仮想MFAデバイス追加をやり直した際に自動的に削除されるのですが、iam:DeleteVirtualMFADeviceの権限がないと先に進めなくなります。
    • 管理者が仮想MFAデバイスを無効化した後、ユーザーが再度仮想MFAデバイスを追加する場合も同じくiam:DeleteVirtualMFADeviceが必要です。
    • IAMユーザーが自分で仮想MFAデバイスの無効化を行うためにはiam:DeactivateMFADeviceが必要です。

3. S3バックアップフォルダ

最後はS3です。ユーザー個人のファイル置き場(他ユーザーからは参照不可)として、S3にバックアップ用バケットを作成し、この中に/Personal/USERNAME*2というフォルダを作成してあります。ここに読み書きする権限を与えます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:ListAllMyBuckets",
                "s3:GetBucketLocation"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::*"
        },
        {
            "Action": "s3:ListBucket",
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::backup-test-sanwasystem",
            "Condition": {
                "StringEquals": {
                    "s3:prefix": [
                        "",
                        "Personal/"
                    ],
                    "s3:delimiter": "/"
                }
            }
        },
        {
            "Action": "s3:ListBucket",
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::backup-test-sanwasystem",
            "Condition": {
                "StringLike": {
                    "s3:prefix": "Personal/${aws:username}/*"
                }
            }
        },
        {
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:s3:::backup-test-sanwasystem/Personal/${aws:username}/*"
        }
    ]
}

それぞれ上から順に、

  • バケットのリストを取得するための権限
  • 指定したバケット/Personal直下を見るための権限
  • 指定したバケット/Personal/USERNAMEを見るための権限
  • 指定したバケット/Personal/USERNAME以下を読み書きするための権限

となります。

説明

これだけだと格好がつかないので、ポリシーについて簡単に解説を加えます。AWSの操作には、

……といったように、すべてに名前が付いています*3。IAMユーザーには、これらを許可するか禁止するかを個別に設定することができます。また、ec2:Describe*rds:*のようにワイルドカードを利用することもできます。

明示的な禁止

"Effect":"Allow"のかわりに"Effect":"Deny"と書くと、そのユーザーはその操作が決して許されなくなります。「決して」というのはつまり、

  • rds:*(RDSに対するすべての操作)を許可
  • rds:DeleteDBInstance(RDSのインスタンス削除)を禁止

というルールを両方設定したとしても禁止の方が勝つということです。

条件

さらに条件をつけて絞ることもできます。

  • 「Groupタグが特定の値なら」という条件で、Groupタグが特定の値を持つEC2インスタンスに対しての操作のみ許可する
  • 「ARNが特定の値なら」という条件で、特定のEC2インスタンスに対しての操作は受け付けないように禁止する
  • 「操作元IPアドレスが特定の値なら」という条件で、社内からの操作のみ受け付ける
  • 「操作が発生した時刻が特定の期間内なら」という条件で、本番リリースまでの間だけ環境構築のための操作を受け付ける
  • 「S3でキーが特定のパターンから始まるなら」という条件で、S3の特定のフォルダ以下の読み書きを受け付ける

最後のパターンを「3. S3バックアップフォルダ」にあるCondition句で使っています。

グループ

管理上の都合から、一般的にはIAMユーザーに対して直接権限を与える(ポリシーをアタッチする)ことは推奨されていません。AWSにはIAMユーザーをまとめて管理するためのIAMグループという機能が用意されているので、

  1. IAMグループを作る
  2. IAMグループに対してポリシーをアタッチする
  3. IAMユーザーを任意のグループに追加する(複数可)

という手順に従うのがベストプラクティスだとされています。このため、IAMユーザー間に上位・下位関係を持たせるようなロールはありません。

まとめ

このようにIAMユーザーの権限は細かくかつ柔軟に設定できますが、あまり「必要最低限」にこだわると、使うサービスやユーザー増えてきたときに管理が難しくなると感じています。何か緊急時にいちいち管理者の対応が必要になるような体制にしてしまうとAWSのメリットが薄れてしまいますし、CloudTrailで操作ログも取ることができます。「お前を信じて…これ(PowerUserAccessポリシー)を……渡す! 受け取れ!!」といった信頼(と割り切り)は大切かもしれません。

*1:ARNとはAmazon Resource Nameの略で、EC2インスタンス、RDSインスタンス、IAMユーザーなど、AWSの全てのリソースに対して振られた一意のIDです。

*2:USERNAMEはユーザーのアカウント名。弊社の運用ルールでは、Slack・Backlogのアカウント名、会社のメールアドレスの先頭部分でもあります。

*3:つまりAPIの名前だと思えばいいです。Management Consoleの操作と1:1では対応していません。1回のクリックや画面遷移で複数の操作が行われることもあります。