Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Infrastructure as Code の静的テスト戦略 #CODT2020 / Clo...

Infrastructure as Code の静的テスト戦略 #CODT2020 / Cloud Operator Days Tokyo 2020

Cloud Operator Days Tokyo 2020 で使用したスライドです。

インフラをコードとして管理することで環境構築の再現性を担保する IaC の手法は、ここ数年で急激に普及しました。しかし、そのコードに対するテストは簡単なチェックに留まっていることも多く、実際に環境を構築してみたら上手く動かない、という状況がしばしば起こります。そこで本セッションでは、実際の環境を構築する「前」に、IaC のコード自体に対してテストを行う手法について解説します。

実際にインフラをコードで管理していて、早い段階で設定ミスを検出して無駄なコストを削減したい方におすすめのセッションです。

イベント概要:https://cloudopsdays.com/
ブログ記事:https://ccvanishing.hateblo.jp/entry/2020/07/30/173935

y_taka_23

July 29, 2020
Tweet

More Decks by y_taka_23

Other Decks in Technology

Transcript

  1. #CODT2020 本日のアジェンダ • なぜ IaC に静的テストが必要なのか • AWS 上で IaC

    を実現する上で考えるべきこと • IaC をテストする上での戦略とツール
  2. #CODT2020 Infrastructure as Code って何だっけ? • ソフトウェア開発のプラクティスをインフラの オートメーションに活かすアプローチ ◦ Git

    によるバージョン管理 ◦ Pull Request によるレビュー ◦ 継続的なテスト ◦ etc... 『Infrastructure as Codeクラウドにおける サーバ管理の原則とプラクティス』 https://www.oreilly.co.jp/books/9784873117966/
  3. #CODT2020 Infrastructure as Code って何だっけ? • ダイナミックインフラプラットフォーム (AWS) ◦ サーバやストレージの提供

    • インフラ定義ツール (CFn, Terraform) ◦ サーバやストレージの構成・設定管理 • サーバ構成ツール (Ansible, Chef) ◦ サーバ自身の細部の設定 • インフラサービス (CloudWatch など) ◦ インフラやアプリの管理支援
  4. #CODT2020 インフラの現状がカオス 何かが壊れそうで心配 例外的な作業 • 失敗経験 • IaC への不信 •

    組織の力関係 • Global: 局所的改善の困難さ • Mutable: 歪みの蓄積
  5. #CODT2020 インフラの現状がカオス 何かが壊れそうで心配 例外的な作業 • 失敗経験 • IaC への不信 •

    組織の力関係 • 不均一な構成 • システム疲労 • ノウハウ散逸 • Global: 局所的改善の困難さ • Mutable: 歪みの蓄積
  6. #CODT2020 インフラの現状がカオス 何かが壊れそうで心配 例外的な作業 塩漬けインフラ負のサイクル (オートメーション恐怖症) • 失敗経験 • IaC

    への不信 • 組織の力関係 • 不均一な構成 • システム疲労 • ノウハウ散逸 • Global: 局所的改善の困難さ • Mutable: 歪みの蓄積
  7. #CODT2020 インフラの現状がカオス 何かが壊れそうで心配 例外的な作業 塩漬けインフラ負のサイクル (オートメーション恐怖症) • 失敗経験 • IaC

    への不信 • 組織の力関係 • 不均一な構成 • システム疲労 • ノウハウ散逸 • Global: 局所的改善の困難さ • Mutable: 歪みの蓄積
  8. #CODT2020 インフラの V 字モデル? アプリ仕様 IaC 実装 E2E テスト Serverspec

    など 単体テストの不在 環境一揃い 個別リソース
  9. #CODT2020 インフラの V 字モデル? アプリ仕様 IaC 実装 E2E テスト Serverspec

    など 単体テストの不在 環境一揃い 個別リソース デプロイの壁
  10. #CODT2020 Section 1 のまとめ • IaC = アプリ開発プラクティスのインフラへの応用 ◦ 今回は

    Global + Mutable の領域にフォーカス • 予測可能性をいかに担保するか? ◦ 実行時に「何が起こるかわからない」という恐怖の克服 • アプリに寄せたテスト戦略 ◦ 静的(= デプロイ前)テストで「何が起こるか」を見切る
  11. #CODT2020 予測可能性の 3 要素 • 再現性 (Reproducibility) ◦ 同じ操作を誰でも、いつでも繰り返すことができる •

    純粋性 (Purity) ◦ 実行前の状態によらず、結果が常に同じになる • モジュール性 (Modularity) ◦ 再利用可能な部品が記述しやすい仕組みを備える
  12. #CODT2020 冪等性 vs 純粋性 • デプロイはパラメータ x と事前状態 e の関数

    ◦ 返り値は変更後の状態:e’ = f (x, e) • 冪等性:複数回実行しても結果が一定 ◦ 任意のパラメータ x と事前状態 e に対して f (x, f (x, e)) = f (x, e) • 純粋性:実行前の状態によらず結果が一定 ◦ 任意のパラメータ x と事前状態 e1, e2 に対して f (x, e1) = f (x, e2)
  13. #CODT2020 冪等性 vs 純粋性 • デプロイはパラメータ x と事前状態 e の関数

    ◦ 返り値は変更後の状態:e’ = f (x, e) • 冪等性:複数回実行しても結果が一定(純粋なら冪等) ◦ 任意のパラメータ x と事前状態 e に対して f (x, f (x, e)) = f (x, e) • 純粋性:実行前の状態によらず結果が一定 ◦ 任意のパラメータ x と事前状態 e1, e2 に対して f (x, e1) = f (x, e2)
  14. #CODT2020 IaC on AWS の 4 ステップ マネジメント コンソール AWS

    CLI CloudFormation CDK (Cloud Dev. Kit) 予測可能性
  15. #CODT2020 AWS CLI • シェルスクリプトなどと組み合わせて自動化 ◦ 機能面では扱える API が最も多い •

    予測可能性はあまり高くない ◦ 再現性:あり(繰り返し実行可) ◦ 純粋性:なし ◦ モジュール性:ほとんどなし
  16. #CODT2020 CloudFormation • YAML による宣言的な定義 ◦ 必要な操作ではなく望まれる状態を記述 • 予測可能性はだいぶ改善した ◦

    再現性:あり ◦ 純粋性:一応あり(宣言的記述、衝突しない名前の生成) ◦ モジュール性:かなり乏しい
  17. #CODT2020 Cloud Development Kit (CDK) • プログラムで CloudFormation 用 YAML

    を生成 ◦ TypeScript / Python / Java / .NET ライブラリ ◦ IDE が使える、型があるので YAML より書くのが楽 • 現状で予測可能性は最も良好 ◦ 再現性:あり ◦ 純粋性:一応あり(実質 CloudFormation と同等) ◦ モジュール性:あり (再利用・配布可能な Construct)
  18. #CODT2020 export class MyStack extend cdk.Stack { constructor(...) { super(...);

    const queue = new sqs.Queue(this, 'MyQueue', { visibilityTimeout = cdk.Duration.Seconds(300) }); const topic = new sns.Topic(this, 'MyTopic'); topic.addSubscription(new subs.SqsSubscription(queue)); } } SQS: MyQueue Subscribe SNS: MyTopic MyStack
  19. #CODT2020 export class MyStack extend cdk.Stack { constructor(...) { super(...);

    const queue = new sqs.Queue(this, 'MyQueue', { visibilityTimeout = cdk.Duration.Seconds(300) }); const topic = new sns.Topic(this, 'MyTopic'); topic.addSubscription(new subs.SqsSubscription(queue)); } } SQS: MyQueue Subscribe SNS: MyTopic MyStack scope(親要素)
  20. #CODT2020 cdk synth aws cloudformation deploy export class MyStack extend

    cdk.Stack { constructor(...) { super(...); const queue = new sqs.Queue(this, 'MyQueue', { visibilityTimeout = cdk.Duration.Seconds(300) }); const topic = new sns.Topic(this, 'MyTopic'); topic.addSubscription(new subs.SqsSubscription(queue)); } }
  21. #CODT2020 cdk synth aws cloudformation deploy Resources: MyQueueXXXXXX: Type: AWS::SQS::Queue

    Properties: ... MyQueuePolicyXXXXXX: Type: AWS::SQS::QueuePolicy ... MyTopicXXXXXX: Type: AWS::SNS::Topic ... MyQueueMyStackMyTopicXXXXXX: Type: AWS::SNS::Subscription ...
  22. #CODT2020 cdk synth aws cloudformation deploy Resources: MyQueueXXXXXX: Type: AWS::SQS::Queue

    Properties: ... MyQueuePolicyXXXXXX: Type: AWS::SQS::QueuePolicy ... MyTopicXXXXXX: Type: AWS::SNS::Topic ... MyQueueMyStackMyTopicXXXXXX: Type: AWS::SNS::Subscription ... CDK では明示していない = Construct が内包
  23. #CODT2020 export class Counter extends cdk.Construct { public readonly handler:

    lambda.Functions; constructor() { const table = new dynamodb.Table(this, 'Hits', { partitionKey: { ... } }); this.handler = new lambda.Handler(this, 'Handler', { ... environment: { ... HITS_TABLE_NAME: table.tableName } }); } }
  24. #CODT2020 export class Counter extends cdk.Construct { public readonly handler:

    lambda.Functions; constructor() { const table = new dynamodb.Table(this, 'Hits', { partitionKey: { ... } }); this.handler = new lambda.Handler(this, 'Handler', { ... environment: { ... HITS_TABLE_NAME: table.tableName } }); } } Construct を継承したクラスを作成
  25. #CODT2020 export class Counter extends cdk.Construct { public readonly handler:

    lambda.Functions; constructor() { const table = new dynamodb.Table(this, 'Hits', { partitionKey: { ... } }); this.handler = new lambda.Handler(this, 'Handler', { ... environment: { ... HITS_TABLE_NAME: table.tableName } }); } } DynamoDB の Construct
  26. #CODT2020 export class Counter extends cdk.Construct { public readonly handler:

    lambda.Functions; constructor() { const table = new dynamodb.Table(this, 'Hits', { partitionKey: { ... } }); this.handler = new lambda.Handler(this, 'Handler', { ... environment: { ... HITS_TABLE_NAME: table.tableName } }); } } Lambda の Construct
  27. #CODT2020 export class Counter extends cdk.Construct { public readonly handler:

    lambda.Functions; constructor() { const table = new dynamodb.Table(this, 'Hits', { partitionKey: { ... } }); this.handler = new lambda.Handler(this, 'Handler', { ... environment: { ... HITS_TABLE_NAME: table.tableName } }); } } 環境変数経由で参照
  28. #CODT2020 CDK とテスト • Snapshot Test ◦ 生成される YAML が前回と同じか(CDK

    のアップデートなど) • Fine-grained Assertion ◦ 生成された YAML に目的のリソースが存在しているか • Validation Test(実体は単なる例外送出のテスト) ◦ 不正なパラメータを渡したときにエラーが発生するか
  29. #CODT2020 export class DeadLetterQueue extends cdk.Queue { public readonly alarm

    cloudwatch.IAlarm; constructor(scope, id, props = {}) { super(scope, id); this.alarm = new cloudwatch.Alarm(this, 'Alarm', { alarmDescription: 'messages in the DLQ', evaluationPeriods: 1, threshold: 1, metric: this.metricApproximateNumberOfMessagesVisible(), }); } }
  30. #CODT2020 import { SynthUtils } from '@aws-cdk/assert'; test('DLQ preserves the

    snapshot', () => { const stack = new Stack(); new dlq.DeadLetterQueue(stack, 'DLQ’); expect(SynthUtils.toCloudFormation(stack)).toMatchSnapshot(); }); cdk synth に相当
  31. #CODT2020 export class DeadLetterQueue extends cdk.Queue { public readonly alarm

    cloudwatch.IAlarm; constructor(scope, id, props = {}) { super(scope, id); this.alarm = new cloudwatch.Alarm(this, 'Alarm', { alarmDescription: 'messages in the DLQ', evaluationPeriods: 1, threshold: 1, metric: this.metricApproximateNumberOfMessagesVisible(), period: cdk.Duration.minutes(1), }); } }
  32. #CODT2020 export class DeadLetterQueue extends cdk.Queue { public readonly alarm

    cloudwatch.IAlarm; constructor(scope, id, props = {}) { super(scope, id); this.alarm = new cloudwatch.Alarm(this, 'Alarm', { alarmDescription: 'messages in the DLQ', evaluationPeriods: 1, threshold: 1, metric: this.metricApproximateNumberOfMessagesVisible(), period: cdk.Duration.minutes(1), }); } } 生成される YAML に影響を与える変更
  33. #CODT2020 import { expect as expectCDK, SynthUtils, haveResource } from

    '@aws-cdk/assert'; test('DLQ has the message alarm', () => { const stack = new Stack(); new dlq.DeadLetterQueue(stack, 'DLQ’); expectCDK(stack).to(haveResource('AWS::CloudWatch::Alarm', { Namespace: 'AWS/Lambda' })); });
  34. #CODT2020 import { expect as expectCDK, SynthUtils, haveResource } from

    '@aws-cdk/assert'; test('DLQ has the message alarm', () => { const stack = new Stack(); new dlq.DeadLetterQueue(stack, 'DLQ’); expectCDK(stack).to(haveResource('AWS::CloudWatch::Alarm', { Namespace: 'AWS/Lambda' })); }); CDK が提供する YAML 生成結果に関する アサーション
  35. #CODT2020 import { expect as expectCDK, SynthUtils, haveResource } from

    '@aws-cdk/assert'; test('DLQ has the message alarm', () => { const stack = new Stack(); new dlq.DeadLetterQueue(stack, 'DLQ’); expectCDK(stack).to(haveResource('AWS::CloudWatch::Alarm', { Namespace: 'AWS/Lambda' })); }); わざと間違えてみた (AWS/SQS)
  36. #CODT2020 export class DeadLetterQueue extends cdk.Queue { public readonly alarm

    cloudwatch.IAlarm; constructor(scope, id, props = {}) { if (props.retention != undefined && props.retention > 14) { throw new Error('retention should be <= 14'); } super(scope, id, { retentionPeriod: cdk.Duration.days(props.retention || 14) }); ... }
  37. #CODT2020 export class DeadLetterQueue extends cdk.Queue { public readonly alarm

    cloudwatch.IAlarm; constructor(scope, id, props = {}) { if (props.retention != undefined && props.retention > 14) { throw new Error('retention should be <= 14'); } super(scope, id, { retentionPeriod: cdk.Duration.days(props.retention || 14) }); ... } 引数 (props) を確認して範囲外なら例外
  38. #CODT2020 test('DLQ retention should be < 14', () => {

    const stack = new Stack(); expect(() => { new dlq.DeadLetterQueue(stack, 'DLQ', { retention: 14 }); }).toThrowError(); }); 範囲内(例外は飛ばない)
  39. #CODT2020 Section 2 のまとめ • デプロイ時の予測可能性のために必要な要素 ◦ 再現性 / 純粋性

    / モジュール性 • 段階的に予測可能性を獲得 ◦ コンソール < CLI < CloudFormation < CDK • CDK には静的テスト機構が備わっている ◦ Snapshot / Fine-grained Assertion / Validation
  40. #CODT2020 export class Counter extends cdk.Construct { public readonly handler:

    lambda.Functions; constructor(...) { const table = new dynamodb.Table(this, 'Hits', { ... }); this.handler = new lambda.Handler(this, 'Handler', { ... environment: { ... HITS_TABLE_NAME: table.tableName } }); } }
  41. #CODT2020 export class Counter extends cdk.Construct { public readonly handler:

    lambda.Functions; constructor(...) { const table = new dynamodb.Table(this, 'Hits', { ... }); this.handler = new lambda.Handler(this, 'Handler', { ... environment: { ... HITS_TABLE_NAME: table.tableName } }); table.grantReadWriteData(this.handler); } } 権限つけ忘れた!
  42. #CODT2020 CloudFormation CDK 作成されるリソース 1. 実装 A. 期待する YAML 2.

    YAML の生成 • CDK が提供する予測可能性:1 + 2 = A 3. リソースの作成
  43. #CODT2020 CloudFormation CDK 作成されるリソース 1. 実装 B. 期待する振る舞い A. 期待する

    YAML 2. YAML の生成 3. リソースの作成 • CDK が提供する予測可能性:1 + 2 = A
  44. #CODT2020 CloudFormation CDK 作成されるリソース 1. 実装 B. 期待する振る舞い A. 期待する

    YAML 2. YAML の生成 3. リソースの作成 • CDK が提供する予測可能性:1 + 2 = A • 本当に欲しい予測可能性:1 + 2 + 3 = B
  45. #CODT2020 CloudFormation CDK 作成されるリソース 1. 実装 B. 期待する振る舞い A. 期待する

    YAML 2. YAML の生成 3. リソースの作成 • 1 + 2 = A かつ A + 3 = B なら 1 + 2 + 3 = B
  46. #CODT2020 CloudFormation CDK 作成されるリソース 1. 実装 B. 期待する振る舞い A. 期待する

    YAML 2. YAML の生成 3. リソースの作成 • 1 + 2 = A かつ A + 3 = B なら 1 + 2 + 3 = B • つまり残りは右側の予測可能性が問題
  47. #CODT2020 リソースに期待する振る舞い • ポリシー的な性質 ◦ リソース・アプリをまたいで制約が掛かっているか ◦ 例:0.0.0.0/0 禁止、コスト集約用のタグ必須 •

    意味論的な性質 ◦ 複数のリソースがうまく「噛み合った」状態で動作するか ◦ 例:ネットワーク疎通が可能か、権限が足りているか
  48. #CODT2020 CloudFormation のテストツール • cfn-nag ◦ CloudFormation 用、分野はセキュリティ系 • CloudFormation

    Guard (cfn-guard) ◦ CloudFormation 用、分野は限定せず汎用 • Conftest ◦ 一般の YAML 用、分野は限定せず汎用
  49. #CODT2020 cfn-nag • セキュリティ系の定番ツール ◦ ベストプラクティスがあらかじめ定義されている • メリット・デメリット ◦ 定義済みルール:デフォルトで豊富(必要なら抑制も可能)

    ◦ 配布・再利用性:低い(一応 S3 Bucket 経由で共有可能) ◦ 拡張性:低い(Ruby でロジックを陽に記述する必要あり) https://github.com/stelligent/cfn_nag
  50. #CODT2020 CloudFormation Guard (cfn-guard) • 新登場の汎用チェックツール (2020/6/17 -) ◦ 開発者プレビューなので

    Rust のソースからビルド • メリット・デメリット ◦ 定義済みルール:なし(既存の YAML からルール生成が可能) ◦ 配布・再利用性:低い(ルールファイル直接指定のみ) ◦ 拡張性:あまり高くない(個別の属性をチェックする DSL) https://github.com/aws-cloudformation/cloudformation-guard
  51. #CODT2020 Conftest • Open Policy Agent (OPA) の派生 ◦ Kubernetes

    との連携 (Gatekeeper) が人気 • メリット・デメリット ◦ 定義済みルール:なし ◦ 配布・再利用性:高い(OCI = Docker レジストリで配布可能) ◦ 拡張性:高い(Prolog の一種 Rego を使用) https://github.com/open-policy-agent/conftest
  52. #CODT2020 Rego 言語ことはじめ is_xxxxx { condition1 condition2 } Rego の記述は

    「xxxxxx とは yyyyyy であることである」 という定義のあつまり
  53. #CODT2020 Rego 言語ことはじめ is_xxxxx { condition1 condition2 } 定義:「xxxxx であるとは」

    Rego の記述は 「xxxxxx とは yyyyyy であることである」 という定義のあつまり
  54. #CODT2020 Rego 言語ことはじめ is_xxxxx { condition1 condition2 } 内容「Conidition 1

    かつ Condition-2 かつ…が成り立つことである」 Rego の記述は 「xxxxxx とは yyyyyy であることである」 という定義のあつまり
  55. #CODT2020 Rego 言語ことはじめ is_xxxxx { condition1 } is_xxxxx { condition2

    } 内容「Conidition 1 または Condition-2 が成り立つことである」 Rego の記述は 「xxxxxx とは yyyyyy であることである」 という定義のあつまり
  56. #CODT2020 Rego 言語ことはじめ is_xxxxx { is_yyyyy } is_yyyyy { is_zzzzz

    } 定義の参照「xxxxx であるとは yyyyy であることで、その yyyyy であるとは…」 Rego の記述は 「xxxxxx とは yyyyyy であることである」 という定義のあつまり
  57. #CODT2020 Rego 言語ことはじめ is_xxxxx { f = functions[_] is_good_func(f) }

    代入ではない(単一化) 配列 functions の中から全体を成り立たせるような添字 _ が存在すれば それを任意に取って f とする。 Rego の記述は 「xxxxxx とは yyyyyy であることである」 という定義のあつまり
  58. #CODT2020 export class Counter extends cdk.Construct { public readonly handler:

    lambda.Functions; constructor(...) { const table = new dynamodb.Table(this, 'Hits', { ... }); this.handler = new lambda.Handler(this, 'Handler', { ... environment: { ... HITS_TABLE_NAME: table.tableName } }); table.grantReadWriteData(this.handler); } } 権限の不足を発見したい
  59. #CODT2020 deny[msg] { ... violations = [ [f, t] |

    f := functions[_]; t := tables[_]; needs_permission(f, t); not has_permission(f, t); ] count(violations) > 0 ... } warn[msg] { ... }
  60. #CODT2020 deny[msg] { ... violations = [ [f, t] |

    f := functions[_]; t := tables[_]; needs_permission(f, t); not has_permission(f, t); ] count(violations) > 0 ... } warn[msg] { ... } deny でエラー、warn は警告
  61. #CODT2020 deny[msg] { ... violations = [ [f, t] |

    f := functions[_]; t := tables[_]; needs_permission(f, t); not has_permission(f, t); ] count(violations) > 0 ... } warn[msg] { ... } 権限が必要だが持っていない Function と Table の組 > 0 ならばエラー
  62. #CODT2020 allows([_, policy], [table_name, _]) { policy.Type = "AWS::IAM::Policy" statements

    := policy.Properties.PolicyDocument.Statement[_] statements.Effect = "Allow" statements.Resource[_]["Fn::GetAtt"][0] = table_name statements.Action[_] = "dynamodb.PutItem" statements.Action[_] = "dynamodb.UpdateItem" }
  63. #CODT2020 allows([_, policy], [table_name, _]) { policy.Type = "AWS::IAM::Policy" statements

    := policy.Properties.PolicyDocument.Statement[_] statements.Effect = "Allow" statements.Resource[_]["Fn::GetAtt"][0] = table_name statements.Action[_] = "dynamodb.PutItem" statements.Action[_] = "dynamodb.UpdateItem" } Statement を適切に選んで 以下を満たせるか?
  64. #CODT2020 allows([_, policy], [table_name, _]) { policy.Type = "AWS::IAM::Policy" statements

    := policy.Properties.PolicyDocument.Statement[_] statements.Effect = "Allow" statements.Resource[_]["Fn::GetAtt"][0] = table_name statements.Action[_] = "dynamodb.PutItem" statements.Action[_] = "dynamodb.UpdateItem" } Action を適切に選んで 条件全体を満たせるか?
  65. #CODT2020 定義済みルール 配布・再利用 拡張性 cfn-nag ◯ × × cfn-guard ×

    × △ Conftest × ◯ ◯ 各ツールの使いどころ Cfn-nag の定義済みルールを最大限使いつつ、複雑な記述は Conftest でテスト
  66. #CODT2020 Section 3 のまとめ • 何をテストしているのか意識する ◦ CDK のテストあくまでも CDK

    から YAML への変換のテスト • リソースに期待する振る舞い ◦ ポリシー的な性質 / 意味論的な性質 • 生成された YAML をテストするコツ ◦ Conftest で具体的な名前に依存せずテストできる
  67. #CODT2020 本日のまとめ • IaC における静的テストの必要性 ◦ デプロイ前に予測可能性を確保したい • AWS のリソース管理と

    CDK ◦ 再現性 / 純粋性 / モジュール性 • YAML に対するテスト戦略とツール ◦ cfn-nag(セキュリティ)/ Conftest(正しく動く条件)