I'm a cloud engineer at foot of the small mountain

クラウド・ETL/ELT・Nocode・BigQuery などを中心に書ければね。最近触ったサービス・フレームワークなどのメモ

Lambda とEventBridgeで起動中のEC2とRDSをTeamsに通知してみた

普段から検証用に EC2 や RDS を頻繁に作成するのですが、検証後にそのまま放置してしまい、月末に注意を受けることが多々あります。

まぁ起動中なのかどうかは、コンソールをみれば一発でわかりますが、実際はそれすらも忘れてやれてませんでした。

なので、今回は EC2・RDS の起動中のリソースをチェックして、Teams に通知するものを作ってみました。

構成

使用している AWS サービスは Lambda と EventBridge だけです。
内容は Lambda で EC2 と RDS のステータスを確認し、 EventBridge で1日1回実行するようにしました。
最終的に確認結果は Teams にLambda から連携するような形です。

Teams でWebhook を作成

通知したいチャネルの・・・をクリックしてコネクタを選択します。
f:id:sennanvolar44:20210626214734p:plain

構成をクリックします。
f:id:sennanvolar44:20210626214907p:plain

Webhook 名やアイコンを設定後、以下のようにURL が表示されますので、コピーして保持しておきます。
f:id:sennanvolar44:20210626215056p:plain

これでTeams の設定は完了です。続いて Lambda でチェック処理を設定します。

Lambda でチェック処理

最初に今回使用したコードは以下になります。

import boto3
import pprint
import urllib3 
import json
import logging

from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
 http = urllib3.PoolManager() 
 teams="https://xxxxxx/xxxx/xxxx/"

 ############ EC2 ##############
 client = boto3.client('ec2')
 responce = client.describe_instances(Filters=[{'Name':'instance-state-name','Values': ['running']}])

 all_list = []
 cnt = 0
 for reservation in responce['Reservations']:
  for instance in reservation['Instances']:
      ec2_tags = dict([(tag['Key'], tag['Value']) for tag in instance['Tags']])
      if ec2_tags!='':
       if cnt == 0:
        all_list.append(" <b>**** EC2 **** </b>")
       all_list.append(ec2_tags['Name'])
       cnt+=1


 ############ RDS ##############
 rds = boto3.client('rds')
 responserds = rds.describe_db_instances()
 cnt = 0

 for i in responserds['DBInstances']:
  if i['DBInstanceStatus'] == 'available':
     db_instance_name = i['DBInstanceIdentifier']
     if cnt == 0:
      all_list.append("")
      all_list.append(" <b>**** RDS **** </b>")
     all_list.append(db_instance_name)
     cnt+=1

 ############ Teams に通知 ##############

 if all_list == []:
 # 起動中のものがない場合は通知しない
  pass
 else:
  request_data = {
      'title': "以下のリソースが起動中です。<br>",
      'text': ' <br>'.join(all_list)
      }

  request = Request(
      teams, 
      data=json.dumps(request_data).encode('utf-8'),
      method="POST"
  )

  try:
   with urlopen(request) as response:
        response_body = response.read().decode('utf-8')
        logger.info(response_body)
  except HTTPError as err:
      logger.error(err.code)
  except URLError as err:
      logger.error(err.reason)

そんなに特別なことをしているわけではありませんが、少しだけ部分的にみていきます。

 http = urllib3.PoolManager() 
 teams="https://xxxxxx/xxxx/xxxx/"

urllib3はHTTP通信用のモジュールのようで、今回は Teams にHTTPで連携するので定義してます。
下段で定義しているURL は Teams のチャネルでWebhook を構成したときに取得できるURLです。後ほど取得方法も説明します。


 client = boto3.client('ec2')
 responce = client.describe_instances(Filters=[{'Name':'instance-state-name','Values': ['running']}])

boto3 というAWS SDK を使用して EC2 や RDS のリソース情報を取得し、 describe_instances で起動中のインスタンスで絞ってインスタンス情報を取得しています。
describe_instances の詳しい内容は下記リファレンスを参照してください。

boto3.amazonaws.com


 for reservation in responce['Reservations']:
  for instance in reservation['Instances']:
      ec2_tags = dict([(tag['Key'], tag['Value']) for tag in instance['Tags']])
      if ec2_tags!='':
       if cnt == 0:
        all_list.append(" <b>**** EC2 **** </b>")
       all_list.append(ec2_tags['Name'])
       cnt+=1

'Reservations'、'Instances' はお決まりっぽい。レスポンスがこうなので。
f:id:sennanvolar44:20210626211612p:plain 最終的にはインスタンス名だけを取得して、配列に追加していきます。


RDS も EC2 とほぼほぼ同じ構成です。なので説明は割愛します。


  request_data = {
      'title': "以下のリソースが起動中です。<br>",
      'text': ' <br>'.join(all_list)
      }

  request = Request(
      teams, 
      data=json.dumps(request_data).encode('utf-8'),
      method="POST"
  )

上段は Teams に投げるタイトルとメッセージ内容です。 インスタンス名が配列にセットされているので、それぞれ改行しながら抜き出している形です。

下段は、Teams に実際に投げるリクエストの定義です。中身のデータはjson.dumps でJSON に変換してエンコードします。


  try:
   with urlopen(request) as response:
        response_body = response.read().decode('utf-8')
        logger.info(response_body)
  except HTTPError as err:
      logger.error(err.code)
  except URLError as err:
      logger.error(err.reason)

最後は Teams にリクエストを投げるところです。

これで、リソースの状況をチェックして Teams に連携するところまでができました。これでLambda でテストを実行すれば、アドホックですがTeams に通知を出すことができます。

EventBrigde で定期実行の設定を行う

Lambda の画面からトリガーを追加をクリックします。
※設定後なので既にEventBridgeが表示されてます f:id:sennanvolar44:20210626215747p:plain

EventBridge を選択します。
f:id:sennanvolar44:20210626215906p:plain

あとは新規ルール作成でスケジュールを選択し、実行したいタイミングをCron式で設定します。なお時間はGMT なので、ローカルタイムゾーンでちゃんと時間が合ってるか確認した方が良いですね。
f:id:sennanvolar44:20210626220821p:plain

これで全ての設定が完了しました!

通知イメージ

あとは時間まで待つと、こんな感じで Teams に通知がきます!
※色々インスタンス名に個人名など入ってたのでぼかしてます f:id:sennanvolar44:20210626221159p:plain

Docker ボリュームマウントしたときのバックアップ方法

前回はボリュームマウントを試してみたので、今回はそのマウントしたボリュームをバックアップする方法についてやってみます。

ボリュームのバックアップ方法

バインドマウントと違ってボリュームの場合は、対象のパスをそのまま tar などで固めてもバックアップファイルとしては有効ではないようです。
というのも、Docker Engine からバックアップを実行しているので、Docker Engineとしてのシステム領域をバックアップするということになり、対象のコンテナにリストアできないかもしれないのです。

バックアップ方法ですが、適当なLinux系のコンテナを起動して、そこに対して対象コンテナをマウントしてバックアップする形となります。

まずは対象コンテナが停止していることを確認します。

[ec2-user@~]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                      PORTS               NAMES
a6a1c6b8eb7a        mysql:5.7           "docker-entrypoint.s…"   7 minutes ago       Exited (0) 20 seconds ago                       mysqldb
[ec2-user@~]$

Linux のコンテナをここで起動します。バックアップとしてしか使用しないので、 busybox という軽量 Linux を使用します。

[ec2-user@~]$ docker run --rm -v samplevol:/src -v "$PWD":/dest busybox tar czvf /dest/backup.tar.gz -C /src .
~
[ec2-user@~]$ ll
total 7036
-rw-r--r-- 1 root root 7202585 Jan 13 21:46 backup.tar.gz
drwxr-xr-x 2 root root      24 Jan  9 21:49 bindData
[ec2-user@~]$

backup.tar.gz がカレントディレクトリに作成されているのが確認できました。

ちなみにそれぞれのオプションの意味ですが、

  • rm :コマンド実行後、コンテナを削除します
  • samplevol:/src :samplevol ボリュームを /src にマウントする
  • "$PWD":/dest busybox : Docker ホストのカレントディレクトリを /dest にバインドマウントする
  • tar czvf /dest/backup.tar.gz -C /src . : /src 以下を /dest/backup.tar.gz にバックアップする(/dest はカレントディレクトリにマウントしてる)

リストアしてみる

まずはボリュームを使用しているコンテナとボリュームを削除してみます。

[ec2-user@~]$ docker rm mysqldb
mysqldb
[ec2-user@~]$ docker volume rm samplevol
samplevol
[ec2-user@~]$

次にボリュームを作成します。

[ec2-user@~]$ docker volume create samplevol
samplevol

ここで、先ほどバックアップしたファイルを使ってリストアしてみます。

[ec2-user@~]$ docker run --rm -v samplevol:/dest -v "$PWD":/src busybox tar xzf /src/backup.tar.gz -C /dest
[ec2-user@~]$

/dest にボリュームマウント、/src にバインドマウントとし、カレントディレクトリにある backup.tar.gz が /src/backup.tar.gz として扱うことができます。
/dest はリストア先のボリュームにマウントしているので、/dest に展開されていきます。

これでボリュームをリストアしたので、MySQLコンテナを起動してみます。

[ec2-user@~]$ docker run --name mysqldb -dit -p 3306:3306 -v samplevol:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=pwsswd mysql:5.7
9aa13f0f109e531714cd22c241dbffbdcf2d23e349b20d316df3a152aa4069c6
[ec2-user@~]$

MySQL WorkBench で接続してみると、前に作成したテーブルが新しいコンテナでも見れるようになっていることを確認できました。
f:id:sennanvolar44:20210120063039p:plain

Docker ボリュームマウントしてDBデータを永続化してみる

今回は MySQL コンテナでボリュームマウントについて試してみます。ちなみに、前回はバインドマウントをやってみました。

miyamon44.hatenablog.jp

ボリュームマウントとは

Docker ホストで確保した領域を利用することで、前回試したバインドマウントはDocker ホストとは関係なしにホスト上のディレクトリをマウントする方法でした。

ボリュームマウントを使うメリットは、Docker で管理するので特にパスなどを考える必要がないということです。
自分のようにすぐにパスを忘れてしまうようなら、ボリュームマウントの方が必須かもですね。というより、どちらも利用ケースはあるにせよ、全体的にこっちの方が使いやすいのかな。

けど、Docker ホストのある場所でファイルやデータを格納してやりとりしたいときは、バインドマウントの方がわかりやすいかも。

なので使い分けは、

  • 単純にコンテナのデータを保存 ボリュームマウント
  • データのやり取り、保存データをホストで編集 バインドマウント

ですかね。

ボリューム関連のコマンド

コマンド 内容 サンプル
cretae ボリュームを作成 docker volume create samplevol
inspect ボリューム情報を確認 docker volume inspect samplevol
ls ボリュームリスト表示 docker volume ls
prune マウントされてないボリュームを削除 docker volume prune
rm ボリューム削除 docker volume create samplevol

ボリュームマウントでコンテナを起動

まずはボリュームを作成します。

[ec2-user@~]$ docker volume create samplevol
samplevol
[ec2-user@~]$ docker volume ls
DRIVER              VOLUME NAME
local               samplevol
[ec2-user@~]$

今回は MySQL コンテナを使ってデータ登録などを行い、その後コンテナを入れ替えてもデータが永続化されてるか確認します。
ではボリュームマウントで MySQL コンテナを起動します。

[ec2-user@~]$ docker run --name mysqldb -dit -p 3306:3306 -v samplevol:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=pwsswd mysql:5.7
d90f630bca70b3a53834584434ffba3a4033c90bac4be37cb5beb906bd4998d1
[ec2-user@~]$

MySQL コンテナに入ってデータを登録

コンテナ起動時に -p で3306 でパブリッシュしてるので MySQL WorkBench からアクセスすることができます。※EC2 のセキュリティインバウンドルールにポート3306 は追加しておく必要はあります。
f:id:sennanvolar44:20210111071003p:plain

適当にスキーマとテーブルを作成
f:id:sennanvolar44:20210111071448p:plain

もし、teraterm で直で MySQL コンテナに入る場合は docker exec コマンドを使います。

[ec2-user@~]$ docker exec -it mysqldb /bin/bash
root@d90f630bca70:/#
root@d90f630bca70:/#
root@d90f630bca70:/# mysql -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 5
Server version: 5.7.32 MySQL Community Server (GPL)

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

コンテナ再作成

ではコンテナを削除→新規作成しても、先ほど作成したデータが表示されるか確認します。

まずはコンテナを削除

[ec2-user@~]$ docker stop mysqldb
mysqldb
[ec2-user@~]$ docker rm mysqldb
mysqldb
[ec2-user@~]$
[ec2-user@~]$ docker ps -a
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS                    PORTS               NAMES
d256c64c0371        httpd:2.4           "httpd-foreground"   24 hours ago        Exited (0) 24 hours ago                       webapp2
[ec2-user@~]$

さっき作成したボリュームをマウントして mysqldb2 という名前で MySQL コンテナを起動してみます。

[ec2-user@~]$ docker run --name mysqldb2 -dit -p 3306:3306 -v samplevol:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=pwsswd mysql:5.7
3e442b1084df59a740b96c11218fc2a773133ad587ce90a62b68169b1f1cc6cc
[ec2-user@~]$

MySQL Workbench でアクセスしてみると、先ほど作成したテーブルやレコードが表示されました!
f:id:sennanvolar44:20210111072238p:plain

Docker バインドマウントしてデータを永続化してみる

前回はApache コンテナを起動してみました。

miyamon44.hatenablog.jp

このままだと一度コンテナを削除するとファイルや設定情報などは消えてしまいますので、バインドマウントをしてデータ保存先を Docker から分離してみたいと思います。

バインドマウントしてみる

最初に対象のディレクトリを作成してみます。

[root@ec2-user]# mkdir bindData
[root@ec2-user]# ll
total 0
drwxr-xr-x 2 root root 6 Jan  9 21:01 bindData
[root@bindData]# pwd
/home/ec2-user/bindData

作成した以下ディレクトリをバインド先にします。
/home/ec2-user/bindData

ではコンテナを起動してみようと思いますが、その前に前回作成したコンテナが残ってたので削除しちゃいます。

[root@bindData]# docker stop webapp1
webapp1
[root@bindData]# docker rm  webapp1
webapp1
[root@bindData]#

これできれいになったのでコンテナを起動してみますが、その際に赤字部分にマウントするパスをセットしてあげます。
docker run -dit --name webapp1 -p 8080:80 -v /home/ec2-user/bindData:/usr/local/apache2/htdocs/ httpd:2.4

[root@bindData]# docker run -dit --name webapp1 -p 8080:80 -v /home/ec2-user/bindData:/usr/local/apache2/htdocs/ httpd:2.4
1e6796d3fd27a9d0a630d7c6809d53aa86eb723345e7da93300363445f295bdd
[root@bindData]#
[root@bindData]# docker ps
CONTAINER ID        IMAGE               COMMAND              CREATED             STATUS              PORTS                  NAMES
1e6796d3fd27        httpd:2.4           "httpd-foreground"   5 seconds ago       Up 4 seconds        0.0.0.0:8080->80/tcp   webapp1
[root@bindData]#

コンテナは起動されましたので、URL:ポートでアクセスしてみます。
f:id:sennanvolar44:20210110064315p:plain
何もないのでこうなりますよね。ではindex.html を置いてみます。

[root@bindData]# ll
total 4
-rw-r--r-- 1 root root 154 Jan  9 21:49 index.html
[root@bindData]# cat index.html
<html>
  <head>
    <meta http-equiv="content-type" charset="utf-8">
  </head>
  <body>
    <div>Docker バインドマウント</div>
  </body>
</html>

[root@bindData]#  

再度ブラウザをリロードしてみると表示されました。
f:id:sennanvolar44:20210110065102p:plain

コンテナを新たに作成

今起動しているコンテナを停止、削除後、再度Apacheコンテナを起動します。(今回はwebapp2という名前にしてみました)

[root@bindData]# docker stop webapp1
webapp1
[root@bindData]# docker rm webapp1
webapp1
[root@bindData]#  
[root@bindData]# docker run -dit --name webapp2 -p 8080:80 -v /home/ec2-user/bindData:/usr/local/apache2/htdocs/ httpd:2.4
d256c64c0371a6e961ee66b6fc1e84f85fa9810ba165e23df91e3f0e739fbf41
[root@bindData]#

新たに作成したコンテナでも index.html ちゃんと表示されました。 f:id:sennanvolar44:20210110070557p:plain

おわり。

Amazon Linux にDocker を入れてApache コンテナを起動してみた

EC2でDocker を使ってWebサーバを作ってみたのでメモ。

EC2 準備

まずは EC2 で VM を用意します。今回は Amazon Linux を利用。

f:id:sennanvolar44:20210108063522p:plain

ポートはとりあえずデフォルトの22 に加えて、8080 も追加。
f:id:sennanvolar44:20210108063839p:plain

キーは以前作成してたのでそれを流用しましたが、なければ新規作成する。
f:id:sennanvolar44:20210108064759p:plain

これで VM の用意が完了です。

Docker Engine のインストール

まずは Teraterm で作成した VM にアクセスします。
Amazon Linux の場合、ユーザー名は ec2-user になります。あとは VM 作成時に指定した秘密鍵を設定。
f:id:sennanvolar44:20210108065412p:plain

以下コマンドで Docker Engine をインストールします。

$ sudo yum update -y
$ sudo amazon-linux-extras install docker
$ sudo yum install docker

インストール後、バージョンを確認する場合

# docker --version
Docker version 19.03.13-ce, build 4484c46

今回は Amazon Linux へのインストールだったので、手順はこちらを参考にしました。
docs.aws.amazon.com

もし、一般ユーザーでdocker コマンドが実行できない場合は、docker グループに入れてあげることで実行できるようになります。

$ sudo usermod -a -G docker ec2-user

Webサーバ起動

いまは VM で Docker を動かすための Docker Engine をインストールまでしたところなので、以下のコマンドで Docker Hub から Apache の Docker Image をダウンロードして実行します。

docker run -dit --name webapp1 -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:2.4

  • -dit : コンテナをバックグラウンドで実行させる
  • --name : コンテナ名
  • -p : Docker ホストとコンテナのポート番号。外部からは8080 でアクセスし、ホストからは 8080 と紐づいたコンテナのポート80 にアクセス
  • -v : ホストのディレクトリ、コンテナのディレクト

それでは実行してみます。

[ec2-user@ip-172-31-42-9 ~]$ docker run -dit --name webapp1 -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:2.4
docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?.
See 'docker run --help'.

もし上記のようなエラーが発生した場合、Docker 自体が起動していないという意味ですので、起動してあげます。

[ec2-user@ip-172-31-42-9 ~]$ sudo service docker start
Redirecting to /bin/systemctl start docker.service
[ec2-user@ip-172-31-42-9 ~]$

実行して ID が返ってきたら起動できたということですので、 ステータスをみてみます。

[ec2-user@ip-172-31-42-9 ~]$ docker ps
CONTAINER ID        IMAGE               COMMAND              CREATED              STATUS              PORTS                  NAMES
a374217edee6        httpd:2.4           "httpd-foreground"   About a minute ago   Up About a minute   0.0.0.0:8080->80/tcp   webapp1

Status がUp になってるので起動しているのが確認できます。
ちなみに、-a を最後に付与すると停止中のコンテナも表示されます。

この状態で EC2 のIPアドレスと8080でブラウザからアクセスするとWeb ページが表示されるようになります。(まだ何もしてないのでカレントディレクトリの中身が表示されてしまいますが・・)
f:id:sennanvolar44:20210109133959p:plain

もし以下のように 403 エラーが出たら、対象ディレクトリのパーミッションを変更してあげます。 f:id:sennanvolar44:20210109133655p:plain

[ec2-user@ip-172-31-33-236 home]$ chmod 755 ec2-user/
[ec2-user@ip-172-31-33-236 home]$ ll
total 0
drwxr-xr-x 2 root     root       6 Jan  9 04:33 centos
drwxr-xr-x 4 ec2-user ec2-user 109 Jan  9 04:24 ec2-user
[ec2-user@ip-172-31-33-236 home]$

あとは、ホスト側の対象ディレクトリ内にindex.html 置くなどして利用できます。

OnsenUIを触ってみたのでタグとかのメモ

f:id:sennanvolar44:20200211003300p:plain
結構前に初めて monaca で Onsen UI を触ってみましたので、ほんの少しだけタグ中心のメモです。

Onsen UI とは

アプリ開発の際に画面を作成するためのフレームワークだそう。

ja.onsen.io

各種タグや処理の説明

<ons-navigator>

複数のHTML ファイルを操作する役割を持つ。画面上には表示されない。また、次ページ前ページへのページ遷移機能も持っている。

<ons-navigator page="初期表示ページ"></ons-navigator>

<ons-page>

Onsen UI でページを定義するために必要なタグとなる。

<ons-page>
  ・
   ・
   ・
  ・
</ons-page>

<ons-toolbar>

画面上部に表示されるツールバー

    <ons-toolbar>
        <div class="center">画面1</div>
    </ons-toolbar>

<ons-button>

画面に表示されるボタン。

        <ons-button> OKボタン</ons-button>

OKボタン押下でページ遷移する場合

<ons-button onclick="document.getElementById('id').pushPage('B.html')">
OKボタン
</ons-button>

TOP画面に戻る。内部で保持していたページの履歴もリセットされる。

<ons-button onclick="document.getElementById('id').resetToPage('TOP.html')">
TOPボタン
</ons-button>

<ons-back-button>

前の画面に戻るボタン。

<ons-back-button>Back</ons-back-button>

<ons-list>と<ons-listi-tem>

リスト形式でデータを一覧表示するためのタグ。リスト内のデータを1件ずつ扱う場合は、<ons-listitem>。

<ons-list>
 <ons-list-item>項目1</ons-list-item>
 <ons-list-item>項目2</ons-list-item>
 <ons-list-item>項目3</ons-list-item>
</ons-list>

初期処理

Onsen UI でのInit処理。

document.addEventListener("init", function(event) {
 if (event.target.id == "ページの ID") {
 //ページの初期処理
 }
});