CDKでEC2のBlue/Greenデプロイをするパイプラインを作る

こんばんは@manoです。もう今年も12月ですね。

12月ということで、今年もNewsPicksではアドベントカレンダーをやります!(って、僕は初参加なんだけども。)

実はここで話すのは初めてですが、僕は(株)NewsPicksの社員で、この記事はNewsPicks Advent Calendar 2019の1日目の記事ということになります。

最近CDKばっかり触っているということもあり、当ブログとしてはここまで2記事連続でCDK関連の話をしてきましたが、今日もCDK関連の話をします。

NewsPicksでは現在、デプロイフローの見直しを進めています。
現状はAWSで、auto scaling groupで管理されたEC2インスタンスに、NewsPicksのアプリケーションはJAVAがメインであるため.warファイルを配置し、Nginx & Tomcatといったwebサーバー構成でアプリケーションを動かしています。
今後それをコンテナ化したり、大きなモノリスになってしまっている部分のマイクロサービス化などをどんどんやっていこうと思っているのですが、まず着手しているのがデプロイフローの改善です。

現在のNewsPicksのデプロイは、自分たちで作ったshellスクリプトによってauto scaling groupのBlue/Greenデプロイを実現しています。
これを、CodeDeployを使用してBlue/Greenデプロイを行うようにしようとしていて、そのあたりの仕組みの実装方法にCDK(言語はtypescript)を採用しています。

CDKを採用した(個人的な)理由

1. 書かなければならないコードの量が少ない。

例えばCloudFormationだと、一つ一つのコンポーネントには基本的にroleの明記が必要で、一つのyaml(またはjson)ファイルが巨大になっていくこともしばしばかと思います。
その点、CDKではコード内の依存関係から、ある程度勝手に必要なpolicyが割り当てられたroleが自動的に作成されます。手軽です。

2. IDE等でclassやinterfaceの定義にジャンプすれば、ドキュメントを見ずとも開発が可能

CloudFormationだと、yamljsonに書いていかなければならないため、自身でドキュメントを確認し、適切に書かなければいけません。
でもCDKだと、typescript等の型のある言語を選択すれば(というかtypescript以外で書いたことないから他はよく知らないんだけど)、classやinterfaceの定義にジャンプすれば必要なパラメータがどのようなものかがすぐにわかるため、便利です!
また、テストが書けるというのもとても魅力的です。「デプロイして初めて書いた内容が合っているのかどうかがわかる」という局面も少なからずありますが、テスト実行時点でわかるものも結構あります。

3. CDK自体の開発がものすごく活発

これも大きな理由です。最近だと毎週のようにバージョンが上がっています

新デプロイフローの構成

このような感じで進めています。 f:id:jumpeim37:20191201180819p:plain

NewsPicksではメインのバージョン管理ツールにbitbucketを使用しているのですが、CodePipelineはbitbucketからのトリガーに対応しておらず・・・、 CodeBuildは対応しているので、

  1. release tagがbitbucketにプッシュされたらCodeBuildプロジェクトがビルドを実行する
  2. 1の成果物がS3にアップロードされる
  3. 2のbucketにアップロードされたことをトリガーとして、CodePipelineが走る
  4. 2でアップロードされた成果物を整理してS3に上げ直す
  5. 手動approve
  6. CodeDeployによりBlue/Greenデプロイを実行する

という構成にしています。

上記構成のCDKでの実装

このような感じで実装しています。
なお、auto scaling groupのBlue/Greenデプロイは、2019/12/1現在CloudFormationが対応していないので、Deployment GroupがCDKでは実装できません。
そのため、上のコードではDeployment Groupに割り当てるIAM roleだけを作っています。
Blue/Greenを行うDeployment Groupを作成するには、 aws deploy create-deployment-group等で行う必要があります。

CodePipelineを採用した理由

1. 手動approveステージが作れる

例えば「ビルドまでは完了してあとはデプロイだ」というとき、そのままデプロイが走ってしまうと、万一それがよからぬタイミングだったりすると大変です。
CodePipelineには、manual approveという機能があり、手動でapproveしないと以降のステージには進めないようになっています。

2. CodeDeployが使える

後述しますが、CodeDeployが素晴らしいです。

3. CDKによりコードで管理できる

素敵ですね。

CodeDeployを採用した理由

1. 過去のリビジョンに遡って簡単にデプロイできる

デプロイ後に戻したくなったとき、Blueのauto scaling groupがまだ生きていればすぐにrollbackできるし、terminate済みだとしても簡単に戻したいリビジョンを選択肢リデプロイできます。

2. 細かいイベントフックごとに任意の処理を実行できる

こちらに詳細が書かれていますが、デプロイ中のフックが結構細かく分けられていて、それぞれに任意の処理を挟むことができます。

終わりに

アドベントカレンダーというものに初めて参加したのですが、記事を強制的に書かざるを得ないという意味で貴重な機会だなと感じました。
これを機に、様々なカレンダーにどんどん参加したいなと思いました。

2019-12-02追記

アドベントカレンダー2日目の記事です。

qiita.com

CDKでbastionサーバーを作る

もはやSession Managerを使うことが主流となり、bastionの出番は減ってきたと思うが、CDKでbastionを作ったので手順をしたためておく。

1. key pairを作る

今回はbastionという名前のkey pairをコンソールから作っておく。 手順はこちらを参照してください。

docs.aws.amazon.com

2. Secret Managerに、1作成時に自動ダウンロードされるpemの中身を登録する

今回はbastion-secretというsecret名で、

{"key": "(ダウンロードしたpemの中身。ただし改行コードを\nで置換したもの)"}

という形で登録しておく。 Secret Mangerについてはこちら。

docs.aws.amazon.com

3. CDK実装

例によってlibのファイルのみ載せておく。binはまぁご自由に。

gist.github.com

ポイントは下記の通り。

1. private subnetにbastionを作る

vpcにpublic・privateの各subnetを作った場合、AWSが推奨している(CDKのデフォルトになっている)のはprivate subnetにbastionを作ること。

github.com

その場合、Session Managerを使ってローカルマシンからbastionにアクセスする。

% aws ssm start-session --target {bastionのinstance id}
% sudo su - ec2-user(必要ならね)

(これができるんだから、bastionなんかいらなくね!!??という疑問はもっともである。)

ちなみに訳あってpublic subnetに作る場合は、instance connectで自分の持っている公開鍵を一時的にbastionに登録してsshで繋ぐ。

% aws ec2-instance-connect send-ssh-public-key --instance-id {bastionのinstance id} --instance-os-user {amazon linuxなら"ec2-user"} --ssh-public-key {"file://~/.ssh/id_rsa.pub"とか} --availability-zone {"ap-northeast-1"とか}
% mssh {bastionのinstance id}
2. Secret Managerから取得するpemの中身は、改行コードを実際の改行状態にしておく

改行も含めて鍵。

3. bastionからのSecurity Groupは、他のstackでも使うだろうからエクスポートしておく

今回はallow-from-bastionという名前でエクスポートしておいた。

4. user dataを使って、bastion内の/home/ec2-user/.ssh/id_rsaにkey pairのpemをセットしておく

他にいいやり方知ってる人がいたら教えてください。

5. bastionから繋ぎたい他のインスタンスは、同一subnet内に、key pairをbastionにして作成する

ちなみにその、他のインスタンスに直接ssm agentを入れちゃえば、直接Session Managerでaws ssm session-startで繋げるのでbastionいらずになります。

CDK CloudFormation Disassemblerで、CloudFormationのyamlをcdk化してみた

久々の投稿であることには触れずに本題をw

最近CDKを触る機会が多いので、記事を書きやすいトピックを探していたのだが、おあつらえ向けのものがあったので書きます。

こんなものがあります。

www.npmjs.com

「CloudFormationのtemplate jsonを喰わせると、よしなにCDKのコードにして吐き出してくれるよ!」というやつ。 どんな感じに出力されるのか興味があったが、最近ついにこれを触る機会が訪れた。

CDK化する対象はこれ。

docs.aws.amazon.com

1 yamljsonに変換

dasmはjson -> CDKに変換するツールなので、このままだと使えない。 yamljsonに変換するツールを使ってなんとかjsonにする。

このときこれみたいなオンラインの変換サービスを使ってみると、 !Ref関数などがことごとくnullになってしまう。この辺をなんとか頑張ってjson化する。

json化すると(きっと)こうなる。

gist.github.com

2 dasmに喰わせる

さっそく喰わせてみる

% npm i -g cdk-dasm
% cdk-dasm < cloudformation.template.json > cloudformation-stack.ts

すると、こんなファイルが出来上がる。

// generated by cdk-dasm at 2019-11-16T08:07:46.297Z

import { Stack, StackProps, Construct, Fn } from '@aws-cdk/core';
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');
import ec2 = require('@aws-cdk/aws-ec2');

export class MyStack extends Stack {
    constructor(scope: Construct, id: string, props: StackProps = {}) {
        super(scope, id, props);
        new ec2.CfnVPC(this, 'VPC', {
            cidrBlock: {
              "Ref": "VpcCIDR"
            },
            enableDnsSupport: true,
            enableDnsHostnames: true,
            tags: [
              {
                "key": "Name",
                "value": {
                  "Ref": "EnvironmentName"
                }
              }
            ],
        });
        new ec2.CfnInternetGateway(this, 'InternetGateway', {
            tags: [
              {
                "key": "Name",
                "value": {
                  "Ref": "EnvironmentName"
                }
              }
            ],
        });
        new ec2.CfnVPCGatewayAttachment(this, 'InternetGatewayAttachment', {
            internetGatewayId: {
              "Ref": "InternetGateway"
            },
            vpcId: {
              "Ref": "VPC"
            },
        });
        new ec2.CfnSubnet(this, 'PublicSubnet1', {
            vpcId: {
              "Ref": "VPC"
            },
            availabilityZone: {
              "Fn::Select": [
                0,
                {
                  "fn:getAZs": ""
                }
              ]
            },
            cidrBlock: {
              "Ref": "PublicSubnet1CIDR"
            },
            mapPublicIpOnLaunch: true,
            tags: [
              {
                "key": "Name",
                "value": {
                  "Fn::Sub": "${EnvironmentName} Public Subnet (AZ1)"
                }
              }
            ],
        });
        new ec2.CfnSubnet(this, 'PublicSubnet2', {
            vpcId: {
              "Ref": "VPC"
            },
            availabilityZone: {
              "Fn::Select": [
                1,
                {
                  "Fn::GetAZs": ""
                }
              ]
            },
            cidrBlock: {
              "Ref": "PublicSubnet2CIDR"
            },
            mapPublicIpOnLaunch: true,
            tags: [
              {
                "key": "Name",
                "value": {
                  "Fn::Sub": "${EnvironmentName} Public Subnet (AZ1)"
                }
              }
            ],
        });
        new ec2.CfnSubnet(this, 'PrivateSubnet1', {
            vpcId: {
              "Ref": "VPC"
            },
            availabilityZone: {
              "Fn::Select": [
                0,
                {
                  "Fn::GetAZs": ""
                }
              ]
            },
            cidrBlock: {
              "Ref": "PrivateSubnet1CIDR"
            },
            mapPublicIpOnLaunch: false,
            tags: [
              {
                "key": "Name",
                "value": {
                  "Fn::Sub": "${EnvironmentName} Private Subnet (AZ1)"
                }
              }
            ],
        });
        new ec2.CfnSubnet(this, 'PrivateSubnet2', {
            vpcId: {
              "Ref": "VPC"
            },
            availabilityZone: {
              "Fn::Select": [
                1,
                {
                  "Fn::GetAZs": ""
                }
              ]
            },
            cidrBlock: {
              "Ref": "PrivateSubnet2CIDR"
            },
            mapPublicIpOnLaunch: false,
            tags: [
              {
                "key": "Name",
                "value": {
                  "Fn::Sub": "${EnvironmentName} Private Subnet (AZ2)"
                }
              }
            ],
        });
        new ec2.CfnEIP(this, 'NatGateway1EIP', {
            domain: "vpc",
        });
        new ec2.CfnEIP(this, 'NatGateway2EIP', {
            domain: "vpc",
        });
        new ec2.CfnNatGateway(this, 'NatGateway1', {
            allocationId: {
              "Fn::GetAtt": [
                "NatGateway1EIP",
                "AllocationId"
              ]
            },
            subnetId: {
              "Ref": "PublicSubnet1"
            },
        });
        new ec2.CfnNatGateway(this, 'NatGateway2', {
            allocationId: {
              "Fn::GetAtt": [
                "NatGateway2EIP",
                "AllocationId"
              ]
            },
            subnetId: {
              "Ref": "PublicSubnet2"
            },
        });
        new ec2.CfnRouteTable(this, 'PublicRouteTable', {
            vpcId: {
              "Ref": "VPC"
            },
            tags: [
              {
                "key": "Name",
                "value": {
                  "Fn::Sub": "${EnvironmentName} Public Routes"
                }
              }
            ],
        });
        new ec2.CfnRoute(this, 'DefaultPublicRoute', {
            routeTableId: {
              "Ref": "PublicRouteTable"
            },
            destinationCidrBlock: "0.0.0.0/0",
            gatewayId: {
              "Ref": "InternetGateway"
            },
        });
        new ec2.CfnSubnetRouteTableAssociation(this, 'PublicSubnet1RouteTableAssociation', {
            routeTableId: {
              "Ref": "PublicRouteTable"
            },
            subnetId: {
              "Ref": "PublicSubnet1"
            },
        });
        new ec2.CfnSubnetRouteTableAssociation(this, 'PublicSubnet2RouteTableAssociation', {
            routeTableId: {
              "Ref": "PublicRouteTable"
            },
            subnetId: {
              "Ref": "PublicSubnet2"
            },
        });
        new ec2.CfnRouteTable(this, 'PrivateRouteTable1', {
            vpcId: {
              "Ref": "VPC"
            },
            tags: [
              {
                "key": "Name",
                "value": {
                  "Fn::Sub": "${EnvironmentName} Private Routes (AZ1)"
                }
              }
            ],
        });
        new ec2.CfnRoute(this, 'DefaultPrivateRoute1', {
            routeTableId: {
              "Ref": "PrivateRouteTable1"
            },
            destinationCidrBlock: "0.0.0.0/0",
            natGatewayId: {
              "Ref": "NatGateway1"
            },
        });
        new ec2.CfnSubnetRouteTableAssociation(this, 'PrivateSubnet1RouteTableAssociation', {
            routeTableId: {
              "Ref": "PrivateRouteTable1"
            },
            subnetId: {
              "Ref": "PrivateSubnet1"
            },
        });
        new ec2.CfnRouteTable(this, 'PrivateRouteTable2', {
            vpcId: {
              "Ref": "VPC"
            },
            tags: [
              {
                "key": "Name",
                "value": {
                  "Fn::Sub": "${EnvironmentName} Private Routes (AZ2)"
                }
              }
            ],
        });
        new ec2.CfnRoute(this, 'DefaultPrivateRoute2', {
            routeTableId: {
              "Ref": "PrivateRouteTable2"
            },
            destinationCidrBlock: "0.0.0.0/0",
            natGatewayId: {
              "Ref": "NatGateway2"
            },
        });
        new ec2.CfnSubnetRouteTableAssociation(this, 'PrivateSubnet2RouteTableAssociation', {
            routeTableId: {
              "Ref": "PrivateRouteTable2"
            },
            subnetId: {
              "Ref": "PrivateSubnet2"
            },
        });
        new ec2.CfnSecurityGroup(this, 'NoIngressSecurityGroup', {
            groupName: "no-ingress-sg",
            groupDescription: "Security group with no ingress rule",
            vpcId: {
              "Ref": "VPC"
            },
        });
    }
}

うーん、やはりまだ安定番ではないだけのことはあってすごいコードだ・・・。

3 手で修正する

ここからは、cdk deployが通る状態になるまで、手で修正するしかない。 まぁ、「どのクラスを使うか」というのは網羅しているので、愚直に直していく。

なお、Parametersは固定値にした。

// generated by cdk-dasm at 2019-11-16T08:07:46.297Z

import { Stack, StackProps, Construct, Fn } from "@aws-cdk/core";
import ec2 = require("@aws-cdk/aws-ec2");

export class MyStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps = {}) {
    super(scope, id, props);
    const environmentName = "development";
    const vpcCidr = "10.192.0.0/16";
    const publicSubnet1CIDR = "10.192.10.0/24";
    const publicSubnet2CIDR = "10.192.11.0/24";
    const privateSubnet1CIDR = "10.192.20.0/24";
    const privateSubnet2CIDR = "10.192.21.0/24";

    const vpc = new ec2.CfnVPC(this, "VPC", {
      cidrBlock: vpcCidr,
      enableDnsSupport: true,
      enableDnsHostnames: true,
      tags: [
        {
          key: "Name",
          value: environmentName
        }
      ]
    });
    const internetGateway = new ec2.CfnInternetGateway(
      this,
      "InternetGateway",
      {
        tags: [
          {
            key: "Name",
            value: environmentName
          }
        ]
      }
    );
    new ec2.CfnVPCGatewayAttachment(this, "InternetGatewayAttachment", {
      internetGatewayId: internetGateway.ref,
      vpcId: vpc.ref
    }).addDependsOn(vpc);
    const publicSubnet1 = new ec2.CfnSubnet(this, "PublicSubnet1", {
      vpcId: vpc.ref,
      availabilityZone: Fn.select(0, Fn.getAzs(props.env!.region)),
      cidrBlock: publicSubnet1CIDR,
      mapPublicIpOnLaunch: true,
      tags: [
        {
          key: "Name",
          value: `${environmentName} Public Subnet (AZ1)`
        }
      ]
    });
    publicSubnet1.addDependsOn(vpc);
    const publicSubnet2 = new ec2.CfnSubnet(this, "PublicSubnet2", {
      vpcId: vpc.ref,
      availabilityZone: Fn.select(1, Fn.getAzs(props.env!.region)),
      cidrBlock: publicSubnet2CIDR,
      mapPublicIpOnLaunch: true,
      tags: [
        {
          key: "Name",
          value: `${environmentName} Public Subnet (AZ1)`
        }
      ]
    });
    publicSubnet2.addDependsOn(vpc);
    const privateSubnet1 = new ec2.CfnSubnet(this, "PrivateSubnet1", {
      vpcId: vpc.ref,
      availabilityZone: Fn.select(0, Fn.getAzs(props.env!.region)),
      cidrBlock: privateSubnet1CIDR,
      mapPublicIpOnLaunch: false,
      tags: [
        {
          key: "Name",
          value: `${environmentName} Private Subnet (AZ1)`
        }
      ]
    });
    privateSubnet1.addDependsOn(vpc);
    const privateSubnet2 = new ec2.CfnSubnet(this, "PrivateSubnet2", {
      vpcId: vpc.ref,
      availabilityZone: Fn.select(1, Fn.getAzs(props.env!.region)),
      cidrBlock: privateSubnet2CIDR,
      mapPublicIpOnLaunch: false,
      tags: [
        {
          key: "Name",
          value: `${environmentName} Private Subnet (AZ2)`
        }
      ]
    });
    privateSubnet2.addDependsOn(vpc);
    const natGateway1Eip = new ec2.CfnEIP(this, "NatGateway1EIP", {
      domain: "vpc"
    });
    const natGateway2Eip = new ec2.CfnEIP(this, "NatGateway2EIP", {
      domain: "vpc"
    });
    const natGateway1 = new ec2.CfnNatGateway(this, "NatGateway1", {
      allocationId: natGateway1Eip.attrAllocationId,
      subnetId: publicSubnet1.ref
    });
    natGateway1.addDependsOn(natGateway1Eip);
    natGateway1.addDependsOn(publicSubnet1);
    const natGateway2 = new ec2.CfnNatGateway(this, "NatGateway2", {
      allocationId: natGateway2Eip.attrAllocationId,
      subnetId: publicSubnet2.ref
    });
    natGateway2.addDependsOn(natGateway2Eip);
    natGateway2.addDependsOn(publicSubnet2);
    const publicRouteTable = new ec2.CfnRouteTable(this, "PublicRouteTable", {
      vpcId: vpc.ref,
      tags: [
        {
          key: "Name",
          value: `${environmentName} Public Routes`
        }
      ]
    });
    publicRouteTable.addDependsOn(vpc);
    const defaultPublicRoute = new ec2.CfnRoute(this, "DefaultPublicRoute", {
      routeTableId: publicRouteTable.ref,
      destinationCidrBlock: "0.0.0.0/0",
      gatewayId: internetGateway.ref
    });
    defaultPublicRoute.addDependsOn(publicRouteTable);
    defaultPublicRoute.addDependsOn(internetGateway);
    const publicSubnet1RouteTableAssociation = new ec2.CfnSubnetRouteTableAssociation(
      this,
      "PublicSubnet1RouteTableAssociation",
      {
        routeTableId: publicRouteTable.ref,
        subnetId: publicSubnet1.ref
      }
    );
    publicSubnet1RouteTableAssociation.addDependsOn(publicRouteTable);
    publicSubnet1RouteTableAssociation.addDependsOn(publicSubnet1);

    const publicSubnet2RouteTableAssociation = new ec2.CfnSubnetRouteTableAssociation(
      this,
      "PublicSubnet2RouteTableAssociation",
      {
        routeTableId: publicRouteTable.ref,
        subnetId: publicSubnet2.ref
      }
    );
    publicSubnet2RouteTableAssociation.addDependsOn(publicRouteTable);
    publicSubnet2RouteTableAssociation.addDependsOn(publicSubnet2);
    const privateRouteTable1 = new ec2.CfnRouteTable(
      this,
      "PrivateRouteTable1",
      {
        vpcId: vpc.ref,
        tags: [
          {
            key: "Name",
            value: `${environmentName} Private Routes (AZ1)`
          }
        ]
      }
    );
    privateRouteTable1.addDependsOn(vpc);
    const defaultPrivateRoute1 = new ec2.CfnRoute(
      this,
      "DefaultPrivateRoute1",
      {
        routeTableId: privateRouteTable1.ref,
        destinationCidrBlock: "0.0.0.0/0",
        natGatewayId: natGateway1.ref
      }
    );
    defaultPrivateRoute1.addDependsOn(privateRouteTable1);
    defaultPrivateRoute1.addDependsOn(natGateway1);
    const privateSubnet1RouteTableAssociation = new ec2.CfnSubnetRouteTableAssociation(
      this,
      "PrivateSubnet1RouteTableAssociation",
      {
        routeTableId: privateRouteTable1.ref,
        subnetId: privateSubnet1.ref
      }
    );
    privateSubnet1RouteTableAssociation.addDependsOn(privateRouteTable1);
    privateSubnet1RouteTableAssociation.addDependsOn(privateSubnet1);
    const privateRouteTable2 = new ec2.CfnRouteTable(
      this,
      "PrivateRouteTable2",
      {
        vpcId: vpc.ref,
        tags: [
          {
            key: "Name",
            value: `${environmentName} Private Routes (AZ2)`
          }
        ]
      }
    );
    privateRouteTable2.addDependsOn(vpc);
    const defaultPrivateRoute2 = new ec2.CfnRoute(
      this,
      "DefaultPrivateRoute2",
      {
        routeTableId: privateRouteTable2.ref,
        destinationCidrBlock: "0.0.0.0/0",
        natGatewayId: natGateway2.ref
      }
    );
    defaultPrivateRoute2.addDependsOn(privateRouteTable2);
    defaultPrivateRoute2.addDependsOn(natGateway2);
    const privateSubnet2RouteTableAssociation = new ec2.CfnSubnetRouteTableAssociation(
      this,
      "PrivateSubnet2RouteTableAssociation",
      {
        routeTableId: privateRouteTable2.ref,
        subnetId: privateSubnet2.ref
      }
    );
    privateSubnet2RouteTableAssociation.addDependsOn(privateRouteTable2);
    privateSubnet2RouteTableAssociation.addDependsOn(privateSubnet2);
    new ec2.CfnSecurityGroup(this, "NoIngressSecurityGroup", {
      groupName: "no-ingress-sg",
      groupDescription: "Security group with no ingress rule",
      vpcId: vpc.ref
    }).addDependsOn(vpc);
  }
}

SRE本3章 - リスクの受容

では引き続き、「SRE サイトリライアビリティエンジニアリング」の、今日は3章を見ていきます。

この章は冒頭でこのように書かれており、非常に重要な章であることがわかります。

「SREの仕事とはどういうものか」、そして「なぜそのような仕事をするのか」
についての幅広い見地を得たい方にとっては、最重要となる必読の章です。

では、さっそく見ていきます。

この章では、大きく4つのことが書かれています。

  1. リスクの管理
  2. サービスリスクの計測
  3. サービスのリスク許容度
  4. エラーバジェットの活用

個人的に特に面白かったのは、4. エラーバジェットの活用 でした。 一つ一つ、いつものように雑にまとめてみましょう。

1. リスクの管理

信頼性のないシステムは急速にユーザーの信頼を失うことになるので、可能性を減らすためのコストは支払う必要があります。 このコストには2つの特徴があります。

  • 冗長なマシン / コンピュートリソースのコスト
    設備の冗長化にかけるコスト。

  • 機会のコスト
    リスクを減らすためのシステムや機能を構築するために組織がエンジニアリングリソースを割り当てることから生じるコスト

SREでは、サービスの信頼性を管理する大部分は、リスクを管理することによって行います。
下記2つは、等しく重要なポイントです。

  • システムの信頼性を高めるためにエンジニアリングを見いだすこと
  • 動作させるサービスの適切な障害許容レベルを定めること

サービスを十分信頼できるものにするための努力はしつつも、必要以上に信頼性を高めようとはしない、というのも重要なポイントです。

2. サービスリスクの計測

Googleが注目している客観的なリスク検知のメトリクスは、計画外の停止時間です。
この計画外の停止時間は、サービスの可溶性に求められるレベルによって把握できます。

可用性=\frac{稼働時間}{稼働時間+停止時間}

ただ、Googleのように24/7で世界中のどこかからサービスのトラフィックを少なくとも一部を提供している可能性が高いとなると、時間を基にしたメトリクスは意味を持たないので、リクエストの成功率の観点から可用性を定義しています。

可用性=\frac{成功したリクエスト数}{総リクエスト数}

このサービス可用性のターゲットを四半期単位で設定し、そのターゲットに対して週単位、もしくは日単位でパフォーマンスを追跡しています。

3. サービスリスクの許容度

サービスリスクの許容度を特定するためには、SREがプロダクトのマネージャーと共同で作業を行い、一連のビジネスゴールをエンジニアリング可能で明確な目標に変換する必要があります。
この共同作業は、現実には簡単ではなく、本の中ではコンシューマサービスとインフラストラクチャサービスの場合で分けて解説されていますが、共通するリスク許容度の評価に関しては、以下の4点が挙げられています。

  • 必要な可用性のレベル
  • 障害の種類の違いによってサービスへの影響に差はないか
  • リスク曲線状にサービスを位置付ける上で、サービスコストはどのように利用できるか
  • 考慮すべき重要なサービスメトリクスとして、他にはどのようなものがあるか

4. エラーバジェットの活用

プロダクト開発者のパフォーマンスがプロダクトの開発速度で主に評価されているのに対し、SREのパフォーマンスはサービスの信頼性を基にして評価されます。
そのため、この両者には本来的な緊張が存在していますが、エラーバジェットはこの緊張を客観的に解消することのできる手法です。

エラーバジェットは、Googleでは以下のように形成されます。

  • プロダクトマネージャーがSLOを規定。四半期内に期待されるサービスの稼働時間を設定
  • 実際の稼働時間は、中立な第三者であるモニタリングシステムによって計測
  • この2つの値の差異が、その四半期内の「損失可能な信頼性」という「予算」の残分となる
  • 計測された稼働時間がSLOを超えている、言い換えればエラーバジェットがまだ残っているなら新しいリリースをプッシュできる

非常にわかりやすいですね。
これなら社内政治等のわかりづらい手法で両者の対立が激化することなく、プロダクト開発とSREとのバランスを保つことができます。
もしエラーバジェットが枯渇したタイミングで、どうしてもその四半期内に追加のプロダクト開発を行いリリースをプッシュしたいときは、SLOを緩和することによってエラーバジェットを増やす、という選択肢を取ることもできます。

まとめ

  • サービスの信頼性管理の大部分はリスク管理に関係することであり、リスク管理には多くのコストがかかることがある
  • 信頼性のターゲットとして100%が適切であることはほぼありえない。
  • エラーバジェットは、SREとプロダクト開発者との間でインセンティブを一致させると共に、共同所有ということを強調する

SRE本2章 - SREの観点から見たGoogleのプロダクション環境

昨日ポエムだったので、今日もポエムを書こうかとも思ったんですが、それだと一体なんのためにやり始めたのかさっぱりわからなくなるので、やっぱりやめました。

2章 SREの観点から見たGoogleのプロダクション環境

1章からガラッと変わって、2章は非常に具体的にGoogleのデータセンターで実際に稼働しているシステムについて語られています。

ちょっとね、正直に言いますが、

この章、難しすぎて全然わかりません

↑この感想だけで終わりたいくらいだけど、それだとポエム以下になるのでまぁもう少し書くけれども・・・。

とりあえず、わかったところを箇条書きにします。間違っている可能性しかないです。

  1. ハードウェア
    • Googleでは、下記2用語の意味合いが通常とは少し異なる
      • マシン: 1つのハードウェア(あるいは1つのVM)
      • サーバー: サービスを実装しているソフトウェア
    • 数十台のマシンが1つのラックに配置されている
    • 複数のラックが1列に並べられている
    • 1つないし複数の列は、1つのクラスタを構成する
    • 通常は1つのデータセンターのビルディングは、複数のクラスタを格納する
    • 近くに配置されたデータセンターのビルディング群はキャンパスを構成する
    • データセンター内のマシン群は互いに通信する必要があるため、Jupiterと呼ばれるClosネットワーク装置で接続されている
    • データセンター群は、SDN(Software Defined Network)アーキテクチャであるB4で相互接続されている
  2. ハードウェアを組織化するシステムソフトウェア
    • マシン群の管理を行うBorg
    • 複数のレイヤーから構成されているストレージ
    • 低価格だが高速に動作する「ダム」スイッチングハードウェア等を使って構築しているネットワーク
    • 複数箇所のデータセンターにまたがって扱うロックサービスのChubby
  3. ソフトウェアインフラストラクチャ
    • すべてのサービスは、Stubbyと呼ばれるRPCを使って通信(StubbyのオープンソースバージョンとしてはgRPCがある)
  4. 開発環境
    • ソフトウェアのビルド時、データセンター内のビルドサーバーにビルドリクエストを送る
    • コンパイルは、多くのビルドサーバーが並列に行うため、大規模なビルドであってもすぐに実行される
    • push-on-greenシステムで、テストをパスした新バージョンが自動的にプロダクション環境に送られるプロジェクトもある

その後、サービスがGoogleのプロダクション環境にデプロイされる様子がわかりやすく例を用いて書かれています。



ま、きっとこの章は今後何度も読むんだろうなきっと。

毎日技術的なことを書くはずが・・・今日はショートポエム

ちょっとわけあって、昨日の続編が今日は書けません。

明日は書けるかなぁ・・・再開は明後日な気がしますね。

なので今日はショートポエムを一つ。

先週から、キックボクシングジムに通い始めたのですが(この系列のどこかに通っております)、結構きついんですよ、カリキュラムが。
1分間サンドバッグを打ちまくってはちょっと休憩してまた1分打ちまくる、みたいなことを何度も何度もやるんです。

そのとき、あーすごくこれって大事だなって思ったのが、とにかくその毎ターンを全力でやる、ということ。
いろんなところで手を抜こうと思えば抜けるんですよね。パンチをのろく打ったり、各動作間のインターバルを少しずつ増やしていったり。
でも、「もうこの1分の後にどうなってもいい!とにかく毎回フルパワーでいくんだ!!」って思いながらやると、意外とサボらずともいけちゃったりする。
で、サボったときと疲労感もそこまで変わらないのに、全力を出し切ったときの方が、いろいろと得られるものが多く感じられるんですよね。

なんか、これが僕にとってはとても新鮮な感覚で。

そして、これって他の様々なことにも言えると思って。
仕事も、家事も、どっちにしろやらないといけないし、どっちにしろ疲れるんだから、全力でやると。これが大事なことなんだなと。


という、以上今日はショートポエム回でした。
微妙にお酒が入って、ものすごく眠い中書いております。

SRE本1章 - 作業のうち50%はエンジニアリングにあてるべし

昨日宣言した通り、「SRE サイトリライアビリティエンジニアリング」の1章について書きたいと思います。

1章 イントロダクション

イントロダクションなので、これ先この本にどのようなことがかかれているのかというのがまずは網羅的に紹介されています。
ただ、イントロダクションの時点ですでにおもしろいし、勉強になる!
どんどん先を読みたくなります。

そもそもサイトリライアビリティエンジニアリング(SRE)とは、Googleで培われたシステム管理とサービス運用の方法論です。こちらの本では、実際にGoogleにおいて、どのような理念に基づき、具体的にどのような手法で運用されているのかということが書かれています。

全部で547ページ!大作です(索引込み)。

Googleで誕生したSREですが、Google社では「SREメンバーの作業における50%は、エンジニアリングであること」というルールがきっちり決まっているそうです。
緊急のオンコール対応や、手作業のルーチンワークみたいなことは、必ず全体の50%以下の時間で行わなければいけません。それを超えないように、SREはがんがん自動化していくのです。その結果が、たとえ複雑なものとなろうとも。

もし、あるサービスがなんらかのSREメンバーが行う手作業に依存してしまうと、そのサービスがスケールしていくごとに手作業の量もどんどん膨らんでいきます。
そうならないよう、個々のメンバーの作業内訳をしっかりと計測し、エンジニアリングが50%を下回ってしまうようなときにはなんらかの手を打つわけです。

僕がここでとても興味があるのは、「どのように個々のメンバーの作業内訳を計測しているのか」という点です。
メンバーが日報などで、「あー今日何やったかなぁ」などと振り返りながらやるのは非常に面倒だし、僕ならそんなことを毎日やりたくはありません。第一、正確に計測できているとも思えません。

この先を読むと出てくるのか、それともそこについては各チームで工夫すべきところなのか。
まぁまずは先を読んでみたいと思います。

なんかこの部分を読んだだけでも、これまで僕がやってきたことを振り返ってみて、手作業が多かったなぁ俺・・・と落ち込みました。デプロイとか、おかしなインスタンスにはアクセスが行かないようにして・・・とか、もっと自動化しちゃえばよかったなぁ、と。

まぁ1章はイントロダクションなので、さらっとこんな感じですかね。 明日は2章 SREの観点から見たGoogleのプロダクション環境を見ていきたいと思います。