計画と改善

エンジニアのブログ

ALB+Auto Scaling+EC2環境の構築

今回はインフラのハンズオンでの学習を行なった。デフォルトVPCを利用して、Auto Scalingでパブリックサブネット(EC2インスタンスロードバランサーからのみアクセスを許可)にEC2インスタンスを異なる2つのAZに1台ずつ配置し、ALBでトラフィック制御を行う構成にしてみた。ほぼ自分用のメモ。

できたのは以下。やりながら思ったが、ネットワークの設定はいつも手探りでいつも自信がない。

毎回結構な時間をかけて構築するが、ちょっとしたミスに気が付かず苦戦をする。(今回はapachemod_sslモジュールの入れ忘れによる502エラー)

ALBの作成

前提としてAuto Scalingグループと同じAZでなければならない。

事前準備としてまずACMの設定から行う。(HTTPSを設定するため、ドメインを取得し、証明書を作成、DNS検証を行う)

ACMの設定

  1. ACMAmazon Certificate Manager)から証明書をリクエストを選択
  2. 証明書タイプからパブリック証明書をリクエストを選択(AWS Certificate Manager でプロビジョニングされたパブリック SSL/TLS 証明書は無料、プライベート証明書の場合は料金がかかってくる)
  3. 設定するドメイン名を入力する
  4. 検証方法にDNSを選択する。DNSの検証についてはドキュメント参照
    1. CNAMEは証明書を自動更新するために ACM が使用する AWS ドメインを指すエイリアスで、ユーザーのドメインおよびアカウントに固有のものとして作成される。各レコードには名前と値が含まれる
    2. リクエストを選択
    3. ドメイン側の管理画面(今回は外部レジストラのお名前.comを使用。理由は安いから)のDNSの設定でCNAMEの欄に取得したCNAME 名とCNAME 値を入力する
    4. しばらくするとACMの証明書発行ステータスが発行済みになる(私の場合大体1時間くらいかかった)

ALBの設定

  1. EC2コンソールからロードバランサーを選択
  2. ALBの作成を選択
  3. ロードバランサープロトコル(リスナー)を選択
    1. HTTPとHTTPSを追加
  4. アベイラビリティゾーンの追加
    1. VPCとサブネットを選択
      1. ap-northeast-1d、ap-northeast-1cを選択する
  5. セキュリティ設定の作成画面に進む
  6. 証明書のタイプを選択
    1. ACMから証明書を選択する
    2. 証明書の名前で先ほどACMで作成した証明書を選択する
  7. セキュリティポリシーはデフォルトで次へ
  8. セキュリティグループの設定
    1. 今回は新しいセキュリティグループを作成した
      1. 443(HTTPS)を許可しておく
  9. ルーティングの設定
    1. ターゲットグループを新規で作成
    2. 名前を入力
    3. ターゲットの種類にインスタンスを選択
    4. プロトコルHTTPS、ポートは443
    5. HTTP1を選択
    6. ヘルスチェックはHTTPSに変更
  10. ターゲットの登録
    1. 何も設定せずに進む(Auto Scalingの方でアタッチする)
  11. 作成

Route53の設定

  • ホストゾーン作成を選択
  • ドメイン名を入力
  • ホストゾーン作成
  • レコード作成を選択
  • エイリアス
    • トラフィックのルーティング先(以下を選択)
      • Application Load BalancerとClassic Load Balancerへのエイリアス
      • アジアパシフィック(東京)
      • 作成したALB
  • レコードを作成
  • 外部レジストラドメイン管理画面のDNSの設定でRoute53に記載されているNSレコードを設定する
  • あとは時々nslookup -type=NS ドメイン名をしてネームサーバーが書き変わるまで待つ。大体40時間くらいかかった

Auto Scalingで起動テンプレートを作成

前提として事前にEC2インスタンスのAMIを作成しておくこと。

今回はAMIの元になるEC2インスタンスApatchサーバーをインストールし、確認用のhtmlファイルが表示されるように設定している。

  1. インスタンスナビゲーションから起動テンプレートを選択する
  2. 名前と説明を入れる
  3. Auto Scaling のガイダンスにチェックを入れる
  4. AMIを選択
  5. AMIと互換性のあるインスタンスタイプを選択する
    1. ここではt2microとした
  6. キーペアは既存のキーペアを選択した
  7. セキュリティグループを設定
    1. 既存のHTTPS(ALBのセキュリティグループのみ許可)、SSHを許可するものを設定
  8. 起動テンプレート作成

起動テンプレートを使用してAuto Scalingグループを作成

  1. EC2ナビゲーションからAuto Scalingグループを選択
  2. 起動テンプレートと同じリージョンでAuto Scalingグループの作成を選択
  3. 名前を入力
  4. 起動テンプレートを選択
  5. VPCとサブネットを選択
    1. ap-northeast-1d、ap-northeast-1cを選択する
  6. 詳細オプション
    1. ロードバランサーの設定
      1. 既存のロードバランサーにアタッチする
      2. ロードバランサーのターゲットグループから選択する
      3. 先ほど作成したロードバランサーのターゲットグループを選択する
    2. ヘルスチェックの設定
      1. ELBのヘルスチェックにもチェックを入れる
  7. CloudWatchメトリクスの設定
    1. 今回はチェックを入れずに次に進む
  8. デフォルトインスタンスのウォームアップの設定
    1. CloudWatchに関わる機能。今回はチェックを入れずに進む
  9. グループサイズとスケーリングポリシー画面に進む
  10. グループサイズ
    1. 希望する容量は起動するインスタンスの初期数。最小、最大もここで設定可能
    2. 今回は2,2,2で設定
  11. スケーリングポリシー
    1. Auto Scalingグループのサイズを自動的にスケーリングするにはターゲット追跡スケーリングポリシーを選択し、指示にしたがう
      1. ここでメトリクスを選択し、どんな時にスケーリングをするか選択できる
        1. CPU使用率
        2. 平均ネットワーク入力
        3. 平均ネットワーク出力
        4. ターゲットごとのALBリクエスト数
    2. 今回はなしで設定
  12. インスタンスのスケールイン保護
    1. 今回はデフォルトの保護を有効にするにチェックを入れずに次に進む
  13. 通知を追加する
    1. 受信者にメールアドレスを設定
      1. topicを作成。名前はお好きなように
  14. イベントタイプにチェックを入れたままにする。
    1. 起動、終了、起動失敗、終了失敗
  15. タグは何も入れずそのまま作成
  16. EC2コンソールのナビゲーションAuto Scalingグループに追加されている
  17. SNSの方も確認するとTopicが作成されていることが確認できる
    1. メールが届くので確認すること
  18. EC2インスタンスの画面を確認するとAuto Scalingグループで作成したインスタンスがrunningとなっている
  19. パブリックIPにアクセスすると元々Apatchサーバーに設定していたHello Worldが表示される
  20. 試しにインスタンスを終了してみると、そのインスタンスは終了し、設定したアベイラビリティゾーンに他のインスタンスが立ち上がる
    1. SNSを通して、インスタンスが終了したことと、起動したというメールも通知される。
  21. auto scalingグループを削除するとそれに紐づくインスタンスも削除される
  22. インスタンスを終了するとそれに紐づくEBS ボリュームも削除された

502 Bad Gateway

途中502 Bad Gatewayが出て大変苦しんだが、apacheSSLの設定をし忘れていただけだった。

sudo yum -y install mod_ssl

オブジェクト指向でなぜつくるのか後半まとめ

オブジェクト指向でなぜつくるのか。後半まとめと感想

デザインパターン

オブジェクト指向を利用すると独立性が高まり、ソフトウェア部品を再利用することができる。ことがメリットであったが、デザインパターンのように、ソフトウェアそのものではなく、ソフトウェアを作る際に用いられたアイデアを再利用することもある。

代表的なのがGoFによるデザインパターン。ソフトウェアを開発するときは通常、一から組み上げず、先人たちが数々の難所を乗り越え、試行錯誤によって築き上げた設計知識の集積を用いて開発を行われる。

オブジェクト指向が登場した後の各言語の標準ライブラリにはデザインパターンの考え方を用いて提供されているものもある。

OOPUML

OOPは現実世界を全てソフトウェアに置き換えるような考え方はできない。ソフトウェアは現実世界の仕事の一部をシステム化して肩代わりしているものに過ぎない。

OOPは二つの側面があり、これらは分けて考える必要がある。1つはプログラミング的な側面(クラスでサブルーチンやインスタンス変数をまとめるとか、カプセル化ポリモーフィズム、継承など)、もう1つは汎用的な整理術という側面。

この二つの側面は前者を下流工程、後者を上流工程に分類することができる。

OOPの概念にクラスとインスタンスの関係があるが、これを集合と要素の考え方として捉えることができ、スーパークラスとサブクラス(継承関係)は全体集合と部分集合という考え方に分けることができる。

こうした集合論的な考え方は上流工程において、現実世界の役割を整理したり、説明したりするのに用いられている。

例えば上流工程で用いられるUML(処理の流れやデータ構造を図示できるモデリング言語)の中にクラス図があるが、これを用いると現実世界の物事を集合の考え方で分類できる。

具体的には電化製品が全体集合、冷蔵庫が部分集合のような考え方ができる。

またUMLを使うと現実世界のそれぞれの役割をその関係性とやりとりから図示することができる。そうしたことができるツールはいくつかあるが、例えばコミュニケーション図を用いると、個々のオブジェクトを線で結び、メッセージパッシング(メソッド呼び出し)をオブジェクト同士の関係に着目しながら図示することができる。

UMLではそうした現実世界の説明に役立てることもできるが、ビジネスアプリケーションにおいては先ほど挙げたクラス図を用いると、現実世界に存在するものを構造化されたソフトウェアデータとしてそのまま映し取ることができる。これを概念データモデルという。

概念データモデルはソフトウェアのデータモデルであることから、これがデータベースの構造にそのまま反映される。

以降、設計の話やアジャイル開発、関数型言語等は省略

感想

オブジェクト指向はプログラミングをしていると自然と触れているものではあるが、その成り立ちは、OOP登場以前の技術的な課題を解決するものであり、大変興味深かった。学習してみると下流工程から上流工程まで幅広くカバーできる概念でもあり、オブジェクト指向の考え方が取り入れられた個別の技術をもっと学ぶ必要があると感じた。またデザインパターンの書籍なども読んで理解を深めていきたいと思う。

書籍的にも体系的にまとまっていて、初心者から中級者までおすすめできる良い書籍だった。

オブジェクト指向でなぜつくるのか前半まとめ

オブジェクト指向でなぜつくるのか。前半部分のみ主なポイントと解釈をまとめた。 なおサンプルコードは全てJava

オブジェクト指向(Object Oriented Programming)とは

物を中心にソフトウェアを組み上げる開発手法。個々の部品の独立性を高め、修正の影響範囲を限定する。独立性を高めることで、再利用性が高まり、それらを組み合わせてソフトウェアを作ることができる。

オブジェクト指向の三代要素として、クラス(カプセル化)、ポリモーフィズム、継承という概念がある。

OOPが誕生するまでの歴史的経緯

機械語アセンブラ言語

コンピュータは二進数で記述された機械語しか理解できない。 そのため、アセンブリ言語と呼ばれるアセンブラと呼ばれるコンパイラ機械語を生成するプログラミング言語が登場した。しかしアセンブリ言語は実行命令を一つ一つ記述していく形式を取っており、人間にとっては煩雑なプログラムであった。

高級言語の登場

アセンブラ言語のような人間ではわかりづらい命令を一つ一つ記述していく形式からより人間にわかりやすい計算式を使った文法表現に進化した。COBOLFORTRANなどの高級言語の登場によりプログラミングの生産性や品質が大きく登場した。

構造化プログラミングの登場

構造化プログラミングとは簡単に言えばプログラムの構造をわかりやすく記述しようというもの。

オランダ人の学者エドガー・ダイクストラ氏によって提唱された考え方。

GOTO文と呼ばれる所定の位置に処理を移動させる構文があったが、スパゲティコードになりやすかったため、それを廃止し、ロジックを順次進行、条件分岐、繰り返しの3つの構造だけで表現することが推奨された。現在で普通に利用されているif文やcase文やfor文などを利用してプログラミングを行うということ。

サブルーチンの独立性を高める動き

サブルーチンとは関数のこと。対して呼び出し側はメインルーチンと呼ぶ。

サブリーチンの独立性を高める動きとは、サブルーチンの中で共有する情報を少なくする動きのことを指す。ここでいう共有する情報とはグローバル変数のこと。

複数のサブルーチンで変数を共有してしまうと、変数がどこで書き変わっているのか突き止めるのが困難になってしまい、保守性が下がってしまう。

そこで、ローカル変数と引数の値渡しという仕組みが導入された。

Javaだと以下のように書ける。引数は値渡しとなるので、xTo999関数に引数としてxを渡し、xを変更してもメインルーチンには影響しない。(サブルーチンが独立している)

public class Main {
  public static void xTo999(int x) {
    x = 999;
  }

  public static void main(String[] args) {
    int x = 10;
    System.out.println(x);//10
    xTo999(x);
    System.out.println(x);//10
  }
}

貧弱な再利用の問題

構造化言語で再利用可能だったのはサブルーチンのみであった。この頃入出力計算、数値計算、文字列処理計算などの基本的な汎用ライブラリは存在していたが、莫大するアプリケーションの規模からすると、より大規模な再利用の必要性は高まっていた。

OOPはその解決策として登場した技術である。

クラス

まとめて隠してたくさん作る仕組み。

  1. サブルーチンと変数をまとめる
  2. クラス内部だけで使用するサブルーチンと変数を隠す
  3. 一つのクラスからインスタンスをたくさん作る

OOPではクラスに定義されたサブルーチンをメソッドと呼び、グローバル変数インスタンス変数と呼ぶ。実際にはメソッドやインスタンス変数は、公開するスコープを限定して利用することもできる。

public class Main {
  public static void main(String[] args) {
    Calculator calculator = new Calculator(10, 20);// インスタンスを作る
    int result = calculator.calc();
    System.out.println(result); // 30

    Calculator otherCalculator = new Calculator(30, 50);// 別のインスタンスを作る
    int otherResult = otherCalculator.calc();
    System.out.println(otherResult); // 80
  }
}
// classにサブルーチン(メソッド)と変数をまとめる
class Calculator {

  private int num1; //privateでスコープを限定して隠す
  private int num2; //privateでスコープを限定して隠す

  public Calculator(int num1, int num2) {
    this.num1 = num1;
    this.num2 = num2;
  }

  public int calc() {
    return this.num1 + this.num2;
  }
}

ポリモーフィズム

共通メインルーチンを作る仕組み。共通サブルーチンは呼び出されるロジックを一つにまとめるが、共通メインルーチンは呼び出すロジックを一本化する。

呼び出す側としては実際に呼び出す処理に関係なく、同じように呼び出す。

public class Main {
  public static void main(String[] args) {
    Calculator calculator = new Add();//ここをnew Multipleに変更してもgetResultのロジックは変わらない
    int result = getResult(calculator, 10, 20);
    System.out.println(result);

  }

  public static int getResult(Calculator calculator, int num1, int num2) {
    return calculator.calc(num1, num2);
  }
}
class Calculator {
  public int calc(int num1, int num2) {
    return 0;
  }
}
class Add extends Calculator {
    @Override
  public int calc(int num1, int num2) {
    return num1 + num2;
  }
}
class Multiple extends Calculator {
    @Override
  public int calc(int num1, int num2) {
    return num1 * num2;
  }
}

継承

クラスの共通部分を別クラスにまとめる仕組み。共通メインルーチンならぬ共通クラス。

継承される共通クラスをスーパークラスと呼び、継承するクラスをサブクラスと呼ぶ。

以下のようにスーパークラスのメソッドをサブクラスで定義していなくても継承(extends)することで利用可能。

public class Main {
  public static void main(String[] args) {
    Computer computer = new Computer("MacBook Pro", 280000, "8core", "16GB", "512GB SSD");
    String name = computer.getName();
    System.out.println(name); // MacBook Pro
  }
}
class Product {
  private String name;
  private int price;

  public Product(String name, int price) {
    this.name = name;
    this.price = price;
  }

  public String getName() {
    return this.name;
  }

  public int getPrice() {
    return this.price;
  }
}
class Computer extends Product {
  private String cpu;
  private String memory;
  private String storage;

  public Computer(String name, int price, String cpu, String memory, String storage) {
    super(name, price);
    this.cpu = cpu;
    this.memory = memory;
    this.storage = storage;
  }
}

OOPにおけるメモリの動作

プログラム動作時にはインスタンスが生成される前にクラス情報がメモリにロードされる。

ここでいうクラス情報とは個々のインスタンスに依存しないクラス固有の情報のこと。

クラス情報のロードは事前に全てのクラス情報をロードする一括ロードの方法と、新しいクラスのコードを実行するたびに、クラス情報をロードする逐次ロードする方法の2種類がある。

インスタンスが生成されるたびにヒープ領域が使用され、そのタイミングでインスタンスからメソッドエリアにあるクラス情報との関連付けが行われる。

メモリ内部

メモリ内部 多態性を利用した場合、メモリ内部の構造が若干変わる。

インスタンスは各クラスに定義されたメソッドがメモリに展開されている場所を示したポインタ情報を格納している。これをメソッドテーブルと呼び、スーパークラスと同様の形式のメソッドテーブルを持つ。詳しくは書籍参照。

ガーベッジコレクション

ヒープ領域に残ったインスタンスを自動的に削除する仕組み。ヒープ領域の空きが少なくなったタイミングで処理を起動する。

OOPを使って作られたアプリケーションはインスタンスが他のインスタンスを参照したり、インスタンス同士が参照しあったりして全体のネットワークを構成するが、インスタンスの中にはメソッドエリア(静的領域)やスタック領域から辿ることのできないインスタンスが存在する場合があり、これがガーベッジコレクトの対象となる。

OpenAPI再入門

OpenAPIとは?

RESTFul APIの仕様のこと。略語はOAS。今回確認したのは最新のVersion 3.0.3。yamlもしくはjsonファイルで記述し、APIの仕様を定義することができる。

スキーマと呼ばれるオブジェクト(データモデル)が存在し、各オブジェクトでどのようなフィールドを持つことができるのかはドキュメントのFixed Fieldsの欄で確認することができる。

オブジェクトの一部のフィールドには接頭辞のx-をつけることでベンダー独自の仕様拡張が可能となっている。拡張可能なフィールドに関してもopenapiの仕様書に定義されている。

構造

ルートの定義

APIのルートパスに関してはopenapiのスキーマであるServerオブジェクトで定義する。urldescriptionがserverオブジェクトのフィールドにあたる。

以下のように複数の環境を定義できる。

servers:
  - url: http://api.example.com/v1
    description: "本番環境"
  - url: http://staging-api.example.com
    description: "ステージング環境" 

このurlフィールドの定義はサーバーのベースパスとなる。

urlフィールドの任意の部分には変数を渡すこともできる。変数を渡したいときはvariablesフィールドに必須のdefaultフィールドを設定し、オプションのenumをセットする。

servers:
  - url: '{protocol}://api.example.com'
    variables:
      protocol:
        enum:
          - http
          - https
        default: https

全てのスキーマに対して言えることだが、Fixed Fieldstype(データ型)に他のオブジェクトが設定されていた場合、そのフィールドは他のオブジェクトを持つことができる。(オブジェクトは定義に従った入れ子構造にできる)

例えばServerオブジェクトのフィールドにはvariablesフィールドが定義されているが、これはServer Variableオブジェクトという別のオブジェクトであり、Server Variableオブジェクトがどのようなフィールドを持つことができるかはServer VariableオブジェクトのFixed Fieldsに従う。

その他のOASのプリミティブなデータ型についてはJSON Schema Specification Wright Draft 00 でサポートされている型に基づいている。

Paths

エンドポイントに対して、複数のHTTPメソッド(操作)を定義できる。

例:

paths:
    /users/{id}:
        get:
          ...
        patch:
          ...
        delete:
          ...

エンドポイント

フィールド名は/で始まる必要がある。このエンドポイントはベースURLに対して相対的なもの。

例えばエンドポイントを/usersのように設定し、serverのベースURLをhttps://api.example.com/v1のように設定している場合、エンドポイントはhttps://api.example.com/v1/usersとして参照される。{id}のように中括弧を利用してURLの一部をパスパラメータとして利用することもできる(パステンプレート)。その場合、APIクライアントはhttps://api.example.com/v1/users/5などと適切なパラメータを提供する必要がある。

HTTPメソッド(操作)

GET、POST、DELETE などのHTTPメソッド。

また、OpenAPIの「操作(operations)」の定義にはパラメータ、リクエストボディ、レスポンスのステータスコード(200 OK または 404 Not Found など)、レスポンスの内容が含まれる。

Pathの定義と入れ子構造

Pathの項目でパラメータ、レスポンス、ステータスコードを定義した場合、以下のような形になる

paths:
  /users/{id}:
    get:
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
      required:
        - id
        - name

それぞれのフィールドを分解してコメントを入れるとこんな感じ。

paths: # pathsオブジェクト
  /users/{id}: # pathオブジェクトのフィールド(Path Itemオブジェクト)
    get: # Path Itemオブジェクトのフィールド(Operationオブジェクト)
      parameters: # Operationオブジェクトのフィールド(Parameterオブジェクト)
        - name: id # Parameterオブジェクトのフィールド(string)
          in: path # Parameterオブジェクトのフィールド(string)
          required: true # Parameterオブジェクトのフィールド(boolean)
          schema: # Parameterオブジェクトのフィールド(Schemaオブジェクト)
            type: integer # Schemaオブジェクトのプロパティ
            format: int64 # Schemaオブジェクトのプロパティ
      responses: # Operationオブジェクトのフィールド(Responsesオブジェクト)
        '200': # Responsesオブジェクトのフィールド(Responseオブジェクト)
          content: # Responseオブジェクトのフィールド(Map[string, Media Typeオブジェクト])
            application/json: # Map[string, Media Typeオブジェクト]型のキー(string)
              schema: # Media Typeオブジェクト型のフィールド(Schemaオブジェクト)
                $ref: '#/components/schemas/User' # Referenceオブジェクト
...
components: # Componentsオブジェクト
  schemas: # Componentsオブジェクトのフィールド(Map[string, Schemaオブジェクト])
    User: # Map[string, Schemaオブジェクト]型のキー(string)
      type: object # Schemaオブジェクトのプロパティ
      properties: # Schemaオブジェクトのプロパティ
        id: # Schemaオブジェクト
          type: integer # Schemaプロパティ
          format: int64 # Schemaプロパティ
        name: # Schemaオブジェクト
          type: string # Schemaプロパティ
      required: # Schemaオブジェクトのプロパティ
        - id
        - name

※Schemaオブジェクトに関してはフィールドという表現ではなく、プロパティという表現になっている。こちらのプロパティについてもJson Schemaに基づき定義されている。