20191002のAWSに関する記事は12件です。

Vue.jsで高校の文化祭投票システムを作った話

Vue.jsで高校の文化祭投票システムを作った話

この記事はVue#2アドベントカレンダーの25日目です?

はじめに

目次

H2タグだけまとめた見出しです

番号 目次
1 はじめに
2 利用したAWSのサービス
3 フロント部分について
4 バックエンド部分について
5 画面遷移時の処理
6 不正投票対策
7 投票処理
8 投票完了処理
9 PC用投票ページ
10 つまずいた点など
11 小ネタ
12 反省点
13 ソースコード(GitHub)
14 参考記事(覚えている範囲で)
15 終わりに

注意事項

できるだけ正確な記事を書こうと心がけておりますが、至らない点があるかもしれません。ご了承ください。
また、そのような間違いを発見された場合は、TwitterのDMまたはコメント欄でご指摘頂けますと大変助かります。

記事の概要

Vue.jsを使って高校の文化祭大賞投票システムを作ったので、後輩への引き継ぎ資料がてら記事を書いてみました。

デモ用ページ

以下のページがデモ用のページとなっており、実際の挙動を確認できます。
https://vote-test.shinbunbun.info/
※スマホからしかアクセスできません。PCからアクセスする場合はChromeのデベロッパーツール等を使ってください。

ソースコードはこちらです。

去年のシステム

去年はLINEBotを使いました。(詳しくはこちら

上の記事にも書いている通り、失敗ではないもののなかなか上手く行かない点がいくつかありました。

今年のシステム

去年の反省を踏まえ、今年は以下のようにWebで投票ができるシステムにしました。
一般出展団体に3票、飲食出展団体に1票投票できます。

制作スケジュール

元々Webで作るという構想が浮かんだのは6月あたりです。ちなみに当時はVue.jsどころかフロント自体ほとんど書いたことがない状態でしたが、文化祭本番は10月末だったので、7月中にパンフレットを完成させて8月からVue.jsを勉強しつつ制作に着手すれば全然間に合いそうだなーとか舐めたことを考えていました。

ですが、パンフレット制作が9月に食い込んでしまい、Vue.jsの勉強すら未着手のまま本番2週間前に突入するという最悪の事態に...。

とりあえずVue.jsを学ばなくてはと思い、行き帰りの電車や休み時間などをフル活用して1週間で猫本を読了し、本番1週間前から制作をはじめました。2日くらい学校へ行くのを忘れていたような気もしますが、総作業時間50時間でどうにか完成させました。

アーキテクチャ

フロント部分はVue.jsで書いてS3に静的ホスティング、バックエンドはNode.jsで書いてLambdaで動かし、DBはDynamoDBを使用しました。
S3の静的ホスティングだけではTLS通信ができないため、CloudFrontとACMをを使用しました。(詳細はCloudFrontACM

Untitled (3).png

利用したAWSのサービス

API Gateway

https://aws.amazon.com/jp/api-gateway/

簡単にAPIの作成ができるサービスです。Lambda関数をAPI経由で叩く際必要になります。

Lambda

https://aws.amazon.com/jp/lambda/

イベント発生時に任意のコードを実行することができるFaaSです。今回はAPI Gatewayと連携させて、APIが叩かれたタイミングで関数を実行するといった風に使用しました。

CloudFront

https://aws.amazon.com/jp/cloudfront/

高速コンテンツ配信ネットワーク (CDN) サービスです。キャッシュサーバーみたいなものですね。世界中にエッジサーバーがあるので、世界中どこからでも高速にアクセスできます。また、一定時間データをキャッシュしてくれるので、オリジンサーバへ負担を軽減させることができます。大規模なサービスではDDoS攻撃対策にも利用されているらしいです。また、サーバ証明書を設定することもできます。今回はこちらの目的で利用しました。

ACM(AWS Certificate Manager)

https://aws.amazon.com/jp/certificate-manager/

無料でサーバ証明書が取得できるサービスです。

Route53

AWSが提供する権威DNSサーバです。EC2やS3、CloudFrontと簡単に連携できるようになっています。

DynamoDB

NoSQLのデータベースです。超早い。

フロント部分について

前述の通り、フロント部分はVue.jsを使用しました。

VueCLI

VueCLIを使ってプロジェクトを作成しました。

Vue Router

Vue Routerを使ってSPAを構築しました。親ルートは以下のような構成になっています

  • 「/」
  • 「/kougakusai」(一般出展団体投票ページ用親ルート)
  • 「/food-gp」(飲食出展団体投票ページ用親ルート)
  • 「/policy」(プライバシーポリシー)
  • 「/thanks」(投票完了ページ)
  • 「/invalidate」(不正投票検知時に表示するページ)

/kougakusai

一般出展団体投票ページ用の親ルートです。子ルートは以下のような構成になっています。

  • 「/kougakusai」(投票方法選択画面)
  • 「/kougakusai/exhibitList」(出展一覧から選ぶ)
  • 「/kougakusai/vote-number」(投票番号から選ぶ)
  • 「/kougakusai/keyword」(キーワードから選ぶ)

/food-gp

飲食出展団体投票ページ用の親ルートです。子ルートは以下のような構成になっています。

  • 「/food-gp」(投票方法選択画面)
  • 「/food-gp/exhibitList」(出展一覧から選ぶ)
  • 「/food-gp/vote-number」(投票番号から選ぶ)
  • 「/food-gp/keyword」(キーワードから選ぶ)

基本的に一般出展団体投票ページと同じような構成になっています。投票可能な団体数が違うなど微妙に異なるところはあるものの画面部分はほぼ同じため、それぞれ共通部分は単一ファイルコンポーネント化しています。

/policy

プライバシーポリシーのページです。ここは1ページしかないため構成解説は割愛します。

/thanks

投票完了後のサンクスページです。一般出展団体の投票完了後と飲食出展団体の投票完了後に表示されます。ここは画面部分も行う処理もまったく同じため、同一ページを使用しています。

/invalidate

不正投票を検知した際に表示するページです。router.beforeEachで不正投票検知の処理を行い、そこで引っかかった場合はこのページに飛びます。(不正投票検知についての詳細は不正投票対策

Vuex

Vuexについてはあまり勉強している時間がなかったため、あまり活用することはできませんでした。本来の使い方として正しいかは分かりませんが、いくつかのファイルで共通して使用する処理はmutations、セッション内で保持しておきたい値はstateに登録するという使い方をしました。

state

  • division
    • 現在見ているページが「/kougausai」以下か「/food-gp」以下かを判別するものです。
    • /food-gpでも述べた通り共通処理はコンポーネント化しています。しかし微妙に違う処理を行うところがあるため、コンポーネント側でその処理を分けるために使用しています。
  • loading
    • 現在読み込み中かどうかを判別するために使用します。
    • trueならローディング画面を表示、falseなら非表示という使い方をしています。
    • 詳細はローディング画面

mutations

  • loadingStart
    • stateのloadingをtrueに変更します。
  • loadingEnd
    • stateのloadingをfalseに変更します。
  • addDivision
    • stateのdivisionを変更します。
  • kougakusaiVote
    • 一般出展団体の投票処理を行います。
    • 主に投票先団体をaxiosでLambdaに飛ばす処理を行なっています。
    • 詳細は一般出展団体
  • foodGpVote
    • 飲食出展団体の投票処理を行います。
    • 主に投票先団体をaxiosでLambdaに飛ばす処理を行なっています。
    • 詳細は飲食出展団体
  • voteComplete
    • 投票完了時の処理です。
    • 一般出展団体の投票時のみ使用します。
    • 以下のように、1団体または2団体に投票した時点で投票完了ボタンを押すとこの処理が走ります。
    • 詳細は投票完了処理

  • invalidate
    • 不正投票が検知された時に走ります。
    • Cookieに不正投票フラグを登録します。

getters

  • returnJsonData
    • divisionがkougakusaiの場合は一般出展団体一覧のjsonを、food-gpの場合は飲食出展団体のjsonを返します。
    • 各出展団体のデータは以下のような形のjsonファイルになっています。

スクリーンショット 2019-10-02 19.50.13.png

単一ファイルコンポーネント

いくつかのページで共通する処理は単一ファイルコンポーネントにしました。

  • choose-voting-way
    • 投票方法の選択画面です。
    • /kougakusai、/food-gpで使用しているコンポーネントです。
    • division(Vuexのstate)を確認して、/kougakusaiの場合と/food-gpの場合の処理を分けています。
  • exhibit-list
    • 出展一覧から投票するページです。
    • /kougakusai/exhibitList、/food-gp/exhibitListで使用しています。
    • choose-voting-wayと同じように、division(Vuexのstate)を確認して、/kougakusaiの場合と/food-gpの場合の処理を分けています。
  • help
    • ヘルプ画面のモーダルウィンドウです。
    • modalウィンドウのコンポーネントをもとに作成しています。
  • keyword
    • キーワードから投票するページです。
    • v-modelでinputに入力されたキーワードを取得しています。
  • LoadingOverlay
  • modal
    • モーダルウィンドウ用のコンポーネントです。
    • モーダルウィンドウの骨組みやスタイルを定義しています。
  • myfooter
    • フッターです。
    • 全ページ共通のため、App.vueで読み込んでいます。
  • myheader
    • ヘッダーです。
    • 全ページ共通のため、App.vueで読み込んでいます。
  • vote-number
    • 投票番号から投票するページです。
    • /kougakusai/vote-number、/food-gp/vote-numberで使用しています。
    • choose-voting-wayと同じように、division(Vuexのstate)を確認して、/kougakusaiの場合と/food-gpの場合の処理を分けています。

バックエンド部分について

バックエンド部分の概要

APIGateway、Lambda、DynamoDBを使用しました。
言語はNode.jsで書いています。

API

以下のように、機能によってAPIエンドポイントを分けました。

  • /kougakusai
    • 一般出展団体投票処理用のエンドポイントです。
    • 詳細は一般出展団体
  • /food-gp
    • 飲食出展団体投票処理用のエンドポイントです。
    • 詳細は飲食出展団体
  • /transition
    • フロントエンド側で不正投票の疑いがある挙動を検知した場合又は初回アクセス時に叩かれます。
    • 本当に不正投票なのかの最終チェックを行います。
    • 最初は遷移時に毎回叩こうと思っていたので「/transition」という名前で作成しました。変更するのが面倒くさかったのでそのままの名前にしています。
  • /vote-complete

画面遷移時の処理

// ルーターナビゲーションの前にフック
router.beforeEach(async (to, from, next) => {

  store.commit('loadingStart');
  console.log('router.js, beforeEach');
  console.log(`path:${to.path}`);

  if (to.path !== '/invalidate') {

    const access = Cookies.get('access');
    const invalidate = Cookies.get('invalidate');
    if (invalidate === 'true') {
      // 不正投票検知ページへ遷移
      console.log('invalidate: true');
      next({
        name: 'invalidate'
      });
    } else {
      //CookieとIPアドレスを使用した不正投票判定処理
      console.log('invalidate: false');
      if (!access) {
        console.log(`accessなし: ${access}`);
        //・
        //・省略
        //・
        } else {
        console.log(`accessあり: ${access}`);
      }
    }
  }
  console.log('せんい');
  next();
});
  • ローディングアニメーションを開始するために、loading(storeのstate)の値をtrueに変更する
  • パスが/invalidateだった場合
    • 不正投票検知のページへ遷移
  • そうでない場合

不正投票対策

Cookie&IPアドレスでチェックしています。まずフロント側でCookieのチェックを行い、そこで不正投票の兆候が見られた場合はバックエンド側でIPアドレスのチェックをするようにしています。
そのほかにも、PC・タブレットからのアクセスをブロックしたり(複数端末での投票防止)、Route53を使って海外からのアクセスをブロッキングしたり(VPNを使用した不正投票対策)するなどの対策を講じました。
ただ、PCのブロックに関してはChromeのデベロッパーツールを使えば回避できてしまうので、何か良い方法はないかなーと考えています。

不正投票が検出された場合は以下の画面を表示し、投票ページへのアクセスをブロックします。

スクリーンショット 2019-12-23 17.33.16.png

以下はCookieとIPアドレスを使った不正投票対策の解説です。

不正投票判定処理

不正投票判定処理のフロント側

      console.log('invalidate: false');
      if (!access) {
        console.log(`accessなし: ${access}`);
        // let err;
        // let responseData;
        try {
          await axios.post('api_endpoint', {
            'path': to.path,
            'sessionId': Cookies.get('sessionId')
          }
            /*, {
                        withCredentials: true
                      } */
          ).then((data) => {
            if (data.data.status === 'pathFoully') {
              console.log('pathFoully');
              Cookies.set('invalidate', 'true');
              next({
                path: '/invalidate'
              });
            } else if (data.data.status === 'ipFoully') {
              console.log('ipFoully');
              Cookies.set('invalidate', 'true');
              Cookies.set('sessionId', data.data.sessionId);
              next({
                path: '/invalidate'
              });
            } else if (data.data.status === 'sessionFoully') {
              console.log('sessionFoully');
              Cookies.set('invalidate', 'true');
              Cookies.set('access', 'true');
              next({
                path: '/invalidate'
              });
            } else if (data.data.status === 'success') {
              console.log('success');
              Cookies.set('access', 'true');
              Cookies.set('sessionId', data.data.sessionId);
            }
          });
        } catch (e) {
          alert(`エラーが発生しました。最初からやり直して下さい。app.vue,router.beforeEach:${e}`);
          console.error(e);
          next({
            name: 'choosing_page'
          });
          // err = true;
        }
      } else {
        console.log(`accessあり: ${access}`);
      }
  • Cookieに、初回アクセス時に登録されるCookieである「access」が保存されているか確認する
  • 保存されていた場合
    • 何もせず終了
  • 保存されていなかった場合
    • 不正投票判定用のAPI(バックエンド)を叩く
    • 不正投票が検知された場合
    • 検知された不正投票の種類(data.data.status)によってCookieを更新する
    • 全種類共通して、'invalidate'には'true'を登録する(不正投票フラグ)
    • 検知されなかった場合(data.data.statusが'success'だった場合)
    • 初回アクセスなので、Cookieの'access'に'true'、'sessionId'にはAPIからのレスポンスに含まれているセッションIDを登録する

不正投票判定処理のバックエンド側

const transitionFunc = async (event) => {

    //レスポンスのインスタンスを生成
    let response = new Response();
    //ヘッダーからIPアドレスを取得
    const ip = event.requestContext.identity.sourceIp;
    //リクエストボディからセッションIDを取得
    let sessionId = JSON.parse(event.body).sessionId;

    //セッションIDが存在する場合
    if (sessionId) {

        //不正投票の可能性があるため、エラーレスポンスを返す
        response.statusCode = 200;
        response.body = JSON.stringify({
            status: "sessionFoully"
        });
        response.headers = {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Credentials': true
        };
        return response;
        //セッションIDが存在しない場合
    } else {
        //IPアドレスをキーにDBを検索する
        let param = {
            TableName: 'kougakusai2019',
            IndexName: 'ip-index',
            KeyConditionExpression: "#k = :val",
            ExpressionAttributeValues: {
                ":val": ip
            },
            ExpressionAttributeNames: {
                "#k": "ip"
            }
        };
        let promise = await new Promise((resolve, reject) => {
            dynamoDocument.query(param, (err, data) => {
                if (err) {
                    reject(`$sendToGroup query err : {err}`);
                } else {
                    resolve(data);
                }
            });
        });

        //レコードが存在した場合
        //セッションIDを再生成してDBに再登録後、エラーレスポンスを返す
        if (promise.Items[0]) {
            console.log("ipアルヨ");
            //セッションID生成
            sessionId = uuidv4();
            //DBにIPアドレスとセッションIDを登録
            await sessionIdRegister(sessionId, ip);

            //レスポンスをreturn
            response.statusCode = 200;
            response.body = JSON.stringify({
                status: "ipFoully",
                sessionId: sessionId
            });
            response.headers = {
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Credentials': true
            };
            return response;

            //レコードが存在しなかった場合
        } else {
            console.log("ipナイヨ");
            //ボディから送信元のパスを取得する
            const path = JSON.parse(event.body).path;
            //パスがルートだった場合
            if (path === '/') {
                //普通に初回アクセスなので、初回アクセス時の処理を行い、レスポンスを返す
                response = await firstAccessFunc(event);
                return response;

                //パスがルートでない場合
                //不正投票の可能性があるため、セッションIDを再登録後エラーレスポンスを返す
            } else {
                sessionId = uuidv4();
                await sessionIdRegister(sessionId, ip);

                response.statusCode = 200;
                response.body = JSON.stringify({
                    status: "pathFoully"
                });
                response.headers = {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Credentials': true
                };
                return response;
            }
        }
    }
};
  • sessionIdが存在するか確認する
    • 存在する場合
    • Cookieの一部を不正に削除または偽装している可能性が高いため、不正投票と判断してstatus :'sessionFoully'を返す
    • 存在しない場合
    • IPアドレスがDBに保存されているか確認
      • 保存されている場合
      • 新しいsessionIdを発行してIPアドレスと一緒にDBへ再登録後、status: 'ipFoully'を返す
      • 保存されていない場合
      • 閲覧しているページのパスを確認
        • トップページの場合
        • 初回アクセスと判定し、status: 'success'を返す
        • トップページでない場合
        • IPアドレスを偽装してアクセスしている可能性が高いため、不正投票と判断してstatus: 'pathFoully'を返す

投票処理

一般出展団体

一般出展団体投票処理のフロント部分

    async kougakusaiVote(state, payload) {
      state.loading = true;
      const id = payload[0];
      const name = payload[1];
      const clubName = payload[2];
      const router = payload[3];
      const sessionId = Cookies.get('sessionId');
      if (Cookies.get('name')) {
        const votedNames = JSON.parse(Cookies.get('name'));
        let i = 1;
        let flag = false;
        console.log(Object.keys(votedNames).length);
        Object.keys(votedNames).forEach(() => {
          console.log(votedNames[i]);
          if (votedNames[i] === name) {
            flag = true;
          }
          i++;
        });
        if (flag) {
          alert('同じ団体に2回以上投票する事は出来ません。もう一度投票する団体を選択して下さい');
          router.push({
            name: 'kougakusai-root'
          });
          return;
        }
      }
      let err = false;
      let data;
      try {
        data = await axios.post('htttps://XXX/kougakusai', {
          'id': id,
          'sessionId': sessionId
        }
          /*, {
                    withCredentials: true
                  } */
        );
      } catch (e) {
        alert(`エラーが発生しました。お手数ですがお近くの執行部員までお問い合わせください。store,mutations,kougakusaiVote:${e}`);
        console.error(e);
        err = true;
        state.loading = false;
      }
      if (data.data.status === 'idsFoully') {
        Cookies.set('invalidate', 'true');
        Cookies.set('voteSession', '3');
        router.push({
          name: 'invalidate'
        });
      }
      if (err) {
        let voteSession = parseInt(Cookies.get('voteSession'), 10);
        voteSession++;
        Cookies.set('voteSession', voteSession);
        this.$store.commit('addDivision', 'none');
        router.push({
          name: 'choosing_page'
        });
      } else {
        let voteSession = parseInt(Cookies.get('voteSession'), 10);
        voteSession++;
        Cookies.set('voteSession', voteSession);
        if (voteSession === 1) {
          Cookies.set('name', JSON.stringify({
            1: name
          }));
          Cookies.set('clubName', JSON.stringify({
            1: clubName
          }));
        } else {
          let names = JSON.parse(Cookies.get('name'));
          let clubNames = JSON.parse(Cookies.get('clubName'));
          names[voteSession] = name;
          clubNames[voteSession] = clubName;
          Cookies.set('name', JSON.stringify(names));
          Cookies.set('clubName', JSON.stringify(clubNames));
        }
        // this.$store.commit('addDivision', 'none');
        console.log(router);
        if (voteSession === 3) {
          router.push({
            name: 'thanks'
          });
        } else {
          router.push({
            name: 'kougakusai-root'
          });
        }
      }
      state.loading = false;
    }

storeのmutationsに登録されており、投票ボタンを押した際に呼び出されます。
最初に、引数から投票する団体の投票番号(id)、出展名、出展団体名を取り出します。
同じ出展団体に複数回投票することは出来ないので、投票済みの投票団体ではないことを確認し、一般出展団体投票用のAPIを叩きます。
API側での処理が正常に完了した場合は、Cookieを更新して次のページへ遷移します。

一般出展団体投票処理のバックエンド部分

const kougakusaiFunc = async(event) => {
    const response = new Response();
    const ip = event.requestContext.sourceIp;
    const sessionId = JSON.parse(event.body).sessionId;
    const id = JSON.parse(event.body).id;
    console.log(sessionId);

    let param = {
        TableName: 'kougakusai2019',
        KeyConditionExpression: "#k = :val",
        ExpressionAttributeValues: { ":val": sessionId },
        ExpressionAttributeNames: { "#k": "sessionId" }
    };
    let promise = await new Promise((resolve, reject) => {
        dynamoDocument.query(param, (err, data) => {
            if (err) {
                reject(`err : ${err}`);
            }
            else {
                resolve(data);
            }
        });
    });
    //console.log(promise.Items[0].ids.values.length);
    console.log(`promise: ${promise.Items[0]}`);
    //console.log(`promise.Items[0].ids: ${promise.Items[0].ids}`);
    if (promise.Items[0].ids) {
        const ids = promise.Items[0].ids.values;
        ids[ids.length++] = id;
        if (ids.length > 3) {
            response.statusCode = 200;
            response.body = JSON.stringify({ status: "idsFoully" });
            response.headers = {
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Credentials': true
            };
            return response;
        }
        else {
            param = {
                TableName: 'kougakusai2019',
                Key: { //更新したい項目をプライマリキー(及びソートキー)によって1つ指定
                    sessionId: sessionId
                },
                ExpressionAttributeNames: {
                    '#i': 'ids'
                },
                ExpressionAttributeValues: {
                    ':ids': dynamoDocument.createSet(ids)
                },
                UpdateExpression: 'SET #i = :ids'
            };
            await new Promise((resolve, reject) => {
                dynamoDocument.update(param, (err, data) => {
                    if (err) {
                        reject(err);
                    }
                    else {
                        resolve(data);
                    }
                });
            });
        }
    }
    else {
        param = {
            TableName: 'kougakusai2019',
            Key: { //更新したい項目をプライマリキー(及びソートキー)によって1つ指定
                sessionId: sessionId
            },
            ExpressionAttributeNames: {
                '#i': 'ids'
            },
            ExpressionAttributeValues: {
                ':ids': dynamoDocument.createSet([id])
            },
            UpdateExpression: 'SET #i = :ids'
        };
        await new Promise((resolve, reject) => {
            dynamoDocument.update(param, (err, data) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(data);
                }
            });
        });
    }

    response.statusCode = 200;
    response.body = JSON.stringify({
        status: "success",
        sessionId: sessionId
    });
    response.headers = {
        'Access-Control-Allow-Origin': '*'
    };
    return response;
};

一般出展団体投票用APIでは、フロント側から受け取った投票先団体の投票番号をDBへ登録する処理を行なっています。
まず、リクエストからipアドレス、セッションID、投票先団体の投票番号(id)を取得します。
その後、セッションIDをキーにDBを叩き、そのユーザーが投票済みの団体を取得します。もしすでに3団体投票済みの場合は不正投票の可能性があるので、エラーレスポンスを返します。投票済み団体数が2団体以下の場合は、DBへ投票先団体の投票番号を追加して正常終了のレスポンスを返します。

飲食出展団体

飲食出展団体投票処理のフロント部分

    async foodGpVote(state, payload) {
      state.loading = true;
      const id = payload[0];
      const name = payload[1];
      const clubName = payload[2];
      const router = payload[3];
      const sessionId = Cookies.get('sessionId');
      let err = false;
      let data;
      try {
        data = await axios.post('https://XXXXX/food-gp', ({
          // 'division': 'food-gp',
          'id': id,
          'sessionId': sessionId
        }));
      } catch (e) {
        alert(`エラーが発生しました。お手数ですがお近くの執行部員までお問い合わせください。store,mutations,food-gpVote:${e}`);
        console.error(e);
        err = true;
      }
      if (data.data.status === 'idsFoully') {
        Cookies.set('invalidate', 'true');
        Cookies.set('food-gp', 1);
        router.push({
          name: 'invalidate'
        });
      }
      if (err) {
        this.$store.commit('addDivision', 'none');
        router.push({
          name: 'choosing_page'
        });
      } else {
        Cookies.set('name', JSON.stringify({
          1: name
        }));
        Cookies.set('clubName', JSON.stringify({
          1: clubName
        }));
        Cookies.set('food-gp', 1);
        // this.$store.commit('addDivision', 'none');
        console.log(router);
        router.push({
          name: 'thanks'
        });
      }
      state.loading = false;
    },

一般出展団体と同様、引数から投票先団体の情報を取得します。飲食出展団体は1団体しか投票できないため投票済み団体の確認をする必要はないので、とくに何もせず飲食出展団体投票用のAPIを叩きます。API側の処理が成功したらサンクスページへ遷移します。

飲食出展団体投票処理のバックエンド部分

//飲食出展団体投票処理用の関数
const foodGpFunc = async (event) => {

    //レスポンスのインスタンスを生成
    const response = new Response();
    //ボディからセッションIDを取得
    const sessionId = JSON.parse(event.body).sessionId;
    //ボディから投票先団体の投票番号を取得
    const id = JSON.parse(event.body).id;
    console.log(sessionId);

    //セッションIDをキーにしてDBを検索する
    let param = {
        TableName: 'kougakusai2019',
        KeyConditionExpression: "#k = :val",
        ExpressionAttributeValues: {
            ":val": sessionId
        },
        ExpressionAttributeNames: {
            "#k": "sessionId"
        }
    };
    let promise = await new Promise((resolve, reject) => {
        dynamoDocument.query(param, (err, data) => {
            if (err) {
                reject(`err : ${err}`);
            } else {
                resolve(data);
            }
        });
    });

    //DBから取得したデータに投票番号が存在する場合(既に投票済みの場合)
    //不正投票の可能性があるので、エラーレスポンスを返す
    if (promise.Items[0].foodId) {
        response.statusCode = 200;
        response.body = JSON.stringify({
            status: "idsFoully"
        });
        response.headers = {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Credentials': true
        };
        return response;

        //まだ投票していない場合
    } else {

        //DBに投票先団体の投票番号を登録
        param = {
            TableName: 'kougakusai2019',
            Key: {
                sessionId: sessionId
            },
            ExpressionAttributeNames: {
                '#i': 'foodId'
            },
            ExpressionAttributeValues: {
                ':foodId': id
            },
            UpdateExpression: 'SET #i = :foodId'
        };
        await new Promise((resolve, reject) => {
            dynamoDocument.update(param, (err, data) => {
                if (err) {
                    reject(err);
                } else {
                    resolve(data);
                }
            });
        });
    }

    //レスポンスをreturn
    response.statusCode = 200;
    response.body = JSON.stringify({
        status: "success",
        sessionId: sessionId
    });
    response.headers = {
        'Access-Control-Allow-Origin': '*'
    };
    return response;
};

大まかな構造は一般出展団体投票用APIと変わりないですが、飲食出展団体は1団体しか投票ができないので、そこの部分だけ少し違う処理になっています。

投票完了処理

1団体または2団体まで投票した時点で投票を完了することができる機能です

フロント側

async voteComplete(state, router) {
      state.loading = true;
      console.log(router);
      Cookies.set('voteSession', 3);
      let err = false;
      try {
        await axios.post('https://XXXXX/vote-complete', ({
          'status': 'voteComplete',
          'sessionId': Cookies.get('sessionId')
        }));
      } catch (e) {
        alert(`エラーが発生しました。お手数ですがお近くの執行部員までお問い合わせください。store,mutations,kougakusaiVote:${e}`);
        console.error(e);
        err = true;
      }
      if (err) {
        this.$store.commit('addDivision', 'none');
        router.push({
          name: 'choosing_page'
        });
      } else {
        router.push({
          name: 'thanks'
        });
      }
      state.loading = false;
    }

投票完了処理には0.5~1秒ほどかかることがあるので、最初にstate.loadingをtrueにしてローディングアニメーションを開始します。
その後、Cookieを更新してから投票完了処理を行うAPIを叩きます。
処理が正常に完了した場合は、ローディングアニメーションを終了してサンクスページへ遷移します。

バックエンド

//投票完了処理用の関数
const voteCompletefunc = async (event) => {
    //レスポンスのインスタンスを生成
    const response = new Response();
    //ボディからセッションIDを取得
    const sessionId = JSON.parse(event.body).sessionId;
    console.log(`sessionId: ${sessionId}`);

    //セッションIDをキーにDBを検索
    let param = {
        TableName: 'kougakusai2019',
        KeyConditionExpression: "#k = :val",
        ExpressionAttributeValues: {
            ":val": sessionId
        },
        ExpressionAttributeNames: {
            "#k": "sessionId"
        }
    };
    let promise = await new Promise((resolve, reject) => {
        dynamoDocument.query(param, (err, data) => {
            if (err) {
                reject(`err : ${err}`);
            } else {
                resolve(data);
            }
        });
    });

    //DBより取得したデータから投票済み団体の投票番号を取得
    const ids = promise.Items[0].ids.values;
    console.log(`ids: ${ids}`);
    //idsの長さ(投票済み団体の数)を取得
    const idsLength = ids.length;
    console.log(`ids: ${ids.length}`);

    //1団体に投票済みの場合
    if (idsLength === 1) {
        //ダミーデータを追加
        ids[2] = 200;
        ids[3] = 0;

        //2団体に投票済みの場合
    } else {
        //ダミーデータを追加
        ids[3] = 0;
    }
    console.log(`ids2: ${ids}`);

    //ダミーデータを追加した投票番号一覧をDBへ再登録
    param = {
        TableName: 'kougakusai2019',
        Key: {
            sessionId: sessionId
        },
        ExpressionAttributeNames: {
            '#i': 'ids'
        },
        ExpressionAttributeValues: {
            ':ids': dynamoDocument.createSet(ids)
        },
        UpdateExpression: 'SET #i = :ids'
    };
    await new Promise((resolve, reject) => {
        dynamoDocument.update(param, (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });

    //レスポンスをreturn
    response.statusCode = 200;
    response.body = JSON.stringify({
        status: "success",
        sessionId: sessionId
    });
    response.headers = {
        'Access-Control-Allow-Origin': '*'
    };
    return response;
};

ボディからセッションIDを取得してそれをキーにDBを検索し、DBより取得したデータから投票番号を取り出して投票済み団体数をチェックします。投票済み団体数に応じて投票番号の配列にダミーデータを追加して、投票番号の配列をDBに再登録しています。

PC用投票ページ

スマホを持っていない人向けで校門前の受付に投票用PCを設置しました。インターネットに接続することができない環境だったので、投票データは配列としてlocalStorageに保存しておき、後から読み出して集計を行いました。

つまずいた点など

大量のバグ

大量のバグが発生しました。思い出したくない。

ドメイン利用制限事件

文化祭本番前日である2019年9月27日23時04分。最終チェックをするために投票ページへアクセスしようとした僕は、突如投票ページにアクセス出来なくなっていることに気がついたのだった...。

ということで、本番前日の夜に突如として投票ページにアクセス出来なくなるという、冷や汗じゃすまないレベルの事件が起きました。

結果としては、ドメインのメール認証をし忘れていたせいで利用制限がかかってしまったというインシデントでした。夜のうちに原因解明と利用制限解除ができたのでことなきを得ましたが、もしこれが本番真っ只中で起こっていたらと思うと冷や汗がでます。僕の当時の焦りを少しでも伝えられればと思い、当時の状況を時系列順に並べてみました。終始半泣きです。

時間 出来事
2019/9/27 21:17 ドメイン利用制限がかかる
21:22 【重要】[お名前.com] ドメイン利用制限設定 完了通知というメールが届く
23:04 投票ページにアクセス出来なくなったことが判明
23:16 DNS系のエラーであることが判明
23:30 whoisなどを調べた結果、ドメインに利用制限がかかっていることに気付く
23:35 メールが届いていたことに気付く
23:39 認証を通す
23:51 利用制限解除を確認
23:53 投票ページにアクセスできるようになっていることを確認

悲痛の叫び

マジで焦った

小ネタ

ローディング画面

こんなやつです。

ダウンロード (2).gif

文字の部分は猫本のサポートページに載っていたものを使わせて頂きました。

クルクル回るやつもどこかの記事から拾ってきたものなのですが、どの記事だったかは忘れました...。

router.beforeEachが実行されるとすぐにstore.commit('loadingStart')でローディング状態に入ります。フロント側での不正投票検出処理などが完了し、router.afterEachに入るとstore.commit('loadingEnd')でローディング状態を終了します。投票時にも同じローディングアニメーションを使用しています。

モーダルウィンドウ

出展団体を表示する際にモーダルウィンドウを使用しました。

こちらも猫本のサポートページに載っていたものを参考にしました。

表示するhtmlはこちらのサイトなどを参考にしながらBootstrapのカードを利用して作成しました。

集計

DynamoDBtoCSVを利用してdynamoDBのデータをすべてcsvで取得し、vscodeで正規表現を使った置換で余計なものを消してからExcelのピボットテーブルを使って集計を行いました。来年は集計用のLambda関数を作成して一発で集計ができるようにしたい所存。

ボタン等の画像

イラレで作成し、svgで書き出して埋め込みました。

スクリーンショット 2019-12-23 17.57.49.png

反省点

不正投票を厳格化しすぎた

Cookieの改竄やIPアドレスの改竄が検出された場合は一発で投票をブロックする仕様になっていたのですが、それでは初回アクセス時と違うブラウザを使用した時点でブロックされてしまいます。また、スマホの場合は基地局が変わったり電源をオンオフしたりWi-Fiに接続したりするだけでIPアドレスが変わってしまうのですが、それも不正投票扱いとなってしまいます。

アクセス解析をしたところ、不正投票ページへのアクセスが一定数あったので、意図せず不正投票扱いになってしまった人が結構な量いたのではないかと思います。

これでは流石に利便性が最悪なので、同一IPでUAが違う場合は不正投票扱いにせずCookieを再登録することで対応する、Cookieは登録されているが前回アクセス時とIPが違う場合も不正投票扱いにせずDBのIPアドレスを更新するなどしたいなと思っています。

コード汚い&変数名が適当すぎる

制作スケジュールでも書いた通りめちゃめちゃ急いで作ったので、コードが汚い上に変数名が目を覆いたくなるレベルの適当さです。リーダブルコード読んで出直してきます。幸い来年まではまだ1年弱あるのでゆっくり直していければと思っています。ちなみに宿題は3日前くらいからまとめてやるタイプです。

宣伝不足

これは本番当日の話ですが、1日目に投票の宣伝をすっかり忘れていたせいで投票数が3桁しかないというとんでもないことが起ってしましました。2日目はラミネート加工したQRコードを中学生に持たせて宣伝しながら校内を回ってもらったところ、1日目の3倍に増えました。そもそも投票の存在を知らないお客さんが多かったので、宣伝をきちんとして周知させていくこともかなり大事だなと思いました。

ソースコード(GitHub)

https://github.com/shinbunbun/vote-system-2019

GitHubにソースコードをあげました。人様に見せられるコードではないので公開しようか迷ったのですが、マサカリを投げて欲しいので公開します。

今回発見できた不具合(改善点)は反省点で書きましたが、それ以外にも大量のバグがあると思います。また、Vue.jsを使ったのがはじめてだったのできちんとした使い方ができていないと思います。(とくにVuex)どんなに些細なことでも良いので、「ここはこうした方が良いよー」とか「ここ間違ってんじゃオラ」みたいなものがあれば、コメント欄、TwitterのDM、GitHubのPRやissueなどでガンガン教えて頂けますと助かります。

参考記事(覚えている範囲で)

以下の記事を参考させて頂きました。ありがとうございます。

終わりに

長いですねこれ。1000行を超えるQiita書いたのはじめてです。ここまで読んでいる人はいないんじゃないかなーと思いながらこの文章を書いています。あと、思いつきで書き進めたので多分文章めちゃくちゃです。ごめんなさい。長すぎて誰も読まない気はしましたが、後輩のために頑張って書きました。僕が卒業しても引き継いでね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS初心者向け】AWSでWebサーバーとデータベースを構築して、ブラウザにデータを表示する【ハンズオン】

はじめに

想定読者

AWS未経験者、初学者、非インフラエンジニア

簡単に自己紹介

システムエンジニア(2年目)(非インフラ)
AWS歴3ヶ月(独学)
最初の1ヶ月の勉強方法はこちら(インフラ知識ゼロの2年目プログラマーがAWS SAAに1ヶ月で合格した方法

この記事の目的

上記の想定読者ができるだけ少ない負担でAWSに触れ、実際に動かしてみること

私自身がAWSを勉強する前に感じていたインフラ・AWSに対する敷居の高さを少しでも払拭できないかという思いがあり、
もし自分が初心者向けにAWSのハンズオンをするならこういうことをしたいなという内容をそのまま記事にしました。

初心者が書いた記事なので、間違い等あれば指摘いただけると幸いです。
また、この記事で紹介するインフラ構成、構築方法はあくまで練習のための最低限のものなので、実際にサービスを構築・運用する際の参考にはならないことをご了承ください。

ゴール

AWS上でWebサーバー、データベースを構築し、データベースに登録したデータをブラウザで表示すること
スクリーンショット 2019-10-02 17.38.14.png
(userテーブルのid,nameカラムを表示)

できるだけ丁寧な解説を心掛けますが、わからない用語、手法が出てくるかもしれません。
ただ、進めていく中で理解できることもあると思うので、とりあえずやってみていただければと思います。

Macユーザー向けに書いていますが、Macの仕様に沿って動かしている箇所をWindowsに置き換えていただければ動作すると思います。(SSH接続でターミナルで動かすかわりにTeraTermを導入するなど)

下準備

この記事で扱うAWSの用語

AWSには用語がたくさんありますが、この記事で扱うものだけざっくり紹介します。最初はイメージしにくいかもしれないので、流し読みでも大丈夫です。

リージョン

AWSがサービスを提供している国の地域のこと
(東京リージョン、オレゴンリージョン、ロンドンリージョンなど)

AZ

・リージョン内のデータセンターのこと
・耐障害性を高めるため一つのリージョン内に複数のAZがある

VPC

Virtual Private Cloud
・AWS上の閉域(自分専用の)ネットワーク
・VPCを作成して、その中にサーバーなどを構築していく

サブネット

・VPC内でさらに細かく区切った任意のネットワークの範囲。
・Webサーバーはパブリックサブネットに配置し、データベースはプライベートサブネットに構築するなど、用途による切り分けができる。(パブリックサブネットとプライベートサブネットの違いは、インターネットと直接通信するかどうか)

EC2

Elastic Compute Cloud
・仮想サーバー
・ひとつひとつのサーバーはEC2インスタンスと呼ばれる。
・WindowsServer、LinuxなどOSの選択可能

RDS

Relational Database Service
・AWSが提供するRDBのサービス
Amazon Aurora、Oracle、SQL Server、PostgreSQL、MySQL、MariaDBから選択可能

IAM

Identity and Access Management
・簡単に言うとユーザーアカウント機能
・ルートユーザー(AWSに登録したアカウント)との差別化を図る
・各サービスのアクセス権限を設定できる

構成

簡単に構成を説明します。こちらも最初は流し読みでも大丈夫です。
ハンズオンを終えた後だとかなり理解しやすくなっていると思うので、もう一度見てみてください。
Untitled Diagram.jpg
AWS Cloud(AWSのネットワーク全体)、リージョン、アベイラビリティゾーンはAWSアカウントを作成していなくても存在しています。
AWSアカウントを作成すると、任意のリージョンの中に存在している状態になります。(初期設定はオレゴンリージョン)
まず初めに、自分が使用するネットワークの範囲を決めるために、VPCを作成します。
WebサイトなのでEC2(サーバー)は外部に公開します。データベースはセキュアな状態にしたいので外部に公開しません。
そのため、サブネットを外部に公開するものとしないものに分けて(パブリックサブネット、プライベートサブネット)その中にそれぞれWebサーバーとデータベースを構築します。
データベースに接続できるのはWebサーバーのみとします。
外部のネットワークとの通信には、インタネットゲートウェイと呼ばれる、サーバーがインターネット上と通信するための窓口が必要なので、VPC内に構築し、Webサーバーと紐づけを行います。これでWebサーバーがインターネットと通信することが可能になります。

以上が今回構築していくインフラ構成になります。

前提

AWSアカウントの登録は完了している状態とします。
AWS アカウント作成
また、ルートユーザー(AWSアカウントを登録したユーザー)と、実際にAWS上でサーバー構築などの作業を行うユーザーは分けることをAWS側が推奨しています。(全権限を持つルートユーザーで作業を行うのは非推奨。)
そのため、IAMユーザーを作成しておき、そのユーザーで作業を行うことをおすすめします。
(参考記事:AWSアカウント作ったらこれだけはやっとけ!IAMユーザーとAuthyを使ったMFAで2段階認証

ハンズオン開始

手順

1.VPCを作成する
2.インターネットゲートウェイを作成する
3.サブネットを作成する
4.ルートテーブルを作成する
5.セキュリティグループを作成する
6.EC2インスタンスを作成する
7.Elastic IPを作成、紐づけする
8.SSHでサーバーにログインしてみる
9.RDSを構築する
10.WebサーバーからRDSにSSHでログインしてみる
11.ブラウザにデータを表示する

リージョンの選択

AWS マネジメントコンソールにログインしてください。
サービスを構築する前にリージョンを設定します。
画面右上で選択できます。今回は東京リージョンを選択します。
スクリーンショット 2019-10-02 8.08.00.png

1.VPCを作成する

AWSマネジメントコンソールを開き、VPCと検索し、VPCダッシュボード(VPCの設定画面)を開きます。
スクリーンショット 2019-09-26 1.15.27.jpg

画面左側のメニューから「VPC」を選択し、「VPCの作成」をクリックします。
スクリーンショット 2019-09-26 1.17.39.jpg

名前をTestVpc,IPv4CIDRブロックを10.0.0.0/16とし、「作成」をクリックします。
ーIPv4CIDRブロック:ネットワーク内で使用するIPアドレスの範囲
スクリーンショット 2019-09-26 1.18.30.jpg

2.インターネットゲートウェイを作成する

VPCの画面の左側のメニューから「インターネットゲートウェイ」を選択し、「インターネットゲートウェイの作成」をクリックします。
スクリーンショット 2019-09-26 1.18.56.jpg

名前をTestIGWとし、「作成」をクリックします。

作成したインターネットゲートウェイをVPCに関連付けます。
もう一度インターネットゲートウェイの画面を開き、作成したインターネットゲートウェイを選択します。
画面上部の「アクション」をクリックし、「VPCにアタッチ」を選択します。
スクリーンショット 2019-09-26 1.19.26.jpg

さっき作成したVPCを選択し、「アタッチ」をクリックします。
スクリーンショット 2019-09-26 1.19.32.jpg

3.サブネットを作成する

Webサーバー用のパブリックサブネットと、データベース用のプライベートサブネットを作成します。
引き続きVPCの画面左側から「サブネット」を選択し、「サブネットの作成」をクリックします。
スクリーンショット 2019-09-26 1.21.43.jpg

まずはパブリックサブネットから。下記を入力し、「作成」をクリックします。
名前タグ PublicSubnet
VPC 作成したVPC
アベイラビリティゾーンap-northeast-1a
IPv4CIDRブロック 10.0.1.0/24
スクリーンショット 2019-09-26 1.22.32.jpg

同じようにプライベートサブネットも作成します。
データベースの作成にサブネットが2つ以上必要なため、2つ作成します。
名前タグ PrivateSubnet1
VPC 作成したVPC
アベイラビリティゾーンap-northeast-1c
IPv4CIDRブロック 10.0.2.0/24
スクリーンショット 2019-09-26 1.23.23.jpg

名前タグ PrivateSubnet2
VPC 作成したVPC
アベイラビリティゾーンap-northeast-1d
IPv4CIDRブロック 10.0.3.0/24
スクリーンショット 2019-09-26 1.23.39.jpg

4.ルートテーブルを作成する

サブネットが任意のネットワークと通信するための経路であるルートテーブルを作成します。
VPCダッシュボードから「ルートテーブル」を選択し、「ルートテーブルの作成」をクリックします。
スクリーンショット 2019-09-26 1.23.53.jpg

下記を入力し、「作成」をクリックします。
名前タグ TestRT
VPC 作成したVPC
スクリーンショット 2019-09-26 1.24.12.jpg

ルートテーブルとインターネットゲートウェイ、ルートテーブルとパブリックサブネットを紐付け、インターネットと通信できるようにします。
VPCダッシュボードから「ルートテーブル」を選択し、作成したルートテーブルを選択します。
画面下半分に表示されているタブから「ルート」を選択し、「ルートの編集」をクリックします。
スクリーンショット 2019-09-26 1.24.36.jpg

「ルートの追加」をクリックし、送信先を0.0.0.0/0とします。
ターゲットに「Internet GateWay」を選択します。
(0.0.0.0/0はIPアドレスの制限がないこと、すべての通信を意味する)
スクリーンショット 2019-09-26 1.25.08 (1).jpg

先程作成したインターネットゲートウェイを選択し、「ルートの保存」をクリックします。
スクリーンショット 2019-09-26 1.25.15.jpg

作成したルートテーブルとパブリックサブネットを関連づけます。
VPCダッシュボードから「ルートテーブル」を選択し、作成したルートテーブルをクリックします。
画面下半分のタブから「サブネットの関連付け」を選択し、「サブネットの関連付けの編集」をクリックします。
スクリーンショット 2019-09-26 1.25.41.jpg

作成したパブリックサブネットを選択し、「保存」をクリックします。
スクリーンショット 2019-09-26 1.25.59.jpg

5.セキュリティグループを作成する

こちらもWebサーバー用とデータベース用を作成します。
まずはWebサーバー用から。
VPCダッシュボードから「セキュリティグループ」を選択し、「セキュリティグループの作成」をクリックします。

下記を入力し、「保存」をクリックします。
セキュリティグループ名 MyWebServerSG
説明 ForWebServer
VPC 作成したVPC
スクリーンショット 2019-09-26 1.27.37.jpg

セキュリティグループの画面から「MyWebServerSG」を選択し、画面下部のタブの「インバウンドのルール」から「ルールの編集」をクリックします。
「ルールの追加」をクリックし、下記のルールを追加します。
タイプ SSH ソース カスタム 0.0.0.0/0
タイプ HTTP ソース カスタム 0.0.0.0/0, ::/0
タイプ HTTPS ソース カスタム 0.0.0.0/0, ::/0
追加したら、「ルールの保存」をクリックします。
スクリーンショット 2019-09-26 20.39.00 (1).jpg

データベース用のセキュリティグループも同じように作成します。
以下を設定し、保存します。
セキュリティグループ名 MyRDSG
説明 ForMyRDS
VPC 作成したVPC

スクリーンショット 2019-10-01 21.42.19.png
インバウンドのルール
タイプ MYSQL/Aurora ソース カスタム Webサーバー用のセキュリティグループ`
↑ここでデータベースにはWebサーバーからしか接続できないように設定しています
スクリーンショット 2019-10-01 21.44.42.png

6.EC2インスタンスを作成する

いよいよWebサーバーの作成です。
マネジメントコンソールのトップの画面に戻り、EC2を検索し、選択します。
スクリーンショット 2019-09-26 1.29.02.jpg

EC2ダッシュボードに移動し、「インスタンスの作成」をクリックします。
スクリーンショット 2019-09-26 1.29.36.jpg

今回は一番上のLinuxのマシンイメージ(AMI)を選択します。

スクリーンショット 2019-09-26 1.29.46.jpg

インスタンスタイプもデフォルトで選択されているものをそのまま選択します。
スクリーンショット 2019-09-26 1.30.06.jpg

インスタンスの詳細の設定画面で下記を設定し、「ストレージの追加」をクリックします。
ネットワーク作成したVPC
サブネット作成したパブリックサブネット
自動割り当てパブリックIP有効 ←パブリックIPアドレスを付与するかどうかの設定
スクリーンショット 2019-09-26 1.30.30.png

手順4ストレージの追加、手順5タグの追加は特に設定を変えずに、
手順6セキュリティグループの設定に移ります。
セキュリティグループの設定で
セキュリティグループの割り当て:既存のセキュリティグループを選択する
を選択肢、先程作成した「MyWebServerSG」を選択します。
スクリーンショット 2019-09-26 1.32.11.png

「確認と作成」をクリックすると次の画面で警告が出ますが(IPアドレスの制限をかけていないため)、このまま「起動」をクリックします。
スクリーンショット 2019-09-26 1.32.31.png
キーペア(SSHでアクセスするための鍵ファイル)を作成するためのポップアップが出るので、
キーペア名 MyTestKeyPairとし、キーペアをローカルの任意の場所にダウンロードします。
スクリーンショット 2019-09-26 1.32.56.png
ダウンロードが終わったら「インスタンスの作成」をクリックします。
インスタンス作成中の画面が表示されます。「インスタンスの表示」をクリックして、
インスタンスの状態:running
ステータスチェック:2/2のチェックに合格しました
になっていたら無事にインスタンス(サーバー)の作成、起動が完了です。

ここで、さっき作成したキーペアですが、このままだとパーミッション(権限レベル)がゆるすぎますよーという警告が出るので、ターミナルで600に変更します。おまじないみたいなものです。
Macのターミナルを開いて chmod 600 【キーペアのパス】を入力し、Enterキーを押します。
スクリーンショット 2019-09-29 10.20.19.png
エラーが出なければ変更完了です。

7.Elastic IPを作成、紐づけする

EC2インスタンス作成時にパブリックIPアドレスの付与を有効化しましたが、実はEC2インスタンスのパブリックIPは停止→起動のたびに違うものが付与されます。
何度起動しても同じIPアドレスを付与する機能がElastic IPです。

EC2ダッシュボードで「Elastic IP」を選択し、「新しいアドレスの割り当て」をクリックします。
スクリーンショット 2019-09-26 1.38.11.png
表示された画面でそのまま「割り当て」をクリックします。
ElasticIPの画面に戻り、「アクション」から「アドレスの関連付け」を選択します。
スクリーンショット 2019-09-26 1.38.25.png

「アドレスの関連付け」の画面で下記を入力します。
リソースタイプインスタンス
インスタンス作成したEC2インスタンス
プライベートIP作成したEC2インスタンスのプライベートIP
スクリーンショット 2019-09-26 1.38.38.png
「関連付け」をクリックして、作業完了です。

8.SSHでサーバーにログインしてみる

SSH(自分のPCから別のPCに安全にリモート接続できるコマンド)で、EC2インスタンスに接続してみます。
ターミナルを開いて下記を入力、Enterをクリックしてください。

ssh ec2-user@【EC2インスタンスのパブリックIPアドレス】 -i 【pemキーのパス】

インスタンスのパブリックIPアドレスは、EC2ダッシュボードから対象のインスタンスを選択し、
画面下部の「説明」タブの中で確認できます。

スクリーンショット 2019-09-26 1.41.21.png
つなげていいですかー?というメッセージが出るのでyesを入力し、Enterをクリックしてしばらく経つと、接続完了の旨のメッセージが表示されます。
スクリーンショット 2019-09-26 1.41.36.png

Webサーバーとして稼働させるため、apacheをインストールしていきます。
ターミナルに順番に下記のコマンドを入力してください。

↓管理者権限で実行

sudo su 

↓アプリケーションのアップデートの確認、実行

yum update -y

↓apacheインストール

yum install httpd -y

↓apache起動

service httpd start

起動していることを確認してみます。
↓フォルダ移動

cd /var/www/html

↓index.htmlファイルを作成し、viエディタを起動する

sudo vi index.html

index.htmlに下記のコードを貼り付けてください。

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <title>テストタイトル</title>
    </head>
    <body>
        <p>Hello AWS!</p>
    </body>
</html>

貼り付けたら、escキーをクリックした後「:」→「w」→「q」→「Enter」の順にクリックして保存してください。

ブラウザでEC2インスタンスのIPアドレスを入力して、画面が表示されたら成功です。
スクリーンショット 2019-10-01 22.32.12.png

9.RDSを構築する

まず、RDSインスタンスが所属するためのサブネットグループを作成します。
RDSダッシュボードの左側のメニューから「サブネットグループ」を選択し、「DBサブネットグループの作成」をクリックします。

「サブネットグループの詳細」エリアに下記を入力します。
名前TestDbSubnetGroup
説明ForTestDb
VPC作成したVPC
スクリーンショット 2019-09-26 2.00.42.png

「サブネットの追加」で、3.で作成したデータベース用のサブネット2つを追加します。
スクリーンショット 2019-09-26 2.00.58.png
スクリーンショット 2019-09-26 2.01.03.png
「作成」をクリックして完了です。

RDSインスタンスを作成します。
画面左上のサービスからRDSを検索し、RDSダッシュボードを表示します。
スクリーンショット 2019-09-26 1.59.28.png

「データベース」を選択し、「データベースの作成」をクリックします。
スクリーンショット 2019-09-26 1.59.52.png

「データベースの設定」で「Standard Create」、「エンジンのオプション」で今回は「MySQL」を選択します。
スクリーンショット 2019-09-26 2.01.36.png

「テンプレート」で「無料利用枠」を選択します。

「設定」-「認証情報の設定」でデータベースのマスターアカウントのユーザー名とパスワードを入力します。
スクリーンショット 2019-09-26 2.02.24.png

「DBインスタンスサイズ」「ストレージ」はデフォルトの設定を利用します。

「接続」の「VPC」で、作成したVPCを選択します。
スクリーンショット 2019-10-02 15.27.57.png

「接続」-「追加の接続設定」で以下を設定します。
サブネットグループ:作成したDBサブネットグループ
パブリックアクセス可能:なし
VPCセキュリティグループ:既存の選択
既存のVPCセキュリティグループ:データベース用に作成したセキュリティグループ
アベイラビリティゾーン:ap-northeast-1d
データベースポート:3306
スクリーンショット 2019-09-26 2.15.37.png

この状態で「データベースの作成」をクリックすると、データベースが作成中になります。
EC2と同様、しばらく経つとインスタンスが作成され、クリックすると詳細情報が確認できます。
スクリーンショット 2019-10-02 15.35.35.png
スクリーンショット 2019-10-02 15.39.40.png

10.WebサーバーからRDSにSSHでログインしてみる(疎通確認)

EC2インスタンスにMySqlクライアントをインストールします。
ターミナルを起動してさっきと同じようにSSHでEC2インスタンス(Webサーバー)に接続します。(すでに接続している場合はそのままでOK)

ssh ec2-user@【EC2インスタンスのパブリックIPアドレス】 -i 【pemキーのパス】

↓WebサーバーにMySQLクライアントをインストールします。

sudo yum -y install mysql

WebサーバーのMySQLクライアントからRDSに接続します。
下記を入力してEnterキーを押すと、パスワードを求められるので入力して再度Enterを押します。

mysql -h 【エンドポイント】 -P 3306 -u 【ユーザー名】 -p

接続できたら疎通確認完了です。
Webサーバー、データベースを構築し、Webサーバーからデータベースへ接続する一連の流れを構築できました。

11.ブラウザにデータを表示する

サーバーサイドにはPHPを使用します(個人的に構築が楽なので)。
データベースサーバーに接続している状態で、データを作って行きます。

↓データベースの作成(testDbという名前のデータベース)

create database testDb;

↓テーブル、カラムの作成
今回はuserテーブルと、その中にid(int型)、name(varchar型)を作成します。

create table testDb.user(id int , name varchar(20));

↓データを作成する

insert into testDb.user VALUES (1,'tanaka'),(2,'satou');

↓データが登録できたことを確認する

select * from testDb.user;

↓データベースサーバーとの接続を切り、Webサーバに戻る

quit

スクリーンショット 2019-10-01 23.33.37.png

↓EC2インスタンスにPHPとMySQLを扱うためのモジュールをインストールする

$ sudo yum -y install php php-mysql

.phpの拡張子のファイルがphpファイルとして読み込まれるように設定する
↓viエディタでhttpd.confファイルを開く

$ sudo vi /etc/httpd/conf/httpd.conf
<IfModule mime_module>
・・・(略)
</IfModule>

↑のタグで囲まれている部分があるので、下記のようにコードを追加する

<IfModule mime_module>
・・・(略)
AddType application/x-httpd-php .php //←追加
</IfModule>

編集が終わったらescキーをクリックした後 「:」→「w」→「q」→「Enter」の順にクリックして保存してください

↓下記を入力して、ブラウザで 【EC2インスタンスのパブリックIPアドレス】/index.php に接続し、テストページが表示されれば成功です。

 sudo sh -c "echo '<?php phpinfo(); ?>' >> /var/www/html/index.php"

phpファイルを、DB情報を取得・表示するコードに修正する

↓viエディタでindex.phpファイルを起動

sudo vi /var/www/html/index.php

↓下記コードの【】をそれぞれ編集し、index.phpに貼り付けてください。
※DB接続にmysqliクラスを使用していますが、現在非推奨です

<?php
// DB接続用クラス
$mysqli = new mysqli('【エンドポイント】', '【ユーザー名】', '【パスワード】', 'testDb');

// 接続
if ($mysqli->connect_error) {
    //失敗 
    echo $mysqli->connect_error;
    exit();
} else {
    //成功 文字コードセット
    $mysqli->set_charset("utf8");
}

// 処理
$sql = "SELECT id, name FROM user";
if ($result = $mysqli->query($sql)) {
    // 行ごとに処理(id+name+改行)
    while ($row = $result->fetch_assoc()) {
        echo $row["id"] . " " . $row["name"] . "<br>";
    }

    $result->close();
}
// 接続を閉じる
$mysqli->close();
?>

escキーをクリックした後 「:」→「w」→「q」→「Enter」の順にクリックして保存 
もう一度ブラウザに【EC2インスタンスのパブリックIPアドレス】/index.phpを入力して、データベースのデータが表示されていたら成功です。

スクリーンショット 2019-10-02 17.38.14.png

ハンズオンは以上になります。
今回はすべて無料利用枠の範囲内に収まるようにしていますが、無料時間枠を超えると請求が発生してしまうので、早めにリソースを削除することをおすすめします。(EC2インスタンス、RDS、Elastic IPは特に)

まとめ

ハンズオンが終了したら、冒頭に書いている用語や構成図を見直してみてください。
きっと理解しやすくなっていると思います。

また、興味を持ったサービスがあればぜひ公式のサービス別資料集を読んでみてください。
AWS サービス別資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Direct ConnectとNTTドコモ固定VPN接続について(覚書)

AWSのDirect Connectを利用してNTTドコモが提供している固定VPN接続サービスとの接続ではまったので記載しておきます。

【環境】
 NTTドコモの固定VPN接続サービス(UNOタイプL3)
 AWS側は以下のシナリオと同じ環境を構築
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/VPC_Scenario3.html

【はまった点】
 NTTドコモのプライベートIPアドレス体系
 末端の端末側:192.168.3.X
 Direct Connect用のVTI:192.168.4.X

 Direct Connect初心者のため、VTIのIPアドレスを見てVPCに192.168.0.0を割り当ててしまった。
 AWS側は、インターネット接続用のEC2インスタンス1台とVPN接続用のEC2インスタンス1台を起動して構築したのだが、当初どのようにルーティングしても192.168.3.Xへの通信(ping)ができなかった。
 原因は、EC2からのパケットは全てインターネットゲートウェイへ流れていたことが判明した。

 対応方法は、192.168.4.Xを2つのネットワークに分けて、一つは、インターネットゲートウェイ用、もう一つは仮想プライベートゲートウェイ用としてルートテーブルを作ったことで問題が解消された。

※今回、固定VPN接続サービスの申し込みが別会社から行ったことでIPアドレス体系について特に何も考えず割り当てたのではないかと考えています。
※また、特定の会社が提供しているこのようなサービスに対する参考資料がほぼ無いに等しく、サービス提供企業からのサポートも受けられないという点が困ったところです。
※Direct Connectも正しく設定できたのかの確認方法が不明でした。一応、up状態になっていたのでOKとして作業を進めて一応つながったからよかったものの、AWSの設定になるのでサービス提供企業には問い合わせができない状況でした。

乱筆でよくわからない文章ですが、とりあえず私が判ればよいので、今後整理してもう少し詳しく記載しようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた④

はじめに

本記事では、Udemy にて Stephane Maarek 氏 が提供している「Amazon EKS Starter: Docker on AWS EKS with Kubernetes」 について紹介していきます。

前回は、Kubernetes Dashboard のデプロイ方法について学んでいきました。今回は、stateless app の作成を行ってまいります。
過去の記事のリンクは以下を参照ください

Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた①
Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた②
Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた③

Deploy & inspect a stateless app

本記事で作成する stateless app の構成は、図の様になります。
image.png

  • フロントエンド:PHP App

    • AWS ELB(パブリック ELB)
    • 複数の Slave DB で読み込みの冗長化
    • シングル Master DB に書き込みリクエスト
  • バックエンド:Redis

    • シングル Master Pod(書き込み)
    • マルチ Slave Pods(読み込み)
    • Master と継続的に同期する複数の Slave を用意

Backend Deployment

image.png
Stateless app の構築にあたり、先ずはバックエンドを作成します。作成内容は以下です。

  • Redis Master pod
  • Redis Master service
  • Redis Slave pods
  • Redis Slave service

それでは、「redis-master.yaml」ファイルを使用して、Redis Master の作成を行います。
image.png
「kubectl apply -f redis-master.yaml」と入力し、「redis-master.yaml」ファイルをデプロイします。
「kubectl get pods」、「kubectl get service redis-master」と入力することで、Redis Master pod、Redis Master service がそれぞれ YAML ファイルの内容通りに作成されていることを確認できます。

同じように、「redis-slaves.yaml」ファイルを使用して、Redis Slave の作成を行います。
image.png
「kubectl apply -f redis-slaves.yaml」と入力し、「redis-slaves.yaml」ファイルをデプロイします。
「kubectl get pods」、「kubectl get service」と入力することで、Redis Slave pod、Redis Slave service がそれぞれ YAML ファイルの内容通りに作成されていることを確認できます。

次に、作成した pods の IP や worker node について確認してみます。
image.png
「kubectl get pods -o wide」と入力すると、作成した pods の情報が表示されます。例えば、1番上に表示されている redis-master の worker node は、ip-192-168-208-206.ec2.internal であることが分かります。

image.png
worekr node の情報は、「kubectl describe node [NODE]」と入力することで、取得することができます。例えば、「kubectl describe node ip-192-168-208-206.ec2.internal」と入力すれば、redis master の worker node の名前やラベルなどといった情報を取得できます。

Frontend Deployment

image.png

フロントエンドを作成します。
使用する YAML ファイル名は、「frontend.yaml」ファイルです。
image.png
「kubectl apply -f frontend.yaml」と入力し、デプロイが完了したら、「kubectl get pods -o wide」を入力します。これで、フロントエンド側で作成した pods の情報を見ることができます。
また、「kubectl get service frontend」と入力することで、

  • サービス名:frontend
  • タイプ  :LoadBalancer

と、フロントエンドサービスの情報を確認することができます。

これよりさらに、細かい情報を取得することもできます。
image.png
より詳しい情報を取得したい場合は、「kubectl describe service frontend」と入力します。
AWS のコンソール画面から、今回作成したサービスを確認することができるので、無事に作成できているか確認しましょう。

Scaling our pods up and down

それでは、次は pod のスケーリングアップ・ダウンを行ってみましょう。

Scaling Up

image.png
「kubectl scale --replicas 5 deployment frontend」とコマンド入力することで、pod を5つスケールアップします。

Scaling Down

今度は逆に、pod を5→4にスケールダウンさせます。
image.png
「kubectl scale --replicas 4 deployment frontend」と入力することで、pod を4つに変更します。

image.png
「kubectl get pods」と入力し、現在の pod の数を確認すると pod の数が減っているのが確認できます。

yaml ファイルを直接書き換えることでも、pod の数を変更することができます。
image.png
image.png
「frontend.yaml」ファイルの「replicas」を 3→10に変更します。

image.png
「kubectl apply -f frontend.yaml」と入力し、「kubectl get deployment fronted」と入力すると、pod の数が10にスケールアップしていることが確認できます。

スケーリングを行う際の注目点としては、レプリカセットが作成されていることです。
image.png
「frontend-5c548f4769」が、レプリカセットです。そして、注意することは、このレプリカセットの数字を直接変えようとしてはいけません。
「kubectl scale --replicas 4 rs frontend-5c548f4769」と入力しても、レプリカセットの数字は変わりませんし、ポッドの数も変わりません。

Self-Healing Mecahnism of K8s

最後に、K8s の自動修復について軽く紹介したいと思います。

image.png
図のように3つの pod が存在している状態で、その中の1つの pod を削除します。

image.png
[frontend-5c548f4769-bdlv2] を削除しても、代わりの[frontend-5c548f4769-thb9h] が作成されるので、pod の数は減ることはありません。

K8s の自動修復は、pod だけでなく redis-master node にも適用されます。
image.png
試しに、[redis-master-55db5f7567-l8jqq] を削除します。すると、削除した redis-master node に代わって [redis-master-55db5f7567-2dwpf] が新たに生成されました。
このように、K8s は pod だけでなく redis-master node も自動修復することができます。

おわりに

stateless app については以上となります。今回は、stateless app を作成するために、backend とfrontend の環境をそれぞれ作成しました。次回は、Amazon EFS を使った app の構築などについて紹介したいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた③

はじめに

本記事では、Udemy にて Stephane Maarek 氏 が提供している「Amazon EKS Starter: Docker on AWS EKS with Kubernetes」 について紹介していきます。

前回は、EKS で使われる主なサービスについて学んでいきました。今回は、 kubernetes Dashboard について紹介していきます。前回の記事のリンクは下記です。

Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた①
Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた②

Kubernetes Dashboard

本コースで利用する Kubernetes Dashboard については、下記のリンクを参照ください。本コースで利用する情報については、ここに全て用意されております。

Kubernetes Dashboard は以下の特徴があります。

  • 以下の情報について閲覧が可能

    • ワークロード
    • ネームスペース
    • サービス
    • ノード・ストレージ
    • メトリクスの利用
  • RBAC の許可に基づくコマンドの実行などが可能

  • 以下の環境を利用したセキュリティ

    • HTTPS
    • Bearer トークンを使用したログイン
    • RBAC を使用した UI コンポーネントへのアクセス確認

Create service account and RBAC rule

今回は、YAML ファイルを使用して以下を作成してまいります。使用する YAML ファイル名は、「dashboard-account-rbac.yaml」です。

  • Dashboard のためのサービスアカウント
  • Dashboard に特権を与えるための複数のロール
  • RBAC ロールにサービスアカウントをリンクさせるためのロール

ターミナルから、上記の YAML ファイルをアタッチします。
image.png
「 kubectl.exe apply -f dashboard-account-rbac.yaml 」と入力して、 YAML ファイルのアタッチは完了です。

Deploy Dashboard

次は、Dashboard をデプロイします。作成するリソースは全部で3となり、図で表すと以下となります。
image.png
作成するリソースとその内容

  • サービス
    • ポート、URL、ユーザフェイス
  • デプロイ
    • ダッシュボードポート
  • ボリューム
    • 秘密証明書

YAML ファイルは、「deploy-dashboard.yaml」を参照ください。
ターミナルから、上記の YAML ファイルをアタッチします。
image.png
「 kubectl.exe apply -f deploy-dashboard.yaml 」と入力して、 YAML ファイルのアタッチは完了です。
次に、「kubectl.exe get all --namespace kube-system --selector=k8s-app=kubernetes-dashboard」と入力します。
これで、サービス・デプロイ・ボリュームのリソースは作成完了しました。

Deploy metrics Add-Ons

それでは、アドオンを作成してまいります。
image.png
構成は図の様になります。そして、作成するのは以下の2つです

  • InfluxDB:Heapster のデータを置くストレージ
  • Heapster:Kuernetesのパフォーマンス情報を集約するコンポーネント

アドオン環境を作成するにあたり、以下の YAML ファイルを使います。

  • 「deploy-influxdb.yaml」
  • 「deploy-heapster.yaml」
  • 「deploy-heapster-rbac.yaml」

それでは、ターミナルに移り、アドオン環境を作成していきます。
image.png
上記の3つの YAML ファイルをデプロイしましょう。
「kubectl.exe apply -f deploy-[YAML ファイル名]」と入力することで、デプロイが完了します。
図の様に「created」がそれぞれ表示されれば、デプロイは無事完了です。

Create admin account and explore dashboard

「admin-service-account.yaml」ファイルを使って、admin サービスアカウントを作成していきます。
image.png
「kubectl.exe apply -f admin-service-account.yaml」と入力しましょう。
これでファイルのデプロイは完了です。

image.png
デプロイが完了したら、ターミナルの別ウィンドウを開いて「kubectl proxy」と入力します。
Kubernetes にアクセスする IP アドレスを指定します(今回は「127.0.0.1」を指定)。

ブラウザウィンドウから Kubernetes Dashboard へのサインイン画面を開きます。
image.png
サインイン画面を開くと、Kubeconfig か、Token のどちらかを選択する必要が出てきます。Token を選択しますが、まだ Token が用意できていないので、再びターミナルに戻って作成します。

image.png
図のコマンドを実行します。

image.png
実行すると、 Token の key が作成されるので、そちらをコピーします。

image.png
コピーした key を「 Enter token 」に貼りつけて、サインインを実行します。

image.png
サインインが無事に完了すると、図の様に Kubernetes のページに入れます。

おわりに

Kubernetes Dashboard のデプロイ方法についての紹介は以上です。
次回は、リソースのデプロイについて紹介していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた①

はじめに

本記事では、Udemy にて Stephane Maarek 氏 が提供している「Amazon EKS Starter: Docker on AWS EKS with Kubernetes」 について紹介したいと思います。

※注意
本コースでは、様々な AWS サービスを利用することになるので、請求金額には気をつけておく必要があります(コース内で Stephan 氏自身も、請求金額には気をつけるようにと述べております)。そのため、使わなくなったサービスやインスタンスの停止や削除、サービスの稼働時間をなるべく短くするなどして、本コースに取り組んで頂けたらと思います。

Introduction

Amazon EKS はマネージド型のサービスで、AWS 内で Kubernetes を実行することが可能となるサービスです。
Amazon EKS のユースケースとしては、以下が考えられます。
image.png

  • ユースケース毎にクラスターを作成
  • コンテナ内にマイクロサービスを用意
  • オンプレクラスターからクラウド Kubernetes クラスターへ移行
  • ML クラスターの用意

Set Up

それでは、 AWS EKS を利用する環境の準備をしていきたいと思います。
image.png
環境自体は、図のように3つの AZ を準備し、それぞれの AZ に対して Master Node、Etcd、K8s worker node を用意します。
また、それ以外にも IAM ユーザなども用意する必要があります。

IAM User

本コース用の IAM ユーザを作成します。基本的には root アカウントではなく、個別に作成した IAM ユーザを利用しましょう。
image.png
作成するIAMユーザには「AdministratorAccess」というポリシーをアタッチすることで、AWS 環境の全サービスを利用することができます。

作成が完了したら、作成した IAM User の「Security Credentials」を選択すると、「Create access key」という項目が表示されるので、そちらをクリックします。
image.png

image.png
「Create access key」をクリックしたら、csvファイルをダウンロードします。csvファイルはくれぐれも無くさないようにお願いします。

IAM Role

次に IAM Role を作成します。K8s がAWS リソースを作成できるようにするためです。
image.png
「Roles」を選択したら、「Create role」をクリックして作成に移りましょう。

image.png
「EKS」を選択します。
image.png
「Role name」「Role description」を入力したら、「Create role」を選択します。
これで IAM Role の作成完了です。

SSH

次に、SSH キーペアの作成を行います。
コンソール画面より EC2 の画面に移動して、「Key Pairs」を選択します。
image.png

「Create Key Pair」を選択して、キーペアの作成を行います。
image.png
作成が完了すると、pem ファイルがダウンロードされるので、ちゃんと保管しましょう。

VPC

次は、VPCを作成したいと思います。図は、今回作成する AWS の全体構成図です。
image.png

  • VPC:リージョンは us-east-1 を指定
  • Subnet:3つ用意し、全て同じ VPC 内に用意
  • Security Group:共通の内容で、IGW への接続が可能

CloudFormation

今回は、 CloudFormation を利用して上記の AWS 構成図を用意します。
image.png
コンソール画面より CloudFormation を選択し、「Create Stack」から作成します。

image.png
テンプレートは、本コースを受講することでダウンロードが可能となるファイルの中に用意されているので、そちらを利用します。ファイル名は「eks-course-vpc.yaml」です。

ここでは、作成したスタックの名前を決めます。
image.png
スタック名を入力したら、Option は飛ばしていただき、Review で作成するスタックの内容を最終確認して、「Create」を選択します。これで、作成完了です。

EKS Control Plane

次は、EKS 制御 Plane を作成します。
image.png

AWS コンソール画面より「EKS」を選択します。EKS の画面に飛んだら、「クラスター」を選択し、「クラスターの作成」を行います。
image.png
クラスター作成時には、以下の項目を埋めていきます。

  • クラスター名:任意
  • Kubernetes のバージョン:最新バージョンを使用
  • VPC(サブネットも自動的に選択されます):CloudFormation で作成した VPC を選択
  • ロール名:先程作成したものを選択
  • セキュリティグループ:先程作成したものを選択

項目を全部埋めたら、「Create」を選択して作成完了です。

Command line Cli tools の準備

次は Kubectl のセットアップを行います。
kubectl のインストールを行う際には、MobaXterm を利用するのがオススメですので、ぜひ利用してみて下さい。
image.png
インストールする項目は以下となります。それぞれ、AWS 公式より OS 毎に手順が用意されておりますので、そちらをご参照ください。

Worker node の準備

Amazon EKS ワーカーノードはスタンダードな Amazon EC2 インスタンスです。

EKS ワーカーノードを CloudFormation で作成します。
マネジメントコンソールから、CloudFormation を検索し、「Create Stack 」を選択します。
image.png
本コースでダウンロードしたコードファイルから、「eks-course-nodegroup.yaml」を選択して、アップロードします。

image.png
後は、インスタンスタイプなどを選択し、確認画面で内容を確認したら「Create」を選択して作成完了です。

image.png
EC2画面に移ると、3台のワーカーノードが作成されたのが確認できます。

おわりに

これで、無事に環境を用意することができました。
次回は、EKS のより詳しい内容についてお話しして行きたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた②

はじめに

本記事では、Udemy にて Stephane Maarek 氏 が提供している「Amazon EKS Starter: Docker on AWS EKS with Kubernetes」 について紹介したいと思います。

前回は AWS EKS の利用環境の構築を行いました。今回は、EKS についてより深く学んで行きたいと思います。前回の記事のリンクは下記です。

Amazon EKS Starter: Docker on AWS EKS with Kubernetes 受けてみた①

EKS in Depth

EKS Control Plane

EKS についてより深く学ぶ必要があります。
まずは EKS の制御プレーンについて理解しておきましょう。
image.png
EKS 制御プレーンは以下の特徴があります。

  • EKS Kubernetes の制御プレーンは高可用である
  • 他の利用者とシェアすることはない
  • AWS コンポーネントで構成されている
  • 制御プレーン全体が NLB で覆われている

EKS Networking - VPC

ネットワークも EKS におけるとても重要な要素です。
次は VPC について紹介していきます。
image.png

EKS における VPC 環境では、以下の構成を用意しておく必要があります。

  • プライベートサブネット:作成したワーカーノードを収容。CIDR は大きめに作成
  • パブリックサブネット:インターネットに向いているロードバランサを配置
  • VPC は、ノードが制御プレーンと接続可能にするためにも、DNS ホスト名と DNS Resolution を必ず用意しておく必要があります。

Security Group

次は、セキュリティグループについてです。
制御プレーンの SG、ワーカーノードの SG は、それぞれ以下の図の様に設定していきます。
image.png

設定した SG のインバウンド・アウトバウンドを図に興したものが以下となります。
image.png

EKS Pod Networking

EKS ポッドネットワーキングについて紹介します。
image.png
- Amazon VPC CNI プラグイン:それぞれのポッドがVPC内より1つのIPアドレス(ENI)を取得します
- ポッドはEKS クラスターの内外で、同じ IP アドレスを保有することとなります
- サブネットの制限について:
- CIDR / 24 は254 IP しか用意できないため、沢山のポッドを実行するには足りない
- CIDR / 18 は16,384 IP も用意できるため、こちらの方がおすすめ
- EC2 の制限:
- EC2 インスタンスは限定された ENI/IP アドレスしか保有できない

IAM & RBAC Integration in EKS

Kubernetes における IAM と RBAC の統合について紹介していきます。
この2つを合わせて使用することで、特定の IAM ユーザのみ、特定の環境でしか Kubernetes を利用することができません。
image.png
具体的な流れは以下となります。

  • Kubectl に対して AWS 情報を送信
  • kubectl が AWS 情報を IAMサービスと照会して情報を確認
  • IAM 情報で問題が無ければ、 RBAC で確認をし、IAM ポリシーに基づいた操作権限を認可

Load Balancers in EKS

最後に、ロードバランサについてです。EKS では、以下の3つのロードバランサに対応しております。

  • Classic Load Balancer
  • Network Load Balancer
  • Application Load Balancer

image.png
EKS はデフォルトで Classic Load Balancer を作成します。指定があった場合は、Network Load balancer を作成します。
ロードバランサの構成については、マニフェストに注釈をつけることでコントロールが可能です。
下記の URL から、Kubernetes で使用する AWS のロードバランサに関する情報を閲覧できます。

image.png
ALB Ingress は、EKS team によってサポートされています。ターゲットグループの IP や、インスタンスモードなどをサポートします。
オープンソースの URL は下記です。

おわりに

EKS 周りの詳細についての紹介は以上となります。
次回より、Kubernetes Dashboard のデプロイの仕方について紹介してまいります

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon SageMaker: Simplifying Machine Learning Application Development Course のページを和訳してみた

はじめに

今回は、edX にて提供されている「Amazon SageMaker: Simplifying Machine Learning Application Development」の紹介ページの和訳となります。機械学習や AWS SageMaker について学びたい方、そして本コースの受講を考えている方のお役に少しでも立てば幸いです。
こちらのコースは無料・有料の両方が用意されております。有料コースの場合は無料と違い、受講内容に制限は設けておらず、また、本コース終了時には修了証を受け取ることもできます。

About this course

機械学習は、テクノロジー分野において最も成長している分野の1つであり、そして今日の市場で特に求められているスキルセットでもあります。
本コースでは、アプリケーションデベロッパーに対して機械学習とアプリケーションの統合を簡略化するために Amazon SageMaker をどう使用すれば良いのか、を講義してまいります。

講義では以下の内容についても触れております。

  • 機械学習と機械学習によって解決可能な課題のまとめ
  • SageMaker のビルトインアルゴリズムをベースとしたモデルのトレーニングを Jupyter Notebook を使用して行う方法
  • SageMaker を使った検証済みモデルの配布方法

本講義終了時には、SageMaker にて作成したエンドポイントと統合したサーバレスなアプリの構築ができるようになるでしょう。
本コースは AWS の認定エキスパートがレクチャ、デモ、ディスカッション、そしてハンズオンでの実験を行います(コースの内容によっては追加費用が発生するものもあります)。

What you'll learn

本講義では、以下の内容についても学ぶことができます。

  • 本講義で学ぶ内容
    • 機械学習での取り組みや、解決が可能な問題について
    • Amazon SageMaker のビルトインアルゴリズムと Jupyter Notebook インスタンスを使ったモデルのトレーニング方法について
    • Amazon SageMaker を使ったモデルの公開方法について
    • 公開した SageMaker エンドポイントをアプリケーションと統合する方法について

Syllabus

本講義は、全部で 合計 4week 編成となっております。それぞれの week 毎に学ぶ内容について紹介してきます。

  • Week1

    • 機械学習と Amazon SageMaker について
    • 機械学習を使うタイミング
  • Week2

    • Amazon SageMaker Notebook と SDK について
    • Jupyter Notebook とは
    • Notebook とライブラリの使い方
  • Week3

    • 機械学習と Amazon SageMaker の用語やアルゴリズムについて
    • ハイパラメータチューニングについて
    • k-平均法アルゴリズムについて
    • XGBoost アルゴリズムについて
  • Week4

    • Amazon SageMaker とアプリケーションの統合方法について
    • サーバレス化の方法について

おわりに

Amazon SageMaker: Simplifying Machine Learning Application Development Course のページの和訳は以上となります。
無料で機械学習と Amazon SageMaker について学ぶことが可能なので、この分野に興味がある方は、ぜひ一度試してみてはいかがでしょうか。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2がvCPUベースのオンデマンドインスタンス起動数の制限に対応した

Amazon EC2 で vCPU ベースのオンデマンドインスタンス制限が利用可能に

EC2 オンデマンドインスタンス制限


vCPUベースのオンデマンドインスタンス起動数の制限に対応したらしいので、確認してみました。
これにより、オンデマンドインスタンスの起動数制限から、vCPUベースになった事でシンプルな管理が可能となりました。

従来は、インスタンスタイプ毎の上限数(Current limit)が設定されています。
image.png

「Opt in」と入力。
image.png

15分間待と、vCPU毎になる。
image.png

vCPUの計算は、簡易 vCPU 計算ツールを使えば簡単に確認することができる。
image.png

引き上げについてはどうなるのか?

Q: これらの制限は時間経過とともに変わりますか ?
はい。制限は時間の経過とともに変わる可能性があります。
Amazon EC2 は各リージョン内で使用状況を継続的に監視しており、EC2の使用に基づいて制限が自動的に引き上げられます。

Q: 制限の引き上げをリクエストするにはどのようにすればよいですか?
EC2 によって使用状況に基づきオンデマンドインスタント制限は自動的に引き上げられますが、必要に応じてAmazon EC2 コンソールの制限ページ、Service Quotas consoleの Amazon EC2 サービスページ、またはサービスクォータ API/CLI から制限引き上げをリクセストできます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS EC2 Amazon Linux2でDocker, Docker Composeをインストールする(忘備)

2019年10月現在で有効だった為、忘備録として残しておく

EC2 への Docker インストール

[root@ip-172-31-46-207 ~]# sudo yum install -y docker
Loaded plugins: extras_suggestions, langpacks, priorities, update-motd
Resolving Dependencies
--> Running transaction check
Installed:
  docker.x86_64 0:18.06.1ce-10.amzn2

Dependency Installed:
  libcgroup.x86_64 0:0.41-15.amzn2  libtool-ltdl.x86_64 0:2.4.2-22.2.amzn2.0.2
  pigz.x86_64 0:2.3.4-1.amzn2.0.1

Complete!
[root@ip-172-31-46-207 ~]# sudo service docker start
Redirecting to /bin/systemctl start docker.service
[root@ip-172-31-46-207 ~]#

ec2-user を docker グループに追加する

$ sudo usermod -a -G docker ec2-user

EC2 への Docker Compose インストール(スーパーユーザー)

[root@ip-172-31-46-207 ~]# sudo -i
[root@ip-172-31-46-207 ~]# curl -L "https://github.com/docker/compose/releases/download/1.11.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   617    0   617    0     0   2346      0 --:--:-- --:--:-- --:--:--  2346
100 8066k  100 8066k    0     0  2894k      0  0:00:02  0:00:02 --:--:-- 3416k
[root@ip-172-31-46-207 ~]# chmod +x /usr/local/bin/docker-compose
[root@ip-172-31-46-207 ~]# exit
logout
[root@ip-172-31-46-207 ~]# docker-compose --version
docker-compose version 1.11.2, build dfed245
[root@ip-172-31-46-207 ~]#

aliasを付与

[root@ip-172-31-46-207 ~]# alias d='docker'
[root@ip-172-31-46-207 ~]# alias dc='docker-compose'

参考サイト
https://qiita.com/shinespark/items/a8019b7ca99e4a30d286

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS】ユーザーアカウントに対して多重認証を行う

概要

AWSは多重認証を推奨していまして、ルートユーザーアカウントに多重認証を行いました。
「ルートユーザーアカウント」で操作していますが、IAMユーザーアカウントに対しても、操作は同じですので、参考にしてください。

多重認証の設定

  • マネジメントコンソールでログイン
  • 右上のアカウント名をクリック、[マイセキュリティ資格情報]をクリック

image.png

  • ルートユーザーアカウントの場合、以下の注意メッセージが表示されます。「セキュリティ保護の観点からIAMユーザーで操作しなさい。」と いう注意喚起ですね。
  • [Continue to Security Credentials]を選択
    image.png

  • セキュリティ認証画面が表示されるので[多要素認証]を選択し、[MFAの有効化]ボタンをクリック
    image.png

  • IAMユーザーの場合、ルートユーザーと画面レイアウトが若干、異なっているので、以下提示しておきます。
    <IAMユーザーの場合>
    image.png

  • 今回はYubicoを使用しますので、U2Fセキュリティキーを選択
    image.png

  • YubicoのセキュリティキーをPCに差し込んで、セキュリティキーにタッチし、処理を続行
    image.png

  • 設定が完了しました。

  • これで、次回のログイン時から、アカウント情報入力後、セキュリティーキーを求められるようになります。

  • 多重認証成功しました。
    image.png

多重認証の設定解除

  • 認証設定と同様に、右上のアカウント名をクリック、[マイセキュリティ資格情報]をクリックします。
    image.png

  • セキュリティ認証情報画面で設定したデバイスの[管理]リンクをクリック

  • 以下は、ルートユーザーの場合の見た目です。ISMユーザーアカウントは若干異なっています。[多重認証の設定]の画面レイアウトを参照ください。
    image.png

  • [削除]を選択し、[削除]を実行します。以上です。
    image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon Web Services (AWS)サービスの正式名称・略称・読み方まとめ #7 (機械学習)

Amazon Web Services (AWS)のサービスで正式名称や略称はともかく、読み方がわからずに困ることがよくあるのでまとめてみました。

Amazon Web Services (AWS) - Cloud Computing Services
https://aws.amazon.com/

まとめルールについては下記を参考ください。

Amazon Web Services (AWS)サービスの正式名称・略称・読み方まとめ #1 (コンピューティング) - Qiita
https://qiita.com/kai_kou/items/a6795dbab7e707b0d1a6

間違いや、こんな呼び方あるよーなどありましたらコメントお願いします!

Machine Learning - 機械学習

Apache MXNet on AWS

Amazon Comprehend

AWS Deep Learning AMI

AWS DeepLens

AWS DeepRacer

Amazon Elastic Inference

Amazon Forecast

Amazon Lex

Amazon Machine Learning

Amazon Personalize

Amazon Polly

Amazon Rekognition

Amazon SageMaker

Amazon Textract

Amazon Transcribe

Amazon Translate

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む