20191012のAWSに関する記事は17件です。

FTP(SSH)ユーザをもう作成したくないのでS3に任せる

あるある

WEBサイトが大きくなってくると、関係するシステム会社、サービス会社が増えてきますし、サイト自体の運用に携わる人も増えてきます。

で、よくあるのが、「サービスで利用するCSVファイルを自動で連携するためにSSHユーザください。」、「サイト運営するメンバが増えたので、FTPでアップするためのユーザを発行してください。」といった依頼です。

これ、結構大変なんですよね。

useradd したら終わりじゃん。というご意見もあるかもですが、最近は公開鍵認証のための鍵作成や、踏み台サーバ経由になってるゆえの多段SSH設定、セキュリティ対策のためのchroot、接続方法マニュアルの作成等々・・結構大変です。

AWSさんお願いします

そんなシステム保守担当の煩わしさを解決するために、「AWSに任せよう」と考えました。

ユーザの管理はIAMにお願いすることで、簡易にユーザ管理できます。

ファイルアップロードはS3にお願いします。システム会社であればaws-cliとアクセスキーを使いこなしてもらえればいいですし、サイト運営者さんは、AWSの管理コンソールからGUIでファイルアップロードしてもらいます。

で、具体的なファイル側の同期方法は2種類です。

  • S3バケットをOS側からマウントする
  • S3バケットをOS側から同期する

2種類のメリットデメリットです。

  • S3バケットをOS側からマウントする

    • S3側、OS側から両方の更新に対応
    • 大量ファイルになるときにS3、EBSの内、EBSの費用分(利用分)を削減できる
    • S3ゆえに結果整合性の縛りが発生する
    • S3ゆえにファイル更新が多い場合に不利
    • マウント自体のシステム保守が発生する(しっかりマウントしているかの生存確認など)
  • S3バケットをOS側から同期する

    • S3側からは、OS側で行われた更新がみえない
    • S3及びEBSの費用が計上される
    • EBSゆえの高速ファイル処理が可能
    • cron+rsyncによる同期で十分だと考えるが、リアルタイム同期が必要な場合、Lambdaを書く必要あり

後者はファイルを2重管理しているという点がシンプルでないので、個人的には前者を推しますが、S3側からしか更新がないといった場合は、後者でもよいかなとおもいます。

以下、具体的なファイル同期の手順です。

下準備

2つの方法で共通で必要な作業として、以下を実施します。

  • バケット(今回は、test-iyaiya-ftp)を作成
  • 上記バケットにアクセスできるポリシーを作成
  • 作業用EC2に割り当てるようのロールを作成し、上記ポリシーを付与
  • 上記ロールをEC2に付与

尚、今回のEC2はAmazon Linux2を利用します。

S3バケットをOS側からマウントする

必要なパッケージをYUMって、goofysコマンドを取得します。

# yum install golang fuse
# wget https://github.com/kahing/goofys/releases/download/v0.0.5/goofys -P /usr/local/bin/
# chmod 755 /usr/local/bin/goofys

fstabを使って、自動マウントの設定を行います。

# vi /etc/fstab

/usr/local/bin/goofys#test-iyaiya-ftp <マウントしたいディレクトリ> _netdev,allow_other,--file-mode=0666,--uid=<マウントしたいディレクトリを所有するユーザのUID>,--gid=<マウントしたいディレクトリを所有するユーザのGID> 0 0

# mount -a

あとは、df を叩いて正しく、S3をマウントできかを確認できれば完了です。
マウントできない場合は、/var/log/message をみたり、S3のアクセス権限があるかなどをみれば解決していくとおもいます。

S3バケットをOS側から同期する

まずは、下記コマンドを実行できるか確認します。

# aws s3 sync s3://test-iyaiya-ftp/ <同期したいディレクトリ>

で、問題がなければ、/etc/crontab にでも書いてください。
軽く前述しまいたが、基本的にcronで1分やら5分毎に同期すればいいとおもいますが、リアルタイム性が必要な場合は、S3にファイルをアップしたイベントをキャッチして、Lambdaで実行させるなどの工夫が必要です。

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

AWS EC2インスタンスの作成手順

はじめに

EC2インスタンスの作成手順をメモする

EC2インスタンスの作成

  • AWS マネジメントコンソールにログイン
  • 下記画像のサービス→EC2をクリック スクリーンショット 2019-10-12 23.05.11.png
  • インスタンスの作成をクリック スクリーンショット 2019-10-12 23.10.03.png
  • AMIを選択 スクリーンショット 2019-10-12 23.12.41.png Amazon Linux AMIを選択する
  • タイプの選択 スクリーンショット 2019-10-12 23.15.14.png 図のように上から2つ目の無料枠を選択しておく。その後画像右下の【確認と作成】をクリック
  • インスタンス作成の確認画面
    • ここでは右下にある【起動】をクリック。
  • キーペアをダウンロードする
    • modalが表示されるので、【新しいキーペアの作成】を選択し、【キーペア名】を任意で入力後、キーペアのダウンロードを行う。
    • 【インスタンスの作成】をクリック
  • インスタンス一覧からインスタンスを選択
    • インスタンスIDをメモ # Elastic IPと紐付ける - 下図の Elastic IPをクリック後、【新しいアドレスの割り当て】をクリック スクリーンショット 2019-10-12 23.28.27.png スクリーンショット 2019-10-12 23.29.21.png
    •  その後、割り当てをクリック
    •  その後、閉じるをクリック
  • 取得したElastic IPアドレスを紐づける スクリーンショット 2019-10-12 23.29.21.png 上図の【アクション】から【アドレスの関連付け】を選択
  • 【アドレスの関連付け】ページにあるインスタンスには先ほどメモしたIDを入力、【プライベートID】には入力しない、【関連付け】をクリック
  • インスタンス画面からElastic IPが紐づけられたことを確認する

ポートを開く

  • インスタンス画面
    • セキュリティグループのリンクをクリック
    • 「インバウンド」タブの中の「編集」をクリック
    • 開かれたモーダルで、ルールの追加」をクリック、タイプを「HTTP」、プロトコルを「TCP」、ポート範囲を「80」、送信元を「カスタム / 0.0.0.0/0, ::/0」に設定

EC2インスタンスへのログイン

$ cd ~

$ mv Downloads/鍵の名前.pem .ssh/

$ cd .ssh/

$ chmod 600 鍵の名前.pem

$ ssh -i 鍵の名前.pem ec2-user@作成したEC2インスタンスと紐付けたElastic IP
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS EC2作成からSSH接続

前提条件

AWSアカウントを持っていて、AWSコンソールにアクセスできる

EC2の作成

  • AWSコンソールにアクセス
  • サービスよりEC2へ
  • インスタンスの作成
  • Amazon Linux AMI 2018.03.0 (HVM), SSD Volume Type - ami-92df37edを選択して、インスタンスを作成

※キーペアを作成した場合、保存場所を忘れないように!
  接続するのに使います

作成後のダッシュボード画面

スクリーンショット 2018-06-24 9.33.40.png

  • 作成したインスタンスを選択後、接続ボタンをクリック
  • 接続方法が表示される

一部抜粋

インスタンスにアクセスするには:
SSH クライアントを開きます。 (PuTTY を使用した接続の方法を確認)
プライベートキーファイル (***.pem) を見つけます。ウィザードが、インスタンスを作成するために使用するキーを自動的に検出します。
SSH が機能するには、キーが公開されていないことが必要です。必要な場合は次のコマンドを使用します。
chmod 400 ***.pem
インスタンスに接続するには、パブリック DNS を使用します。
EC2インスタンス名が表示されている
例: 
ssh -i "プライベートキー" ec2-user@"EC2のパブリックDNS名"
ほとんどの場合、上のユーザー名は正確ですが、AMI の使用方法を読んで AMI 所有者がデフォルト AMI ユーザー名を変更していないことを確認してください。
インスタンスへの接続に関してアシスタンスが必要な場合は、接続ドキュメントを参照してください。

ターミナルでの接続

$ キーペアのあるディレクトリに移動

$ chmod 400 ***.pem

$ ssh -i "***.pem" ec2-user@"EC2のパブリックDNS名"

The authenticity of host 'ec2-*-*-*-*.ap-northeast-1.compute.amazonaws.com (*.*.*.*)' can't be established.
ECDSA key fingerprint is SHA256:aDABcIu4l1SfKoEZwPpyGPcrXvmh2kiaIvaGxpkOazY.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'ec2-*-*-*-*.ap-northeast-1.compute.amazonaws.com,*.*.*.*' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2018.03-release-notes/
11 package(s) needed for security, out of 13 available
Run "sudo yum update" to apply all updates.
[ec2-user@ip**** ~]$ 

$マークの左側が[ec2-user@ip**** ~]になっていれば、SSH接続が完了

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

AWS Ubuntu 18 で fluentd (td-agent) を使って CloudWatch Logs へログを送ってみる

Ubuntu版の記事があまり見当たらなかったので、なるべく分かり易く纏めてチュートリアル風にしてみました。
※AWSのUIはコロコロ変わるみたいなので、参考までに。(2019/10/12)

チュートリアル

このチュートリアルでは、Ubuntuサーバーの認証ログ(/var/log/auth.log)をCloudWatchへ送って、AWSコンソール上でSSHのログイン・ログアウトを監視してみます。

  • AWSでEC2サーバー(Ubuntu)を立てる
  • アクセスキーID、シークレットアクセスキーを取得する
  • EC2サーバー(Ubuntu)にtd-agentを設定する
  • ログ送信テスト

AWSでEC2サーバー(Ubuntu)を立てる

まずはEC2にUbuntuサーバーを立てて起動している事を前提としています。
このチュートリアルでは、「Ubuntu Server 18.04 LTS (HVM), SSD Volume Type」(t2.micro) を使用します。
どのリージョンでも良いですが、東京リージョンはレスポンス早くて快適です。

00_EC2.png
※EC2設定はコチラの記事が分かり易いと思います。この記事は「Amazon Linux 2」で書かれているので、ここを「Ubuntu Server 18.04」に読み替えて下さい。

アクセスキーID、シークレットアクセスキーを取得する

td-agentを使ってサーバーからAWSへログを送信するには、アクセスキーIDとシークレットアクセスキーが必要です。アクセスキーIDとシークレットアクセスキーは、AWSコンソールでユーザーを作成する事で取得出来ます。

ユーザーの追加

[セキュリティ、ID、およびコンプライアンス] > [IAM] をクリックします。
00_AIM.png

[ユーザ] > [ユーザーを追加] をクリックします。
01_user.png

[ユーザーを追加] 画面で、ユーザー詳細の設定を行い、[次のステップ:アクセス制限] をクリックします。
- ユーザー名 td-agent-user
- アクセスの種類 プログラムによるアクセス にチェックを入れる
02_user_add_1.png

アクセス許可の設定画面では、[既存のポリシーを直接アタッチ]をクリックし、ポリシーのフィルタに「CloudWatchLogsFullAccess」と入力して、検索結果に表示されたら左端のチェックをONにして[次のステップ:タグ]をクリックします。
- アタッチするポリシー名 CloudWatchLogsFullAccess
02_user_add_2.png
 ※この権限があれば、CloudWatchにログ送信する事が出来ます(もっと機能を限定してもいいかもです)。

後は、次の[ユーザータグの追加]で[次のステップ:確認]をクリックし、最後の[確認]画面で[ユーザーの作成]をクリックすれば完成です。
02_user_add_3.png
表示されるアクセスキーIDとシークレットアクセスキーを忘れずメモしておきましょう。
これでAWS側は準備が整いました。

EC2サーバー(Ubuntu)にtd-agentを設定する

EC2サーバー(Ubuntu)側の設定を行う為、サーバーに初期アカウント(Ubuntu)で接続します。

ssh -i .ssh/pri-key.pem ubuntu@ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com

td-agentパッケージをインストール

ここではUbuntu 18 (Bionic)用のパッケージをインストールします。

curl -L https://toolbelt.treasuredata.com/sh/install-ubuntu-bionic-td-agent3.sh | sh

※他のバージョンをインストールする場合は、以下のサイトから探してください。
Install by DEB Package (Debian/Ubuntu)

インストール直後はサービス起動するので、一旦停止させておきます。

sudo systemctl stop td-agent

プラグインfluent-plugin-cloudwatch-logsのインストール

次に、td-agentからAWSのCloudWatchにログを送信する為のプラグインをインストールします(※こちらはsudoを使います)。
インストール用のツール(fluent-gem)は、td-agentインストール時に用意されているものを使います(gemのインストールは不要です)。

sudo /opt/td-agent/embedded/bin/fluent-gem install fluent-plugin-cloudwatch-logs

AWS環境変数の設定

/etc/default/td-agentファイルを編集して、リージョン(東京:ap-northeast-1)、アクセスキーID、シークレットアクセスキーを設定して下さい。サービス再起動時に有効になります。

/etc/default/td-agent
# This file is sourced by /bin/sh from /etc/init.d/td-agent
# Options to pass to td-agent
TD_AGENT_OPTIONS=""
AWS_REGION="ap-northeast-1" ←リージョンを合わせる
AWS_ACCESS_KEY_ID="xxxxxxxxxxxxxxxxxxxx" ←書き換える
AWS_SECRET_ACCESS_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ←書き換える

※ちなみに、/etc/init.d/td-agentは今回の方法では不要なので書き換えは行いません。

td-agent.conf設定

/etc/td-agent/td-agent.confを編集し、ログ送信設定を行います。ここでは、認証ログ(/var/log/auth.log)を指定しています。

/etc/td-agent/td-agent.conf
<source>
  @type tail
  path /var/log/auth.log
  pos_file /tmp/auth.log.pos
  tag syslog.auth.log
  format none
</source>

<match syslog.**>
  @type cloudwatch_logs
  log_group_name logstest
  log_stream_name targetserver/var/log/auth.log
  auto_create_stream true
</match>

 ※デフォルトのtd-agent.confは全て消して、上記に置き換えてください。

設定パラメータについて:
<source>
- @type tail 監視対象ログファイルに新たなログが追記された分を送信対象とする
- path /var/log/auth.log 送信対象ファイル(今回はUbuntu標準で出力されている認証ログを指定)
- pos_file pathの読み込み位置を記録するファイル
- tag syslog.auth.log ここで設定したtagは <match> で検知されます
- format none fluentdの標準的なフォーマットでログを調整する(noneでも何もしない訳では無い)

<match syslog.**>
- @type cloudwatch_logs プラグインfluent-plugin-cloudwatch-logsでログを送信
- log_group_name logstest CloudWatchのロググループ名を指定
- log_stream_name targetserver/var/log/auth.log CloudWatchのログストリーム名を指定
- auto_create_stream true ロググループ・ログストリームを自動的に作成する
※設定についてはgithubに詳しく書かれています。

td-agentユーザーをadmグループに所属させる

(※本セクションはvar/log/auth.logをCloudWatchに送る為に必要な設定です。それ以外のログ送信では恐らく不要です。)

このチュートリアルでは/var/log/auth.logのログをCloudWatchに送りますが、auth.logファイルは所有者でも所有グループでもない"その他のアカウント"からの読み込みは禁止されています。

$ ls -la /var/log
  :
-rw-r-----   1 syslog    adm   212987 Oct 12 07:46 auth.log
  :
  ※認証ファイル(auth.log)は、admグループに所属しています。

td-agentサービスは、td-agentをインストールした際に自動生成されたユーザー(td-agent)で実行されるので、
ここでは対策として、ユーザーtd-agentをグループadmに所属させる事でファイルを読み込める様に設定します。

sudo gpasswd -a td-agent adm

設定が出来たか確認します。

$ cat /etc/group | grep adm
adm:x:4:syslog,ubuntu,td-agent
  :

admグループにtd-agentが加わった事が確認出来ました。これでauth.logにアクセス出来る様になります。

td-agentサービス再起動

準備が整いました。サービスを再起動して設定を読み直します。

sudo systemctl start td-agent

各種設定がうまく行けば、td-agentサービスに下記の様なログが確認出来る筈です。

$ tail -f /var/log/td-agent.log
2019-10-12 08:24:51 +0000 [info]: gem 'fluent-plugin-s3' version '1.1.11'
2019-10-12 08:24:51 +0000 [info]: gem 'fluent-plugin-td' version '1.0.0'
2019-10-12 08:24:51 +0000 [info]: gem 'fluent-plugin-td-monitoring' version '0.2.4'
2019-10-12 08:24:51 +0000 [info]: gem 'fluent-plugin-webhdfs' version '1.2.4'
2019-10-12 08:24:51 +0000 [info]: gem 'fluentd' version '1.7.0'
2019-10-12 08:24:51 +0000 [info]: adding match pattern="syslog.**" type="cloudwatch_logs"
2019-10-12 08:24:51 +0000 [info]: adding source type="tail"
2019-10-12 08:24:51 +0000 [info]: #0 starting fluentd worker pid=20217 ppid=20210 worker=0
2019-10-12 08:24:51 +0000 [info]: #0 following tail of /var/log/auth.log
2019-10-12 08:24:51 +0000 [info]: #0 fluentd worker is now running worker=0

うまく行かない場合は、もう一度設定を見直して見て下さい。

ログ送信テスト

まずは準備として、AWSコンソールからCloudWatchを確認しましょう。

CloudWatch確認

まだログ送信していない筈なので、CloudWatchには何も表示されていないと思います。確認してみましょう。

[管理とガバナンス]から、[CloudWatch]を選択して下さい。
03_cw_1.png

[ログ]をクリックしてみましょう。まだ何もログをキャッチしていない為、ロググループが存在していません。
03_cw_2.png

ログ送信

ではログ送信を行いましょう。認証ログ(auth.log)は、sshログインでも出力されるので、この方法で試します。

もう一つのシェルを開き、EC2サーバーに接続します。

ssh -i .ssh/pri-key.pem ubuntu@ec2-xx-xx-xx-xx.ap-northeast-1.compute.amazonaws.com

ログインにすれば、認証ログ(auth.log)がCloudWatchに送信された筈です。

CloudWatch再確認

では再びCloudWatchを確認しましょう。
AWSコンソール上でログが確認できる迄、10秒~30秒ほど時間が掛かる場合があるので注意して下さい。
03_cw_3.png
ロググループに「logtest」が現れました!
「logtest」をクリックすると、ログストリーム「targetserver/var/log/auth.log」が表示されました。
03_cw_4.png

ログストリーム「targetserver/var/log/auth.log」をクリックすると、auth.logの内容をCloudWatchLogsで確認する事が出来ました。
03_cw_5.png

念のためサーバー側でauth.logを確認してみましょう。

$ tail -f /var/log/auth.log
  :
Oct 12 09:10:24 ip-172-31-39-76 sudo: pam_unix(sudo:session): session closed for user root
Oct 12 09:10:33 ip-172-31-39-76 sudo:   ubuntu : TTY=pts/1 ; PWD=/etc/default ; USER=root ; COMMAND=/bin/systemctl start td-agent
Oct 12 09:10:33 ip-172-31-39-76 sudo: pam_unix(sudo:session): session opened for user root by ubuntu(uid=0)
Oct 12 09:10:34 ip-172-31-39-76 sudo: pam_unix(sudo:session): session closed for user root
Oct 12 09:11:48 ip-172-31-39-76 sshd[20376]: Accepted publickey for ubuntu from 124.209.150.139 port 64980 ssh2: RSA SHA256:43Gtsdhxbp1l6H6AQMY2BmOVuva1TO3ex4zIZBDQW40
Oct 12 09:11:48 ip-172-31-39-76 sshd[20376]: pam_unix(sshd:session): session opened for user ubuntu by (uid=0)
Oct 12 09:11:48 ip-172-31-39-76 systemd-logind[843]: New session 49 of user ubuntu.
Oct 12 09:12:28 ip-172-31-39-76 sshd[20495]: Invalid user usuario from 49.83.149.194 port 40281
Oct 12 09:12:30 ip-172-31-39-76 sshd[20495]: error: maximum authentication attempts exceeded for invalid user usuario from 49.83.149.194 port 40281 ssh2 [preauth]
Oct 12 09:12:30 ip-172-31-39-76 sshd[20495]: Disconnecting invalid user usuario 49.83.149.194 port 40281: Too many authentication failures [preauth]
Oct 12 09:17:01 ip-172-31-39-76 CRON[20505]: pam_unix(cron:session): session opened for user root by (uid=0)
Oct 12 09:17:01 ip-172-31-39-76 CRON[20505]: pam_unix(cron:session): session closed for user root

同じログがCloudWathLogsにアップロードされている事が確認出来ました。

CloudWatchの使い道

一度CloudWatchに上げてしまえば、AWS上でアラート設定したり、Eメールでアラートを受け取ったり、溜まりすぎたログを自動的にS3に移動させたり(Lambda)と、様々な機能が簡単に使える様になって便利です。

最後に

もし不明点等あれば、なるべく分かり易く修正したいと思います。よろしくお願いします。

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

プライベートサブネット内のインスタンスにpublic IPを割り当てると外からpingは届くのか?

答え:届く

privateサブネットのルートテーブル

0.0.0.0/0へのルーティングは無い状態

image.png

EC2インスタンス

Elastic IPが割り当ててある。

image.png

セキュリティグループでICMPを許可してある。
image.png

tcpdumpで確認

インスタンス内でtcpdumpを起動しておいて、AWS外からpingしてみた。

tcpdump -nn icmp

届いていた。ただし、pingを打ったマシンまで返ってはいなかった。ルートテーブルにインターネットへのゲートウェイが無いので。

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

[Lambda][Angular] 知らないことを知りたい→Googleの検索補完を一覧にしたらいい?

動機と思いつき

  • 知らない街で美味しいご飯に巡り会いたい
  • レビューサイトを使いこなすのは難しい
  • 他のひとが何を探しているかわかったらいいかも
  • 検索語 + 「あ」〜「ん」をGoogleに渡して、なにが補完されるか一覧してみよう

さて、Google補完API

どうも公式なAPIではないようです。

Google API プログラミング解説

を参考にしました。JSONじゃなくてXMLが返ってくるんですね。

構成

Google APIはJavaScriptでたたけばいいし、Angularで作ったフロントエンドだけで完結しようと思いましたができません。
(どうしてできなかったのかは忘れました)。
間にプロキシを挟むことにしました。

image.png

稼働環境

AWSの鉄板構成です。

フロントエンドはS3に格納します。S3単体ではhttpsできないため、CloudFrnotを使います。
Google API呼び出しはLambdaで作りました。

image.png

できあがったもの

https://hokanchan.uart.dev/

g9q54-soen8.gif

ソースコード https://github.com/sengokyu/hokanchan

考察

  • 漫然と検索語を入力すると、期待する結果を得られない。
  • 狭い範囲の検索語に絞り込むと良い結果が得られる。
    • 例)「銀座 ランチ」 → 「銀座 ランチ 天ぷら」
  • 期待する結果が得られる地域と、得られない地域がある。
    • 暮らす人々がどのくらいネットを使っているかによる?

ふりかえり

Keep

  • RxJSでエラーが起きたかどうか関係なく最後に処理を挟むのであれば、pipe(finalize(()=> { /* ... */ }) を使えばいい。

Problem

  • 欲張ってCloudFormationですべてデプロイしようとして時間がかかりました。

Try

  • CloudFormationを親子関係にして、環境全体とLambdaに分ける。

その他

  • .dev ドメインはブラウザ(Chrome/Firefox)がhttpsアクセスを強制します。httpへアクセスできなくて悩みました。

リンク

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

Slack, Twitterのアイコンを定期更新して同僚・フォロワーに今の天気を通知する

僕はずっといらすとやさんのグラサン太陽アイコンを使っているのですが、その時の天気に合わせて動的にアイコンを変えたら簡易天気速報みたいになって面白いんじゃないかとふと思い、AWSとGo(特に非同期処理)の勉強にもなりそうだったので実装してみました。
スクリーンショット 2019-10-05 10.23.22.png

ざっくりとした処理の流れ

以下の処理を行うスクリプトをGoで書き、Lambda上で定期実行

使用技術

  • Go v1.12
  • AWS Lambda
  • AWS S3
  • AWS SDK for GO
  • Slack API
  • Twitter API

下準備

Lambda

AWS上で、バイナリ実行環境であるLambdaとアイコン置き場であるS3をセットアップします。LambdaはRuntimeをGoに設定し、Cloudwatch Eventsで3時間ごとにkickされるようにします。

S3

S3にはいらすとやから貰った画像を適当な名前をつけてアップロード:sunny:
スクリーンショット 2019-10-05 10.55.50.png

IAM

今回AWS SDK for goを用いてコード内でS3のオブジェクトを取得するので、IAMで有効なセッションを作成します。コンソールからユーザーを作成しAccess key IDとSecret access keyを控えておきます。

コードを書く

Goでコードを書いていきます。

気象情報APIの情報を元にS3上の画像名を取得

Open Weather Map APIにリクエストを飛ばして現在の気象情報を取得します。tokenはLambda側に環境変数としておきます。(以下SlackやTwitterのAPI tokenも同様です。)どこかで処理が失敗した場合は notifyAPIResultToSlackを呼び出してSlackに通知を送ります。(詳細は後述)

type weatherAPIResponse struct {
    Weather []struct {
        ID   int    `json:"id"`
        Main string `json:"main"`
    }
}

func fetchCurrentWeatherID() int {
    log.Println("[INFO] Start fetching current weather info from weather api")

    city := "Tokyo"
    token := os.Getenv("WEATHER_API_TOKEN")
    apiURL := "https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=" + token

    log.Printf("[INFO] Weather api token: %s", token)

    resp, _ := http.Get(apiURL)
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to read weather api response body: %s", err.Error())
    }
    defer resp.Body.Close()

    var respJSON weatherAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to unmarshal weather api response json: %s", err.Error())
    }

    log.Printf("[INFO] Current weather: %s", respJSON.Weather[0].Main)
    return respJSON.Weather[0].ID
}

APIからのレスポンスを元に画像名を選択

func getImageName() string {
    currentWeatherID := fetchCurrentWeatherID()

    var imageName string
    // ref: https://openweathermap.org/weather-conditions
    switch {
    case 200 <= currentWeatherID && currentWeatherID < 300:
        imageName = "bokuthunder"
    case currentWeatherID < 600:
        imageName = "bokurainy"
    case currentWeatherID < 700:
        imageName = "bokusnowy"
    default:
        imageName = "bokusunny"
    }

    // 夜は天気に関係なくbokumoonに上書き
    location, _ := time.LoadLocation("Asia/Tokyo")
    if h := time.Now().In(location).Hour(); h <= 5 || 22 <= h {
        imageName = "bokumoon"
    }

    return imageName
}

選択した画像名をキーとしてS3から画像データを取得

IAM作成時に控えたAccess key IDとSecret access keyでクライアントを認証し、S3から先ほど取得した画像名をキーにアイコンのオブジェクトを取得します。

func fetchS3ImageObjByName(imageName string) *s3.GetObjectOutput {
    AWSSessionID := os.Getenv("AWS_SESSION_ID")
    AWSSecretAccessKey := os.Getenv("AWS_SECRET")

    log.Println("[INFO] Start fetching image obj from S3.")
    log.Printf("[INFO] AWS session id: %s", AWSSessionID)
    log.Printf("[INFO] AWS secret access key: %s", AWSSecretAccessKey)

    sess := session.Must(session.NewSession())
    creds := credentials.NewStaticCredentials(AWSSessionID, AWSSecretAccessKey, "")

    svc := s3.New(sess, &aws.Config{
        Region:      aws.String(endpoints.ApNortheast1RegionID),
        Credentials: creds,
    })

    obj, err := svc.GetObject(&s3.GetObjectInput{
        Bucket: aws.String("bokuweather"),
        Key:    aws.String(imageName + ".png"),
    })
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to get image object: %s", err.Error())
    }

    return obj
}

Slack API/Twitter APIでアイコンをアップデート

画像をSlack API, Twitter APIに投げます。ここではgo routineを用いて非同期処理にしました。

    imgByte, err := ioutil.ReadAll(obj.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to read the image object: %s", err.Error())
    }
    defer obj.Body.Close()

    c := make(chan apiResult, 1)

    go updateSlackIcon(imgByte, c)
    go updateTwitterIcon(imgByte, c)

    result1, result2 := <-c, <-c
    if result1.StatusCode != 200 || result2.StatusCode != 200 {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Something went wrong with updateImage func.")
    }

Slack APIのドキュメントを参考にリクエストを飛ばす関数を作ります。最後に結果をチャネルを介して大元のゴールーチンに渡します。

type slackAPIResponse struct {
    Ok    bool   `json:"ok"`
    Error string `json:"error"`
}

func updateSlackIcon(imgByte []byte, c chan apiResult) {
    imgBuffer := bytes.NewBuffer(imgByte)

    reqBody := &bytes.Buffer{}
    w := multipart.NewWriter(reqBody)
    part, err := w.CreateFormFile("image", "main.go")
    if _, err := io.Copy(part, imgBuffer); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to copy the image: %s", err.Error())
    }
    w.Close()

    req, _ := http.NewRequest(
        "POST",
        // "https://httpbin.org/post", // httpテスト用
        "https://slack.com/api/users.setPhoto",
        reqBody,
    )

    token := os.Getenv("SLACK_TOKEN")
    log.Printf("[INFO] Slack token: %s", token)

    req.Header.Set("Content-type", w.FormDataContentType())
    req.Header.Set("Authorization", "Bearer "+token)

    log.Println("[INFO] Send request to update slack icon!")
    client := &http.Client{}
    resp, err := client.Do(req)

    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Something went wrong with the slack setPhoto request : %s", err.Error())
    }
    log.Printf("[INFO] SetPhoto response status: %s", resp.Status)

    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to read the response: %s", err.Error())
    }

    var respJSON slackAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to unmarshal the response: %s", err.Error())
    }

    if !respJSON.Ok {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Something went wrong with setPhoto request: %s", respJSON.Error)
    }

    c <- apiResult{"slack", resp.StatusCode}
}

Twitter APIも同様にドキュメントを参考にリクエストを飛ばす関数を作ります。OAuthは便利なライブラリがあったのでそちらを用いました。途中突然リクエストが401 Unauthorizedを返してきてハマりましたが、URL Queryの予約語をエスケープすることで解決しました。

func updateTwitterIcon(imgByte []byte, c chan apiResult) {
    oauthAPIKey := os.Getenv("OAUTH_CONSUMER_API_KEY")
    oauthAPIKeySecret := os.Getenv("OAUTH_CONSUMER_SECRET_KEY")
    oauthAccessToken := os.Getenv("OAUTH_ACCESS_TOKEN")
    oauthAccessTokenSecret := os.Getenv("OAUTH_ACCESS_TOKEN_SECRET")

    log.Printf("[INFO] Twitter API Key: %s", oauthAPIKey)
    log.Printf("[INFO] Twitter API Secret Key: %s", oauthAPIKeySecret)
    log.Printf("[INFO] Twitter Access Token: %s", oauthAccessToken)
    log.Printf("[INFO] Twitter Secret Access Token: %s", oauthAccessTokenSecret)

    config := oauth1.NewConfig(oauthAPIKey, oauthAPIKeySecret)
    token := oauth1.NewToken(oauthAccessToken, oauthAccessTokenSecret)

    httpClient := config.Client(oauth1.NoContext, token)

    encodedImg := base64.StdEncoding.EncodeToString(imgByte)
    encodedImg = url.QueryEscape(encodedImg) // replace URL encoding reserved characters
    log.Printf("Encoded icon: %s", encodedImg)

    twitterAPIRootURL := "https://api.twitter.com"
    twitterAPIMethod := "/1.1/account/update_profile_image.json"
    URLParams := "?image=" + encodedImg

    req, _ := http.NewRequest(
        "POST",
        twitterAPIRootURL+twitterAPIMethod+URLParams,
        nil,
    )

    log.Println("[INFO] Send request to update twitter icon!")
    resp, err := httpClient.Do(req)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Something went wrong with the twitter request : %s", err.Error())
    }
    defer resp.Body.Close()

    log.Printf("[INFO] Twitter updateImage response status: %s", resp.Status)

    c <- apiResult{"twitter", resp.StatusCode}
}

結果の通知

両方のAPIからレスポンスが帰って来次第Slackの自分宛DMに結果を送信します。ここでは chat.postMessageを使います。

func notifyAPIResultToSlack(isSuccess bool) slackAPIResponse {
    channel := os.Getenv("SLACK_NOTIFY_CHANNEL_ID")
    attatchmentsColor := "good"
    imageName := getImageName()
    attatchmentsText := "Icon updated successfully according to the current weather! :" + imageName + ":"
    iconEmoji := ":bokurainy:"
    username := "bokuweather"

    if !isSuccess {
        lambdaCloudWatchURL := "https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logStream:group=/aws/lambda/bokuweather;streamFilter=typeLogStreamPrefix"
        attatchmentsColor = "danger"
        attatchmentsText = "Bokuweather has some problems and needs your help!:bokuthunder:\nWatch logs: " + lambdaCloudWatchURL
    }

    jsonStr := `{"channel":"` + channel + `","as_user":false,"attachments":[{"color":"` + attatchmentsColor + `","text":"` + attatchmentsText + `"}],"icon_emoji":"` + iconEmoji + `","username":"` + username + `"}`
    req, _ := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", bytes.NewBuffer([]byte(jsonStr)))

    req.Header.Set("Authorization", "Bearer "+os.Getenv("SLACK_TOKEN"))
    req.Header.Set("Content-Type", "application/json")

    log.Printf("[INFO] Send request to notify outcome. isSuccess?: %t", isSuccess)
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatalf("[Error] Something went wrong with the postMessage reqest : %s", err.Error())
    }
    log.Printf("[INFO] PostMessage response states: %s", resp.Status)

    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("[Error] Fail to read the response: %s", err.Error())
    }

    var respJSON slackAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        log.Fatalf("[Error] Fail to unmarshal the response: %s", err.Error())
    }

    return respJSON
}

成功した場合どの画像にアップデートされたかを、失敗した場合Cloudwatch Logsへのリンクを通知します。
スクリーンショット 2019-10-05 12.52.07.png

コードのアップロード

$ GOOS=linux go build main.go
$ zip main.zip main # これでzipされたファイルをLambdaにアップロード

感想

AWSとGo(特に非同期処理周り)の入門としてこのアプリを作ってみようと思ったのですが、実際に何かを作りながら勉強するとやっぱり楽しいし書籍とにらめっこするより効率がいいと思いました。
また、各種APIやSDKなど本来意図していなかった分野の技術についても理解が深まって有意義でした。
まだまだ初心者なのでおかしい部分などあれば指摘大歓迎です!

ソースコード

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

Slack・Twitterのアイコンを定期更新して同僚・フォロワーに今の天気を通知する

僕はずっといらすとやさんのグラサン太陽アイコンを使っているのですが、その時の天気に合わせて動的にアイコンを変えたら簡易天気速報みたいになって面白いんじゃないかとふと思い、AWSとGo(特に非同期処理)の勉強にもなりそうだったので実装してみました。
スクリーンショット 2019-10-05 10.23.22.png

ざっくりとした処理の流れ

以下の処理を行うスクリプトをGoで書き、Lambda上で定期実行

使用技術

  • Go v1.12
  • AWS Lambda
  • AWS S3
  • AWS SDK for GO
  • Slack API
  • Twitter API

下準備

Lambda

AWS上で、バイナリ実行環境であるLambdaとアイコン置き場であるS3をセットアップします。LambdaはRuntimeをGoに設定し、Cloudwatch Eventsで3時間ごとにkickされるようにします。

S3

S3にはいらすとやから貰った画像を適当な名前をつけてアップロード:sunny:
スクリーンショット 2019-10-05 10.55.50.png

IAM

今回AWS SDK for goを用いてコード内でS3のオブジェクトを取得するので、IAMで有効なセッションを作成します。コンソールからユーザーを作成しAccess key IDとSecret access keyを控えておきます。

コードを書く

Goでコードを書いていきます。

気象情報APIの情報を元にS3上の画像名を取得

Open Weather Map APIにリクエストを飛ばして現在の気象情報を取得します。tokenはLambda側に環境変数としておきます。(以下SlackやTwitterのAPI tokenも同様です。)どこかで処理が失敗した場合は notifyAPIResultToSlackを呼び出してSlackに通知を送ります。(詳細は後述)

type weatherAPIResponse struct {
    Weather []struct {
        ID   int    `json:"id"`
        Main string `json:"main"`
    }
}

func fetchCurrentWeatherID() int {
    log.Println("[INFO] Start fetching current weather info from weather api")

    city := "Tokyo"
    token := os.Getenv("WEATHER_API_TOKEN")
    apiURL := "https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=" + token

    log.Printf("[INFO] Weather api token: %s", token)

    resp, _ := http.Get(apiURL)
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to read weather api response body: %s", err.Error())
    }
    defer resp.Body.Close()

    var respJSON weatherAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to unmarshal weather api response json: %s", err.Error())
    }

    log.Printf("[INFO] Current weather: %s", respJSON.Weather[0].Main)
    return respJSON.Weather[0].ID
}

APIからのレスポンスを元に画像名を選択

func getImageName() string {
    currentWeatherID := fetchCurrentWeatherID()

    var imageName string
    // ref: https://openweathermap.org/weather-conditions
    switch {
    case 200 <= currentWeatherID && currentWeatherID < 300:
        imageName = "bokuthunder"
    case currentWeatherID < 600:
        imageName = "bokurainy"
    case currentWeatherID < 700:
        imageName = "bokusnowy"
    default:
        imageName = "bokusunny"
    }

    // 夜は天気に関係なくbokumoonに上書き
    location, _ := time.LoadLocation("Asia/Tokyo")
    if h := time.Now().In(location).Hour(); h <= 5 || 22 <= h {
        imageName = "bokumoon"
    }

    return imageName
}

選択した画像名をキーとしてS3から画像データを取得

IAM作成時に控えたAccess key IDとSecret access keyでクライアントを認証し、S3から先ほど取得した画像名をキーにアイコンのオブジェクトを取得します。

func fetchS3ImageObjByName(imageName string) *s3.GetObjectOutput {
    AWSSessionID := os.Getenv("AWS_SESSION_ID")
    AWSSecretAccessKey := os.Getenv("AWS_SECRET")

    log.Println("[INFO] Start fetching image obj from S3.")
    log.Printf("[INFO] AWS session id: %s", AWSSessionID)
    log.Printf("[INFO] AWS secret access key: %s", AWSSecretAccessKey)

    sess := session.Must(session.NewSession())
    creds := credentials.NewStaticCredentials(AWSSessionID, AWSSecretAccessKey, "")

    svc := s3.New(sess, &aws.Config{
        Region:      aws.String(endpoints.ApNortheast1RegionID),
        Credentials: creds,
    })

    obj, err := svc.GetObject(&s3.GetObjectInput{
        Bucket: aws.String("bokuweather"),
        Key:    aws.String(imageName + ".png"),
    })
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to get image object: %s", err.Error())
    }

    return obj
}

Slack API/Twitter APIでアイコンをアップデート

画像をSlack API, Twitter APIに投げます。ここではgo routineを用いて非同期処理にしました。

    imgByte, err := ioutil.ReadAll(obj.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to read the image object: %s", err.Error())
    }
    defer obj.Body.Close()

    c := make(chan apiResult, 1)

    go updateSlackIcon(imgByte, c)
    go updateTwitterIcon(imgByte, c)

    result1, result2 := <-c, <-c
    if result1.StatusCode != 200 || result2.StatusCode != 200 {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Something went wrong with updateImage func.")
    }

Slack APIのドキュメントを参考にリクエストを飛ばす関数を作ります。最後に結果をチャネルを介して大元のゴールーチンに渡します。

type slackAPIResponse struct {
    Ok    bool   `json:"ok"`
    Error string `json:"error"`
}

func updateSlackIcon(imgByte []byte, c chan apiResult) {
    imgBuffer := bytes.NewBuffer(imgByte)

    reqBody := &bytes.Buffer{}
    w := multipart.NewWriter(reqBody)
    part, err := w.CreateFormFile("image", "main.go")
    if _, err := io.Copy(part, imgBuffer); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to copy the image: %s", err.Error())
    }
    w.Close()

    req, _ := http.NewRequest(
        "POST",
        // "https://httpbin.org/post", // httpテスト用
        "https://slack.com/api/users.setPhoto",
        reqBody,
    )

    token := os.Getenv("SLACK_TOKEN")
    log.Printf("[INFO] Slack token: %s", token)

    req.Header.Set("Content-type", w.FormDataContentType())
    req.Header.Set("Authorization", "Bearer "+token)

    log.Println("[INFO] Send request to update slack icon!")
    client := &http.Client{}
    resp, err := client.Do(req)

    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Something went wrong with the slack setPhoto request : %s", err.Error())
    }
    log.Printf("[INFO] SetPhoto response status: %s", resp.Status)

    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to read the response: %s", err.Error())
    }

    var respJSON slackAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to unmarshal the response: %s", err.Error())
    }

    if !respJSON.Ok {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Something went wrong with setPhoto request: %s", respJSON.Error)
    }

    c <- apiResult{"slack", resp.StatusCode}
}

Twitter APIも同様にドキュメントを参考にリクエストを飛ばす関数を作ります。OAuthは便利なライブラリがあったのでそちらを用いました。途中突然リクエストが401 Unauthorizedを返してきてハマりましたが、URL Queryの予約語をエスケープすることで解決しました。

func updateTwitterIcon(imgByte []byte, c chan apiResult) {
    oauthAPIKey := os.Getenv("OAUTH_CONSUMER_API_KEY")
    oauthAPIKeySecret := os.Getenv("OAUTH_CONSUMER_SECRET_KEY")
    oauthAccessToken := os.Getenv("OAUTH_ACCESS_TOKEN")
    oauthAccessTokenSecret := os.Getenv("OAUTH_ACCESS_TOKEN_SECRET")

    log.Printf("[INFO] Twitter API Key: %s", oauthAPIKey)
    log.Printf("[INFO] Twitter API Secret Key: %s", oauthAPIKeySecret)
    log.Printf("[INFO] Twitter Access Token: %s", oauthAccessToken)
    log.Printf("[INFO] Twitter Secret Access Token: %s", oauthAccessTokenSecret)

    config := oauth1.NewConfig(oauthAPIKey, oauthAPIKeySecret)
    token := oauth1.NewToken(oauthAccessToken, oauthAccessTokenSecret)

    httpClient := config.Client(oauth1.NoContext, token)

    encodedImg := base64.StdEncoding.EncodeToString(imgByte)
    encodedImg = url.QueryEscape(encodedImg) // replace URL encoding reserved characters
    log.Printf("Encoded icon: %s", encodedImg)

    twitterAPIRootURL := "https://api.twitter.com"
    twitterAPIMethod := "/1.1/account/update_profile_image.json"
    URLParams := "?image=" + encodedImg

    req, _ := http.NewRequest(
        "POST",
        twitterAPIRootURL+twitterAPIMethod+URLParams,
        nil,
    )

    log.Println("[INFO] Send request to update twitter icon!")
    resp, err := httpClient.Do(req)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Something went wrong with the twitter request : %s", err.Error())
    }
    defer resp.Body.Close()

    log.Printf("[INFO] Twitter updateImage response status: %s", resp.Status)

    c <- apiResult{"twitter", resp.StatusCode}
}

結果の通知

両方のAPIからレスポンスが帰って来次第Slackの自分宛DMに結果を送信します。ここでは chat.postMessageを使います。

func notifyAPIResultToSlack(isSuccess bool) slackAPIResponse {
    channel := os.Getenv("SLACK_NOTIFY_CHANNEL_ID")
    attatchmentsColor := "good"
    imageName := getImageName()
    attatchmentsText := "Icon updated successfully according to the current weather! :" + imageName + ":"
    iconEmoji := ":bokurainy:"
    username := "bokuweather"

    if !isSuccess {
        lambdaCloudWatchURL := "https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logStream:group=/aws/lambda/bokuweather;streamFilter=typeLogStreamPrefix"
        attatchmentsColor = "danger"
        attatchmentsText = "Bokuweather has some problems and needs your help!:bokuthunder:\nWatch logs: " + lambdaCloudWatchURL
    }

    jsonStr := `{"channel":"` + channel + `","as_user":false,"attachments":[{"color":"` + attatchmentsColor + `","text":"` + attatchmentsText + `"}],"icon_emoji":"` + iconEmoji + `","username":"` + username + `"}`
    req, _ := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", bytes.NewBuffer([]byte(jsonStr)))

    req.Header.Set("Authorization", "Bearer "+os.Getenv("SLACK_TOKEN"))
    req.Header.Set("Content-Type", "application/json")

    log.Printf("[INFO] Send request to notify outcome. isSuccess?: %t", isSuccess)
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatalf("[Error] Something went wrong with the postMessage reqest : %s", err.Error())
    }
    log.Printf("[INFO] PostMessage response states: %s", resp.Status)

    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("[Error] Fail to read the response: %s", err.Error())
    }

    var respJSON slackAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        log.Fatalf("[Error] Fail to unmarshal the response: %s", err.Error())
    }

    return respJSON
}

成功した場合どの画像にアップデートされたかを、失敗した場合Cloudwatch Logsへのリンクを通知します。
スクリーンショット 2019-10-05 12.52.07.png

コードのアップロード

$ GOOS=linux go build main.go
$ zip main.zip main # これでzipされたファイルをLambdaにアップロード

感想

AWSとGo(特に非同期処理周り)の入門としてこのアプリを作ってみようと思ったのですが、実際に何かを作りながら勉強するとやっぱり楽しいし書籍とにらめっこするより効率がいいと思いました。
また、各種APIやSDKなど本来意図していなかった分野の技術についても理解が深まって有意義でした。
まだまだ初心者なのでおかしい部分などあれば指摘大歓迎です!

ソースコード

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

Slack, Twitterのアイコンを定期更新して同僚とフォロワーに今の天気を通知する

僕はずっといらすとやさんのグラサン太陽アイコンを使っているのですが、その時の天気に合わせて動的にアイコンを変えたら簡易天気速報みたいになって面白いんじゃないかとふと思い、AWSとGo(特に非同期処理)の勉強にもなりそうだったので実装してみました。
スクリーンショット 2019-10-05 10.23.22.png

ざっくりとした処理の流れ

以下の処理を行うスクリプトをGoで書き、Lambda上で定期実行

使用技術

  • Go v1.12
  • AWS Lambda
  • AWS S3
  • AWS SDK for GO
  • Slack API
  • Twitter API

下準備

Lambda

AWS上で、バイナリ実行環境であるLambdaとアイコン置き場であるS3をセットアップします。LambdaはRuntimeをGoに設定し、Cloudwatch Eventsで3時間ごとにkickされるようにします。

S3

S3にはいらすとやから貰った画像を適当な名前をつけてアップロード:sunny:
スクリーンショット 2019-10-05 10.55.50.png

IAM

今回AWS SDK for goを用いてコード内でS3のオブジェクトを取得するので、IAMで有効なセッションを作成します。コンソールからユーザーを作成しAccess key IDとSecret access keyを控えておきます。

コードを書く

Goでコードを書いていきます。

気象情報APIの情報を元にS3上の画像名を取得

Open Weather Map APIにリクエストを飛ばして現在の気象情報を取得します。tokenはLambda側に環境変数としておきます。(以下SlackやTwitterのAPI tokenも同様です。)どこかで処理が失敗した場合は notifyAPIResultToSlackを呼び出してSlackに通知を送ります。(詳細は後述)

type weatherAPIResponse struct {
    Weather []struct {
        ID   int    `json:"id"`
        Main string `json:"main"`
    }
}

func fetchCurrentWeatherID() int {
    log.Println("[INFO] Start fetching current weather info from weather api")

    city := "Tokyo"
    token := os.Getenv("WEATHER_API_TOKEN")
    apiURL := "https://api.openweathermap.org/data/2.5/weather?q=" + city + "&appid=" + token

    log.Printf("[INFO] Weather api token: %s", token)

    resp, _ := http.Get(apiURL)
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to read weather api response body: %s", err.Error())
    }
    defer resp.Body.Close()

    var respJSON weatherAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to unmarshal weather api response json: %s", err.Error())
    }

    log.Printf("[INFO] Current weather: %s", respJSON.Weather[0].Main)
    return respJSON.Weather[0].ID
}

APIからのレスポンスを元に画像名を選択

func getImageName() string {
    currentWeatherID := fetchCurrentWeatherID()

    var imageName string
    // ref: https://openweathermap.org/weather-conditions
    switch {
    case 200 <= currentWeatherID && currentWeatherID < 300:
        imageName = "bokuthunder"
    case currentWeatherID < 600:
        imageName = "bokurainy"
    case currentWeatherID < 700:
        imageName = "bokusnowy"
    default:
        imageName = "bokusunny"
    }

    // 夜は天気に関係なくbokumoonに上書き
    location, _ := time.LoadLocation("Asia/Tokyo")
    if h := time.Now().In(location).Hour(); h <= 5 || 22 <= h {
        imageName = "bokumoon"
    }

    return imageName
}

選択した画像名をキーとしてS3から画像データを取得

IAM作成時に控えたAccess key IDとSecret access keyでクライアントを認証し、S3から先ほど取得した画像名をキーにアイコンのオブジェクトを取得します。

func fetchS3ImageObjByName(imageName string) *s3.GetObjectOutput {
    AWSSessionID := os.Getenv("AWS_SESSION_ID")
    AWSSecretAccessKey := os.Getenv("AWS_SECRET")

    log.Println("[INFO] Start fetching image obj from S3.")
    log.Printf("[INFO] AWS session id: %s", AWSSessionID)
    log.Printf("[INFO] AWS secret access key: %s", AWSSecretAccessKey)

    sess := session.Must(session.NewSession())
    creds := credentials.NewStaticCredentials(AWSSessionID, AWSSecretAccessKey, "")

    svc := s3.New(sess, &aws.Config{
        Region:      aws.String(endpoints.ApNortheast1RegionID),
        Credentials: creds,
    })

    obj, err := svc.GetObject(&s3.GetObjectInput{
        Bucket: aws.String("bokuweather"),
        Key:    aws.String(imageName + ".png"),
    })
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Fail to get image object: %s", err.Error())
    }

    return obj
}

Slack API/Twitter APIでアイコンをアップデート

画像をSlack API, Twitter APIに投げます。ここではgo routineを用いて非同期処理にしました。

    imgByte, err := ioutil.ReadAll(obj.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to read the image object: %s", err.Error())
    }
    defer obj.Body.Close()

    c := make(chan apiResult, 1)

    go updateSlackIcon(imgByte, c)
    go updateTwitterIcon(imgByte, c)

    result1, result2 := <-c, <-c
    if result1.StatusCode != 200 || result2.StatusCode != 200 {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Something went wrong with updateImage func.")
    }

Slack APIのドキュメントを参考にリクエストを飛ばす関数を作ります。最後に結果をチャネルを介して大元のゴールーチンに渡します。

type slackAPIResponse struct {
    Ok    bool   `json:"ok"`
    Error string `json:"error"`
}

func updateSlackIcon(imgByte []byte, c chan apiResult) {
    imgBuffer := bytes.NewBuffer(imgByte)

    reqBody := &bytes.Buffer{}
    w := multipart.NewWriter(reqBody)
    part, err := w.CreateFormFile("image", "main.go")
    if _, err := io.Copy(part, imgBuffer); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to copy the image: %s", err.Error())
    }
    w.Close()

    req, _ := http.NewRequest(
        "POST",
        // "https://httpbin.org/post", // httpテスト用
        "https://slack.com/api/users.setPhoto",
        reqBody,
    )

    token := os.Getenv("SLACK_TOKEN")
    log.Printf("[INFO] Slack token: %s", token)

    req.Header.Set("Content-type", w.FormDataContentType())
    req.Header.Set("Authorization", "Bearer "+token)

    log.Println("[INFO] Send request to update slack icon!")
    client := &http.Client{}
    resp, err := client.Do(req)

    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Something went wrong with the slack setPhoto request : %s", err.Error())
    }
    log.Printf("[INFO] SetPhoto response status: %s", resp.Status)

    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to read the response: %s", err.Error())
    }

    var respJSON slackAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Fail to unmarshal the response: %s", err.Error())
    }

    if !respJSON.Ok {
        notifyAPIResultToSlack(false)
        log.Fatalf("[ERROR] Something went wrong with setPhoto request: %s", respJSON.Error)
    }

    c <- apiResult{"slack", resp.StatusCode}
}

Twitter APIも同様にドキュメントを参考にリクエストを飛ばす関数を作ります。OAuthは便利なライブラリがあったのでそちらを用いました。途中突然リクエストが401 Unauthorizedを返してきてハマりましたが、URL Queryの予約語をエスケープすることで解決しました。

func updateTwitterIcon(imgByte []byte, c chan apiResult) {
    oauthAPIKey := os.Getenv("OAUTH_CONSUMER_API_KEY")
    oauthAPIKeySecret := os.Getenv("OAUTH_CONSUMER_SECRET_KEY")
    oauthAccessToken := os.Getenv("OAUTH_ACCESS_TOKEN")
    oauthAccessTokenSecret := os.Getenv("OAUTH_ACCESS_TOKEN_SECRET")

    log.Printf("[INFO] Twitter API Key: %s", oauthAPIKey)
    log.Printf("[INFO] Twitter API Secret Key: %s", oauthAPIKeySecret)
    log.Printf("[INFO] Twitter Access Token: %s", oauthAccessToken)
    log.Printf("[INFO] Twitter Secret Access Token: %s", oauthAccessTokenSecret)

    config := oauth1.NewConfig(oauthAPIKey, oauthAPIKeySecret)
    token := oauth1.NewToken(oauthAccessToken, oauthAccessTokenSecret)

    httpClient := config.Client(oauth1.NoContext, token)

    encodedImg := base64.StdEncoding.EncodeToString(imgByte)
    encodedImg = url.QueryEscape(encodedImg) // replace URL encoding reserved characters
    log.Printf("Encoded icon: %s", encodedImg)

    twitterAPIRootURL := "https://api.twitter.com"
    twitterAPIMethod := "/1.1/account/update_profile_image.json"
    URLParams := "?image=" + encodedImg

    req, _ := http.NewRequest(
        "POST",
        twitterAPIRootURL+twitterAPIMethod+URLParams,
        nil,
    )

    log.Println("[INFO] Send request to update twitter icon!")
    resp, err := httpClient.Do(req)
    if err != nil {
        notifyAPIResultToSlack(false)
        log.Fatalf("[Error] Something went wrong with the twitter request : %s", err.Error())
    }
    defer resp.Body.Close()

    log.Printf("[INFO] Twitter updateImage response status: %s", resp.Status)

    c <- apiResult{"twitter", resp.StatusCode}
}

結果の通知

両方のAPIからレスポンスが帰って来次第Slackの自分宛DMに結果を送信します。ここでは chat.postMessageを使います。

func notifyAPIResultToSlack(isSuccess bool) slackAPIResponse {
    channel := os.Getenv("SLACK_NOTIFY_CHANNEL_ID")
    attatchmentsColor := "good"
    imageName := getImageName()
    attatchmentsText := "Icon updated successfully according to the current weather! :" + imageName + ":"
    iconEmoji := ":bokurainy:"
    username := "bokuweather"

    if !isSuccess {
        lambdaCloudWatchURL := "https://ap-northeast-1.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-1#logStream:group=/aws/lambda/bokuweather;streamFilter=typeLogStreamPrefix"
        attatchmentsColor = "danger"
        attatchmentsText = "Bokuweather has some problems and needs your help!:bokuthunder:\nWatch logs: " + lambdaCloudWatchURL
    }

    jsonStr := `{"channel":"` + channel + `","as_user":false,"attachments":[{"color":"` + attatchmentsColor + `","text":"` + attatchmentsText + `"}],"icon_emoji":"` + iconEmoji + `","username":"` + username + `"}`
    req, _ := http.NewRequest("POST", "https://slack.com/api/chat.postMessage", bytes.NewBuffer([]byte(jsonStr)))

    req.Header.Set("Authorization", "Bearer "+os.Getenv("SLACK_TOKEN"))
    req.Header.Set("Content-Type", "application/json")

    log.Printf("[INFO] Send request to notify outcome. isSuccess?: %t", isSuccess)
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatalf("[Error] Something went wrong with the postMessage reqest : %s", err.Error())
    }
    log.Printf("[INFO] PostMessage response states: %s", resp.Status)

    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("[Error] Fail to read the response: %s", err.Error())
    }

    var respJSON slackAPIResponse
    if err = json.Unmarshal(body, &respJSON); err != nil {
        log.Fatalf("[Error] Fail to unmarshal the response: %s", err.Error())
    }

    return respJSON
}

成功した場合どの画像にアップデートされたかを、失敗した場合Cloudwatch Logsへのリンクを通知します。
スクリーンショット 2019-10-05 12.52.07.png

コードのアップロード

$ GOOS=linux go build main.go
$ zip main.zip main # これでzipされたファイルをLambdaにアップロード

感想

AWSとGo(特に非同期処理周り)の入門としてこのアプリを作ってみようと思ったのですが、実際に何かを作りながら勉強するとやっぱり楽しいし書籍とにらめっこするより効率がいいと思いました。
また、各種APIやSDKなど本来意図していなかった分野の技術についても理解が深まって有意義でした。
まだまだ初心者なのでおかしい部分などあれば指摘大歓迎です!

ソースコード

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

DockerやECR, ECS, Fargateなど、コンテナ周りのAWS知識を効率的にキャッチアップしたい人のために

概要

私自身がコンテナや、コンテナ関連のAWSサービスについてはほぼ分からない状態だったのですが、そこからできる限り効率的に知識をキャッチアップしたくて学習したときの道のりです。

同じように困っていらっしゃる方のお役に立てばと思い、記事にしてみました。この道のりの通りに進んでいただければ、時間を無駄にすることなく、多少なりともスムーズに知識をキャッチアップできると思います。

主要な概念や全体像を理解するまでの道のり

いきなり詳細に踏み込んでも、つまりいきなりFargateなどのAWSサービスを使っても、すぐに迷子になることは目に見えていましたので、まずは全体感や重要な概念、用語を理解しようと思いました。

そこで色々と調べていると、次の記事を見つけました。ものすごく分かりやすかったです。

これを読んでよく分かったのは、以下の点です。

  • レジストリとは、Docker Hubのようなもので、Dockerイメージを管理するところ。AWSだとECRがこれにあたる。
  • それとは別次元で、以下がある。
    • コントロールプレーン
      • コンテナ数の管理など、コンテナの管理をするもの。データプレーンを起動する役割したりする。ECSがこれにあたる。
    • データプレーン
      • 実際にコンテナが動く場所で、EC2やFargateがこれにあたる。

ただ、上記の記事を読んでも、ECSのクラスター、サービス、タスク、コンテナの概念はよく分かりませんでした。うーむ、難しい・・。

また、それらとデータプレーン(EC2, Fargate)との違いについても、ほぼ理解できませんでした。

こういったトピックについては、以下の記事がとても分かりやすいです。

DockerコンテナをFargateで動かすことの素晴らしさ

こうやって調査をしていきますと、Fargateというサービスの何が素晴らしいのか、という点が見えてきました。

DockerコンテナをFargateで動かすということは

  • PaaS(Elastic Beanstalkなど)よりも自由度が高くなり
  • IaaS(EC2など)よりも生産性が高くなる(DockerのインストールやOSセキュリティパッチの適用などから解放される)

ということです。一言でいうと「良い塩梅でめっちゃいい」ということですね。

EC2(つまりIaaS)でコンテナを動かすことはできます。しかし、それにはEC2インスタンスを構築し、Dockerという基盤をインストールする必要があります。そしてOSセキュリティパッチなどの面倒を見続けなければなりません。それは、ものすごく大変なことです。

そのあたりの大変さをAWSが引き受けてくれる、というのがFargateの素晴らしいところです。私たちは、Dockerイメージさえつくれば良いのです。

こういった点から、色々な制約はもちろんあるものの、マネージドの恩恵を最大限に受けるようにした方が得策だと感じてきました。

Dockerfileとdocker-composeの関係

Dockerについてまだ慣れていない私は、この二つの関係がよく分からず混乱していました。

この点については、以下の記事で分かりやすく解説されています。

実際に動かしてみるまでの道のり

主要な概念や全体像を理解できましたので、今度は実際にECR, ECS, Fargateを使って、Dockerイメージを動かしてみることにしました。

私の場合、自作したアプリを動かしてみたかった関係で、Spring Bootが動くコンテナをつくってみようと思いました。

その際、以下の記事を参考にさせていただきました。

こちらの記事を参考に、以下のDockerfileを、Mavenプロジェクト直下に作成しました。

Dockerfile
FROM openjdk:8-jdk-alpine

COPY target/my-app-0.0.1-SNAPSHOT.jar my-app.jar

ENTRYPOINT ["java","-jar","/my-app.jar"]

そして、Dockerイメージをつくります。

docker build ./ -t my-app

tオプションは作成したイメージにどんな名前をつけるか、を指定するものです。

ところが、このイメージの名前というものが意外に分かりづらいのです。ここに指定したものは、何を意味するのか、何に影響してくるのかが、分かりません。

tオプションについては、以下の公式リファレンスで解説されています。

しかし、これらを読んでもイマイチよく分かりません。他の記事を調べても「なるほど!そういうことか!」という思いに至ることができませんでした。

公式リファレンスや先人の解説記事の内容をつなぎ合わせると、イメージ名は以下のものだと分かってきました。

「イメージ名」とは?

イメージ名というのは、全世界のなかでイメージを一意に特定するためのIDみたいなものです。
では、どうやって一意に特定するのかというと

  • どのレジストリで管理されているか?
    • レジストリ、というのはDockerHubのようにDockerのイメージを管理するところです。
    • 平たく言うと、DockerHubなのか?ECRなのか?全く別のホストなのか?、ということです。
    • ですから、ここにはホスト名やIPアドレス、およびポート名が記載されることになります。
  • そのレジストリの中の、なんというリポジトリなのか?
    • リポジトリというのは、レジストリの中にあるもので、Githubのリポジトリのようなものです。
    • DockerHubではユーザー名によって名前空間が区切られていますから、ここが[ユーザー名]/[リポジトリ名]という構造になります。一方、そのようにユーザー名で区切られていない場合は、シンプルに[リポジトリ名](スラッシュで区切られない)という感じです。
  • そのリポジトリの中の、どのタグか。
    • これもGithubのタグと同じようなものです。1.0とかバージョン番号にするのが典型ですね。
    • docker build -tのtオプションでタグ名を省略すると、latestがデフォルトで付きます。

イメージ名は以上の3要素から構成され、具体的には以下の構造です。

レジストリ名(多くの場合ホスト名) / リポジトリ名(DockerHubの場合は、「ユーザー名/リポジトリ名」) : タグ名

では、先ほどように

docker build ./ -t my-app

と指定した場合はどうなるのでしょうか?

ホスト名が省略されていますので、公式リファレンスの

ホストの指定が無ければ、デフォルトで Docker の公開レジストリのある registry-1.docker.io を使います。

の記述に基づいて、registry-1.docker.ioであると見なされます(つまりDockerHubだと見なされます)。

タグ名が省略されていますので、latestがデフォルトで指定されます。

つまり、以下で指定したのと同じ意味になる、ということだと私は解釈しています。

registry-1.docker.io/my-app:latest

作成したDockerイメージを、ECRに登録します

ローカルPCで先ほどつくったDockerイメージを、ECRに登録していきます。

この手順については公式ガイドで解説されています。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/docker-basics.html#use-ecr

以下、この公式ガイドの手順どおりに実行したことについて、解説してきます。

ECRでは、レジストリはユーザー単位に作成されます。ここはDockerHubとは異なりますね。DockerHubは全体で一つのレジストリで、その中の名前空間をユーザー名で分割してる感じです。

ですので、どのレジストリにアクセスするかは、aws cliでログインしているユーザに紐づくレジストリとなります。このため、コマンドにはレジストリを指定する内容が出てきません。

まずは、レジストリという自分専用の部屋に、今回のDockerイメージを格納するための空間(リポジトリ)を作成します。

aws ecr create-repository --repository-name my-app --region ap-northeast-1

すると、以下の結果がかえってきます。

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:{アカウントID}:repository/my-app",
        "registryId": "{アカウントID}",
        "repositoryName": "my-app",
        "repositoryUri": "{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app",
        "createdAt": 1570824304.0
    }
}

次に、リポジトリーに登録したDockerイメージに、ECR用のイメージ名(tag)をつけます。

docker tag my-app {アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app

ここでついに、イメージ名にホスト名を明示的に追加したわけです。

  • {アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.comというECR上の私専用の部屋(レジストリにある、
  • my-appというリポジトリに配置するイメージだよ

ということを、イメージ名が表しています。なお、タグについては省略しましたので、自動的に「latest」が付与されます。

次に、レジストリ(ECR)にログインする必要があるのですが、「ログインするためのコマンド」を取得するためのコマンドを叩かねばなりません。

aws ecr get-login --no-include-email --region ap-northeast-1

すると、以下のコマンドを実行するよう、指示されます。

docker login -u AWS -p {ものすごく長い文字列。12 時間有効な認証トークン、つまりパスワード。} https://{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com

このコマンドを実行してECRにログインします。

そして、さきほどつけたタグ名を指定して、ECRにイメージを登録します。

docker push {アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app
The push refers to repository [{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app]
ffb5690b4e55: Preparing 
ceaf9e1ebef5: Preparing 
9b9b7f3d56a0: Preparing 
f1b5933fe4b5: Preparing 
no basic auth credentials

このようにno basic auth credentialsが出てしまった場合は、以下を参考に対応しましょう。
https://qiita.com/NaokiIshimura/items/1886dbd04631c3f7d0e1

うまくいくと・・・

The push refers to repository [{アカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/my-app]
ffb5690b4e55: Pushed 
ceaf9e1ebef5: Pushed 
9b9b7f3d56a0: Pushed 
f1b5933fe4b5: Pushed 
latest: digest: sha256:{ハッシュ値}
size: 1160

のようになります。

管理コンソールから見ると・・・

スクリーンショット 2019-10-12 5.30.51.png
スクリーンショット 2019-10-12 5.31.03.png

しっかり登録されています!よかった!

ECRに登録したDockerイメージを、ECSを使ってFargateの上で動かします。

次は、このECRに登録したdockerイメージを、Fargateの上で動かしてみましょう。Fargateで動くDockerコンテナの管理は、ECSで行います。

この手順については、公式ガイドで解説されています。
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_GetStarted_Fargate.html

ガイドに記載のとおり、以下ウィザードを使ってECSおよびFargateをセットアップできます。
https://console.aws.amazon.com/ecs/home#/firstRun

私が分かりづらかった点を、以下に記載します。

ポートマッピング

ポートマッピングをSpring Boot(のtomcat)が待ち受けているポート番号に指定します。たとえば、Tomcatが5000番ポートをリッスンする設定にしているならば、5000を指定する必要があります。Fargateをつかう場合、前面で待ち受けるポートと、コンテナ側のポート番号は同じじゃないとダメみたいです。それはもはや「マッピング」ではないと思いますが・・。

セキュリティグループのインバウンドルール

ポートマッピングでFargateが待ち受けるポート番号を、Tomcatが待ち受けるポート番号と同じにしないといけないので、そのポート番号でFargateへ流入してくるトラフィックを許可しないといけません。

Fargateから他のAWSサービスを呼び出す場合は、権限設定が必要

権限がない状態でAWSサービスを呼び出すと、エラーが起こります。今回のアプリの場合、Fargateで動くSpring Bootアプリから、DynamoDBを呼び出していました。

ECSの管理コンソールからログが見れるのですが、そこでログを見て見ると・・・

Caused by: com.amazonaws.SdkClientException: Unable to load AWS credentials from any provider in the chain: [EnvironmentVariableCredentialsProvider: Unable to load AWS credentials from environment variables (AWS_ACCESS_KEY_ID (or AWS_ACCESS_KEY) and AWS_SECRET_KEY (or AWS_SECRET_ACCESS_KEY)), SystemPropertiesCredentialsProvider: Unable to load AWS credentials from Java system properties (aws.accessKeyId and aws.secretKey), com.amazonaws.auth.profile.ProfileCredentialsProvider@46393853: profile file cannot be null, com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper@53f52e4e: Unable to load credentials from service endpoint]

というエラーが出ていました。

↓整形すると・・・

Caused by: com.amazonaws.SdkClientException: 
Unable to load AWS credentials from any provider in the chain: 

[
EnvironmentVariableCredentialsProvider: 
 Unable to load AWS credentials from environment variables
 (AWS_ACCESS_KEY_ID (or AWS_ACCESS_KEY) and
  AWS_SECRET_KEY (or AWS_SECRET_ACCESS_KEY)), 

SystemPropertiesCredentialsProvider: 
 Unable to load AWS credentials from Java system properties
 (aws.accessKeyId and aws.secretKey),

com.amazonaws.auth.profile.ProfileCredentialsProvider@46393853:
 profile file cannot be null, 

com.amazonaws.auth.EC2ContainerCredentialsProviderWrapper@53f52e4e: 
 Unable to load credentials from service endpoint
]

これを素直に読めば「どこかにcredentialを置け」、と指示されていることになります。

しかし、その前にそもそも、EC2やFargateからAWSのAPIを呼び出すときは、どのユーザーのAWS_ACCESS_KEY_IDとAWS_SECRET_KEYを指定すべきなのでしょうか?

普通に考えたら、サーバーや設定画面に、アクセスキーやシークレットキーを配置するなど、セキュリティ的に考えられませんから、実行しているFargate, EC2のロールに権限をつけることで対象のサービスを呼び出したいところです。

先ほどのエラーも、タスクを実行するロールに、対象のAWSサービス(今回はDynamoDB)にアクセスする権限が付与されていないため、しょうがなくどこかにアクセスキーなどを探しにいったところ、見つからなかった、という顛末でしょう。

ということで、そもそもタスクを実行するロールに適切な権限さえついていれば、問題がないということです。

これを実現するには、ECSのタスク定義でタスクロールを適切に設定すれば良いです。

スクリーンショット 2019-10-12 9.52.08.png

私の場合は、この「タスクロール」に何のロールも設定されていませんでした。ecsTaskExecutionRoleはデフォルトで用意されているもののようで、ひとまずこのロールを設定します。

あとは、対象のAWSサービスを呼び出す権限を、このロールに付与すればOKです。

ecsTaskExecutionRoleのリンクから、ロールの設定画面に飛べます。

スクリーンショット 2019-10-12 9.54.18.png

私の場合は、Fargateで動かすコンテナからDynamoDBを使いたいので、ここにAmazonDynamoDBFullAccessを付与してみました。(権限が強すぎるとは思いますが・・)

すると、先ほどのエラーも出ず、通常どおりに動作しました!

終わりに

以上、私のキャッチアップの道のりをご紹介しました。

aws cliの導入手順などは端折ってしまいましたが、誰かのお役に立てたらと思います。
ご覧いただき、ありがとうございました。

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

AWS amplify フレームワークの使い方Part1〜Auth設定編〜

はじめに

私は、現在Nuxt.jsとamplifyのフレームワークを使って、WEBサイトを絶賛製作中のエンジニアです。
amplifyのフレームワークは、WEBサイト開発の初心者に私とって、非常に便利と感じる一方で、amplify自体が絶賛開発中であるため、新しい仕様が次々に追加され、なかなか日本語での解説された情報が少なく、情報収集にとても苦戦しています。
結局は、公式リファレンスが一番ベストな情報源だったりしますが、その内容を読み解くにも一苦労でしたので、自身の備忘録も兼ねてアウトプットとできればと思います。

対象

この記事は以下のような方におすすめしたいです。

  • amplifyで認証の実装はしてみたが、デフォルトのままで細かい設定は何も行えていない
  • 公式リファレンスを見たが、イマイチよくわからない
  • amplifyのフレームワークを最大限に活用したい
  • GUIから各サービスの設定をしたことがあるが、amplify経由では行ったことがない
  • firebaseではサイト実装の経験はあるがAWSでは初めて

バージョン

  • nuxt 2.9.2
  • aws-amplify 1.1.40

導入

amplifyのフレームワークの導入については、すでに良質な記事がいくつも公開されていますので、この記事では割愛します。

【参考サイト】
AWS Amplify CLIの使い方〜インストールから初期セットアップまで〜

Authの詳細設定

まずはおなじみのコマンドから実行。

$ amplify add auth

初期設定

細かい設定を行いたいため、マニュアル設定を選択。

 Do you want to use the default authentication and security configuration?
Manual configuration 

下記の項目は、正確に把握できていませんが、とりあえず、サインインとサインアップができればいいのでこちらを選択。

 Select the authentication/authorization services that you want to use: 
User Sign-Up & Sign-In only (Best used with a cloud API only)

リソース名とユーザープール名を決定。

 Please provide a friendly name for your resource that will be used to label this category in the project: <resource名>
 Please provide a name for your user pool: <user_pool名>

サインイン設定

今回はユーザー名でのサインインを選択。後から変更はできないので、注意です。
画像では、「検証済みのEメールアドレス」の部分にチェック入っていますが、amplify push前に別作業が必要。(後ほど解説)

 How do you want users to be able to sign in? Username

【参考GUI】
スクリーンショット 2019-10-09 22.37.48.png

MFA設定

次は、MFAの設定。今回は、必須ではなく、ユーザーの選択性にしたいので、OPTIONALを選択し、SMSとTOTPのとちらも利用したいので、両方ともに選択。(必須を選ぶ場合は、このタイミングでしか選択できないため注意)
TOTPの具体的なサイトへの実装方法は別記事でかけたらと思います。

 Multifactor authentication (MFA) user login options: 
OPTIONAL (Individual users can use MFA)
 For user login, select the MFA types: SMS Text Message, Time-Based One-Time Password (TOTP)

【参考GUI画面】
スクリーンショット 2019-10-09 22.43.08.png

検証コード設定

検証コードのメッセージやタイトルはGUI上でいつでも変更可能(メッセージのカスタマイズから)のなので、ここでは変更なし。登録時とパスワードの再発行は、Eメール登録を有効化。

 Please specify an SMS authentication message: Your authentication code is {####}
 Email based user registration/forgot password: Enabled (Requires per-user email entry at registration)
 Please specify an email verification subject: Your verification code
 Please specify an email verification message: Your verification code is {####}

パスワードポリシー設定

パスワードのポリシーは「後から編集できないよ」と怒られますが、実際はGUI上で後から変更可能なので、テスト段階では、設定はしない。

Do you want to override the default password policy for this User Pool? No
 Warning: you will not be able to edit these selections. 

【参考GUI】

スクリーンショット 2019-10-09 23.12.25.png

サインアップ基本設定

今回は、Emailのみを必須項目に選択。(今後編集不可なので注意)
リフレッシュトークンはGUIで変更可能なので、デフォルト値。
読み書きのアクセス権限は、正確に把握できていないため、設定は行っていません。

 What attributes are required for signing up? (Press <space> to select, <a> to toggle all, <i> to invert selection)Email
 Specify the app's refresh token expiration period (in days): 30
 Do you want to specify the user attributes this app can read and write? No

【参考GUI】
スクリーンショット 2019-10-09 23.25.38.png
スクリーンショット 2019-10-09 23.33.41.png

サインアップ追加機能

今回は未実装ですが、サービスが本格化してくるとどの機能も有用なものなので、今後の実装は検討中です。

 Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ Add Google reCaptcha Challenge
 ◯ Email Verification Link with Redirect
 ◯ Add User to Group
 ◯ Email Domain Filtering (blacklist)
 ◯ Email Domain Filtering (whitelist)
 ◯ Custom Auth Challenge Flow (basic scaffolding - not for production)

OAuth設定

ソーシャルログインも行いたいので、OAuthの設定も実施。ドメインも一旦、デフォルト値を使用。

 Do you want to use an OAuth flow? Yes
 What domain name prefix you want us to create for you? <こだわりなければdefault値でOK>

【参考GUI】
スクリーンショット 2019-10-09 23.45.41.png

リダイレクト先を一旦、テスト環境に設定。Authフローとスコープを選択。

【参考サイト】
RFCとなった「OAuth 2.0」――その要点は? (1/2)

Enter your redirect signin URI: http://localhost:3000/
? Do you want to add another redirect signin URI No
 Enter your redirect signout URI: http://localhost:3000/
? Do you want to add another redirect signout URI No
 Select the OAuth flows enabled for this project. Authorization code grant
 Select the OAuth scopes enabled for this project. (Press <space> to select, <a> to toggle all, <i> to invert selection)Phone, Email, OpenID, Profile, aws.cognit
o.signin.user.admin

【参考GUI】
スクリーンショット 2019-10-09 23.56.15.png

ソーシャルログイン設定

今回は、まずFacebookとGoogleログインを選択。各サイトで、取得したIDとシークレットキーを入力。
ソーシャルログイン設定についても1記事にまとめたい、、、。

 Select the social providers you want to configure for your user pool: Facebook, Google

 You've opted to allow users to authenticate via Facebook.  If you haven't already, you'll need to go to https://developers.facebook.com and create an App ID. 

 Enter your Facebook App ID for your OAuth flow: <ID>
 Enter your Facebook App Secret for your OAuth flow:  <SecretKey>

 You've opted to allow users to authenticate via Google.  If you haven't already, you'll need to go to https://developers.google.com/identity and create an App I
D. 

 Enter your Google Web Client ID for your OAuth flow:  <ID>
Enter your Google Web Client Secret for your OAuth flow:  <SecretKey>

トリガー設定

こちらもGUIから設定が可能なので、今回は未設定。ここで設定することで、amplify設定フォルダ内にfunctionフォルダが生成され、amplifyでLambda関数を管理することができる。

? Do you want to configure Lambda Triggers for Cognito? No
Successfully added auth resource

【参考GUI】
スクリーンショット 2019-10-10 0.11.01.png

補足情報

調べるのに地味に時間がかかったプチ情報と罠をご紹介。

検証済みのEメールアドレスでもサインイン可にする方法

amplify add apiを実行してから、amplify pushをする前にamplify/backend/auth/<プロジェクト名>/<プロジェクト名>-cloudformation-template.ymlのファイルに書きを追加する。

xxx-cloudformation-template.yml
Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Ref userPoolName
      // 以下の2行を追加
      AliasAttributes:
        - 'email'

GUI設定の罠

追加でGUI設定をして問題なく、動くものとGUI設定してもAmplify上では認識をしてくれず、動作しないものが存在します。GUIで設定して動かない場合は、amplifyの設定ファイルに直接変更を加え、amplify pushを行う必要があります。私自身、検証しきれていない部分が多いため、ご存知な情報があれば、教えていただきたいです。

GUI設定の罠① OAuth設定

ドメイン設定やリダイレクトURLなど、GUIを変更してもamplifyを経由したアプリでは動作しないため、amplify側の設定ファイルの修正が必要です。(後回しにしていて未調査です、、、)

最後に

AmplifyのAuth様ともっと深いお友達になるためには、設定ファイルを積極的に触っていく必要があると感じている今日このごろです。
今後、API(GraphQL Transform)の@key@auth、Auth(cognito)の設定方法なども記事にしていけたらと思っています。

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

スクリプト実行ディレクトリを取得ためのシェルスクリプト

1958年9月の狩野川台風に匹敵する規模台風19号がやってきて、避難命令が発令されたり、河川が溢れたり大変になっています。ラグビーワールドカップの試合も中止になったりと気持ちの面での被害が大きくなっています。

せっかく中心街のアーケード街で買い物でもしようかなと考えていたのに、電車も計画運休で動いていないので、仕方なく部屋でゴソゴソとパソコン触っています。

個人用にAWS Lightsailに立てているUbuntuサーバーへ実行スクリプトを作成しようとしたら、Shell Scriptをどこからでも呼び出しても、pathがないでエラーを発生させないように絶対パスを取得する方法を忘れてしまったので、覚書として残しておきます。

環境

$ uname -a
Linux ip-172-26-5-169 4.4.0-1074-aws #84-Ubuntu SMP Thu Dec 6 08:57:58 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/issue
Ubuntu 16.04.5 LTS \n \l

Ubuntu 16.04.5 です。

簡単な方法

もし1行で記述する場合は以下の書き方が楽ちんです。

DIR="$( cd "$( dirname "$0" )" >/dev/null 2>&1 && pwd )"
echo "DIR is '$DIR'"

ただし、この方法はディレクトリがSymbolic linkでは対応していません。

シンボリックリンクでも大丈夫な方法

#!/bin/bash

SOURCE="$0"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
echo "DIR is '$DIR'"

だいたいはこのスニペットで大丈夫だと思いますが、もし相対シンボリックリンクも対応したい場合は、下記の方法で取得します。

相対シンボリックパスも大丈夫な方法

#!/bin/sh

SOURCE="$0"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  TARGET="$(readlink "$SOURCE")"
  if [[ $TARGET == /* ]]; then
    echo "SOURCE '$SOURCE' is an absolute symlink to '$TARGET'"
    SOURCE="$TARGET"
  else
    DIR="$( dirname "$SOURCE" )"
    echo "SOURCE '$SOURCE' is a relative symlink to '$TARGET' (relative to '$DIR')"
    SOURCE="$DIR/$TARGET" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  fi
done
echo "SOURCE is '$SOURCE'"
RDIR="$( dirname "$SOURCE" )"
DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
if [ "$DIR" != "$RDIR" ]; then
  echo "DIR '$RDIR' resolves to '$DIR'"
fi
echo "DIR is '$DIR'"

エイリアス、sourcebash -c、シンボリックリンクなどの任意の組み合わせで動作します。

ただし、このスニペットを実行する前にcdコマンドを利用して別のディレクトリに移動した場合、結果が間違っている可能性がありますので注意してください。

最後に

Ubuntuのデフォルトシェルはbashではなく、dashだったんですね(汗

$ readlink -f $(which sh)
/bin/dash

それを知らなかったので、BASH_SOURCE[0]を使うとBad substitutionと警告が表示されてしまった。

$ sh publish.sh 
publish.sh: 7: publish.sh: Bad substitution

参考

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

Shell Scriptの実行ディレクトリを取得する

1958年9月の狩野川台風に匹敵する規模台風19号がやってきて、避難命令が発令されたり、河川が溢れたり大変になっています。ラグビーワールドカップの試合も中止になったりと気持ちの面での被害が大きくなっています。

せっかく中心街のアーケード街で買い物でもしようかなと考えていたのに、電車も計画運休で動いていないので、仕方なく部屋でゴソゴソとパソコン触っています。

今回はShell Scriptをどこから呼び出しても、実行スクリプトの絶対パスを取得できる方法をいつも忘れてしまうので、覚書として残しておこうと思います。

環境

$ uname -a
Linux ip-172-26-5-169 4.4.0-1074-aws #84-Ubuntu SMP Thu Dec 6 08:57:58 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/issue
Ubuntu 16.04.5 LTS \n \l

Ubuntu 16.04.5 です。

簡単な方法

もし1行で記述する場合は以下の書き方が楽ちんです。

DIR="$( cd "$( dirname "$0" )" >/dev/null 2>&1 && pwd )"
echo "DIR is '$DIR'"

ただし、この方法はディレクトリがSymbolic linkでは対応していません。

シンボリックリンクでも大丈夫な方法

#!/bin/bash

SOURCE="$0"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
  SOURCE="$(readlink "$SOURCE")"
  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
echo "DIR is '$DIR'"

だいたいはこのスニペットで大丈夫だと思いますが、もし相対シンボリックリンクも対応したい場合は、下記の方法で取得します。

相対シンボリックパスも大丈夫な方法

#!/bin/sh

SOURCE="$0"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  TARGET="$(readlink "$SOURCE")"
  if [[ $TARGET == /* ]]; then
    echo "SOURCE '$SOURCE' is an absolute symlink to '$TARGET'"
    SOURCE="$TARGET"
  else
    DIR="$( dirname "$SOURCE" )"
    echo "SOURCE '$SOURCE' is a relative symlink to '$TARGET' (relative to '$DIR')"
    SOURCE="$DIR/$TARGET" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  fi
done
echo "SOURCE is '$SOURCE'"
RDIR="$( dirname "$SOURCE" )"
DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )"
if [ "$DIR" != "$RDIR" ]; then
  echo "DIR '$RDIR' resolves to '$DIR'"
fi
echo "DIR is '$DIR'"

エイリアス、sourcebash -c、シンボリックリンクなどの任意の組み合わせで動作します。

ただし、このスニペットを実行する前にcdコマンドを利用して別のディレクトリに移動した場合、結果が間違っている可能性がありますので注意してください。

最後に

Ubuntuのデフォルトシェルはbashではなく、dashだったんですね(汗

$ readlink -f $(which sh)
/bin/dash

それを知らなかったので、BASH_SOURCE[0]を使うとBad substitutionと警告が表示されてしまった。

$ sh publish.sh 
publish.sh: 7: publish.sh: Bad substitution

参考

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

「IoT@Loft ハンズオン スマートファクトリー IoT基盤構築プロトタイピング」参加メモ

はじめに

先日、「IoT@Loftハンズオン スマートファクトリー IoT基盤構築プロトタイピング」というイベントに参加してきましたので、忘れないうちにポイントをメモしておきます。

なお当日の様子は、他に以下の方がQiitaに記事を投稿しています。

アーキテクチャ

今回作成するシステムのアーキテクチャは、以下のようになっています。
image.png

通常エッジ側はRaspberry Piなどを使用するのですが、今回はCloud9で代用します。

構築

それぞれのサービスを作成していきます。
作成する順番としては、末端のほうからになります。
(つないでいくため)

Cloud9

image.png

エッジデバイスの代わりですので、linuxで作成します。
ハンズオンの時間が2時間程度でしたので、自動停止を4時間で設定しました。
image.png

Greengrass

image.png

Cloud9上でGreengrassが動作するようにします。

まず、今回使用するGreengrassのグループ、および、Coreを作成します。
指定した名前でモジュールが出来上がりますので、CoreのセキュリティリソースとGreengrass Coreソフトウェアをダウンロードします。
image.png

Greengrass Coreソフトウェアのダウンロードリンクは、下のほうにスクロールしないと出てこないので注意。
image.png

ダウンロードした2つのtar.zipファイルをCloud9にアップロードします。
image.png

その後、Cloud9にGreengrass用の環境をセットアップします。詳しくはこちらを参照してください。
※今回のハンズオンでは、これらを実行するスクリプトを用意してあり、それを実行するだけでした

Cloud9再起動後、先ほどアップロードしたリソースとGreengrass Coreソフトウェアの展開を行います。
Greengrass Coreはルート「/」に展開、リソースは「/greengrass」に展開します。

さらに、AWS IoTの証明書をダウンロードします。
image.png

準備がすべて終わったら、Greengrassを再起動します。
image.png

Elasticsearch

image.png

本番環境ではありませんのでデプロイタイプは「開発およびテスト」にし、外部からアクセスできるように「パブリックアクセス」を選択します。ただし誰でもアクセスできるようにするのは問題ですので、アクセスポリシーを「特定のIPからのドメインへのアクセスを許可」にして、自分のPCのグローバルIPアドレスからだけアクセスできるようにします。
image.png

Kinesis Firehose

image.png

Elasticsearchが作成されたら、そこに接続するKinesis Firehoseを作成します。
ちなみに、Kinesis Firehoseを使用せずに、直接IoT CoreとElasticsearchをつなぐこともできるのですが、今回は、Elasticsearch側の負荷やデータロスト時の設定が簡単な方法を選んだとのことでした。
image.png

出力を先ほど作成したElasticsearchにします。
image.png
image.png

エラーを出力するS3のバケットは、ここで作成します。
image.png

Elasticsearchへの送信間隔などを設定し、最後にロールを設定します。
image.png

IoT Core

image.png

いよいよ、エッジとクラウドをつなぎます。

送信元は特に指定する必要が無いので、どういったデータを受け取り、どこへ送るのかを設定します。
ここでは送受信のルールとして作成します。
ルールはSQL文で設定します。

SELECT device, value, parse_time("yyyy-MM-dd'T'HH:mm:ss", timestamp) AS timestamp FROM 'data/mine/#'

ここではトピック名を「data/mine/」としています。
(後でも出てきます)

また、送信先としてKinesis Firehoseを指定します。
image.png

IoT Analytics

image.png

受け取ったデータの分析を行います。

今回はクイック作成を利用します。
トピック名は、IoT Coreでの設定に合わせます。
image.png

IoT CoreとIoT Analyticsのつなぐ設定を何もしていないように見えますが、実はこのタイミング(チャネルを作成するとき)で自動的に、IoT Core側のルールができています。(便利!)
image.png

分析用データセットの作成

作成したデータセットを選択し、集計用のSQLクエリを作成して分析用のデータセットに出力するようにします。

SELECT
  timestamp,
  devices['sensor1'] as sensor1,
  devices['sensor2'] as sensor2,
  devices['sensor3'] as sensor3,
  devices['sensor4'] as sensor4
FROM (
  SELECT 
    timestamp, 
    map(array_agg(device),array_agg(value)) as devices
  FROM (
    SELECT
      device,
      value,
      timestamp
    FROM mine_20191012_datastore
    WHERE __dt >= current_date - interval '1' day
  ) GROUP BY timestamp
  ORDER BY timestamp ASC
) WHERE devices['sensor1'] IS NOT NULL AND 
devices['sensor2'] IS NOT NULL AND 
devices['sensor3'] IS NOT NULL AND 
devices['sensor4'] IS NOT NULL

Lambda

image.png

センサー代わりにLambdaを利用します。
クラウド側でLambdaを作成しGreengrassにデプロイします。

なお、今回はすでに作成済みをLambda関数(Python)を使用しました。
image.png

※送信するメッセージは「data/mine/sensore1」~「data/mine/sensor4」となっており、それぞれsinカーブ(異常値あり)の値となっています

Greengrassへの設定

その後、Greengrassに作成したLambda関数を指定します。
image.png

さらに、一部の設定を修正します。
image.png
また、トピック名を環境変数として設定します。
image.png

次に、Greengrassのサブスクリプションを設定します。
今回は、IoT Coreに送るメッセージと送らないメッセージをGreengrassで切り分けます。
判断はトピック名で行います。

指定したトピック名のメッセージをLambdaから受け取り、IoT Coreに送るメッセージを対象とします。
image.png

最後に、これらの設定をGreengrassにデプロイします。

IoT Coreで受け取ったメッセージは、このようになっています。
image.png

Elasticsearch(可視化)

すでに作成済みのElasticsearchで、受け取ったデータを可視化します。
可視化はKibanaを使用します。
image.png

ここではKibanaの設定は省略します。
最終的にはこんな感じに可視化できます。
image.png

QuickSight

image.png

今回のハンズオンでは、時間の関係上、省略されました。

SageMaker

image.png

Notebookを使用して、異常値の判定をSageMakerで行います。
NotebookはIoT Analyticsの「ノートブック」から作成します。
(「ノートブック」はIoT Analyticsで、そこにつながっている「ノートブックインスタンス」がSageMaker)

すでにいくつかサンプルのノートブックが用意されているので、今回は異常検出のノートブックを利用します。
image.png

ノートブックインスタンスが起動したら、ノートブックを表示します。
表示されているコードの一部を、今回のデータ用に変更します。
image.png
image.png

すると、ちゃんと今回のデータで異常値の検知ができるようになります。
image.png

SageMaker(機械学習)

image.png

同じくNotebookを使用して機械学習を行い、モデルをS3に出力します。
出力したモデルは、後ほどエッジで利用します。

機械学習用のノートブックは、先ほどのノートブックインスタンスで実行します。
またノートブックは、すでに用意されているものを使用しました。

ノートブックを実行すると、S3にモデルが出力されました。
image.png

Lambda(機械学習)

image.png

先ほどのS3に保存されたモデルを利用して、エッジ側で異常検知を行います。
ここで使用するLambda関数も、すでに用意されているものを使用しました。

Greengrassへの設定

前回と同じように、作成したLambda関数をGreengerassグループに追加します。
image.png

また、前回同様に一部の設定を修正します。
image.png
image.png

さらに、今回は追加したLambda関数に機械学習のリソースを追加します。
先ほど保存したS3のモデルを指定します。
image.png

次に、今回もサブスクリプションの設定行います。
今回は、センサーから異常検知のLambda関数へのルートと、異常検知のLambda関数からIoT Coreへのルートの2つを設定します。
image.png
image.png
作成できたらデプロイします。

最後に、IoT Coreのほうで、ちゃんと異常検知した場合のメッセージが届いているか確認します。
image.png

まとめ

やることがいっぱいあって大変ですが、一つ一つ意味を考えながら作成していくと、非常にわかりやすいハンズオンだったなと感じました。

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

AWS で、フロントを Web サーバー、バックエンドに DB サーバーの構築時のメモ(その1)

自分のアマゾンのリソースを、そのアカウント情報も含めて、アマゾンのベストプラクティスに合わせる取組をはじめました。

自分のアカウントは、今まで、root ログインであったものを IAM (AWS Identity and Access Management)を活用して、やりたい事象毎に IAM ユーザーを作成して、そのユーザー上で構築を行うように直しました。

過去 5年間にやっていたことは、言わば過去に自分でやっていた自宅サーバーをクラウド上にもってくる、という意味で、コミュニティ版の OS イメージを EC2 インスタンスとして立ち上げ、その上で PHP+PostgreSQL のサイトを立ち上げていただけでいた。まあ、これはこれでオンプレのサーバーをクラウドにおいて管理する、ということで意味があるかとはおもいますが、AWS のベストプラクティスを実践していたとは言えなかったです。

これを、あらたに VPC を作成し、Web サーバーと DB サーバーに分離して、前者を public subnet に、後者を private subnet に置き、internet-gateway、nat-gateway を作成して、DB サーバーをバックエンドに格納してセキュリティを高め、Web サーバーと DB サーバーとの通信は、あらたに作成する routing table により行うことにしてみます。また、EIP もあらたに作成して、インスタンス再起動によっても EC2 インスタンスは複数必要となりますが、最近数年間で、AWS は値下げを続けているそうで、実際、私が数年前、試しに複数インスタンスや EIP をつかってみた時の課金状況よりも、大分改善された(安くなった)と感じているので、AWS のベストプラクティスに従ってちょっとやり直しをしてみよう、ということです。

では、実際にやってみたことを、以下に記述します。

アカウントについて
root ユーザーでログインするときは、メールアドレスでログインして、パスワードを入れます。

IAM ユーザーを作成します。
このアカウントには、一つの環境しか作成しない方がいいです。
また、試験環境のアカウントと、本番環境のアカウントで、分けた方がいいです。
要すれば、アカウント単位で分けるのがいいということになります。

IAM ユーザーでログインする時は、ID + IAMユーザー名 + IAMユーザーのパスワード でログインします。
なお、IAM ユーザーに billing を閲覧させるには、以下のページの通りを設定してください。
https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_billing.html?icmpid=docs_iam_console#tutorial-billing-step1

要すれば、web サーバーを public に置いて、プライベートに置いた db サーバーからデータを取得する、ということをやりたいです。

VPC をクリックします。
vpc を作成します(10.0.0.0/16)。
10.0.0.0/16 のネットワークを作成します。

4つの subnet を作成します。

subnet-xxxx-public-1a (10.0.11.0/24)
subnet-xxxx-public-1c(10.0.12.0/24)
subnet-xxxx-private-1a(10.0.21.0/24)
subnet-xxxx-private-1c(10.0.22.0/24)

それぞれ、north-eastern-1a,1c と紐づけます。

internet-gateway を作成します。
internet-gateway を、取得した EIP に紐づけます。

セキュリティグループを作成します。
web サーバー用のセキュリティグループを作成します。
ssh を、Myip にします。
http も追加します。

dbサーバー用のセキュリティグループを作成します。
ssh を、subnet-xxxx-public-1a のアドレス体系に許可します。
postgresql を追加します。これも、subnet-xxxx-public-1a のアドレス体系に許可します。

nat-gateway を、subnet-xxxx-public-1a (10.0.11.0/24) に作成します。

EC2 を Launch します。
community の fedora をチェックします。
atomic-host-upgrade-20181127 を選びます。
それぞれの項目を適切に選択します。
(あとで記述を追加する)

セキュリティグループ
ssh でログインします。
Elastic IP を指定しておきます。
そしてそれを、作成したマシンに紐づけておきます。

$ ssh -i xxxx.pem fedora@<EIP に紐づいたグローバルIP>
[fedora@ip-10-0-11-112 ~]$

上記のように、subnet に紐づいた名前が出てきますね。
- では、OS のアップグレードをします。
OS のアップグレード

# sudo su -
# atomic host upgrade
# systemctl reboot
  • パッケージの確認をします。
# rpm -qa | wc -l
# rpm -q kernel
kernel-4.19.3-300.fc29.x86_64
  • vim のインストールをします。
# atomic host install vim
  • httpd のインストールをします。
# atomic host install httpd
  • php のインストールをします。
# atomic host install php

ところで、インストールした後に、systemctl reboot としろ、となりますが、
これはなんなんだろう。これやらないと、rpm -qa | grep で
出てこないので、やっておきます。

# systemctl reboot

ところで、dnf も yum も入っていません。。インストール・アップデートは、atomic host コマンドを使うようです。あとでまた調べよう。。

ここで、ip a としてみます。

[fedora@ip-10-0-11-112 ~]$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc fq_codel state UP group default qlen 1000
link/ether 06:20:e7:b2:4a:12 brd ff:ff:ff:ff:ff:ff
inet 10.0.11.112/24 brd 10.0.11.255 scope global dynamic noprefixroute eth0
valid_lft 3323sec preferred_lft 3323sec
inet6 fe80::420:e7ff:feb2:4a12/64 scope link
valid_lft forever preferred_lft forever
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:9e:5b:b1:77 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 scope global docker0
valid_lft forever preferred_lft forever

なるほど、あくまで、ip は、10.0.11.112 という事なのですね。

# systemctl enable httpd
# systemctl start httpd
# systemctl status httpd

DB サーバーを Launch します。PostgreSQL をインストールします。

# atomic host upgrade
# atomic host install postgresql vim postgresql-server
# systemctl reboot

db サーバーを web サーバーと同様に作成します。

web サーバーを踏み台として、dbサーバーに ssh 経由でログインしましょう。
その時に、ローカルホストから、秘密鍵を scp でコピーしておきます。
要すれば、web サーバーに ssh 経由でログインしてから、さらに、db サーバー
にログインする、ということになります。

db サーバーにログインできたら、次のコマンドを実行します。

# rpm -qa | grep postgresql
postgresql-10.6-1.fc29.x86_64
postgresql-libs-10.6-1.fc29.x86_64
postgresql-server-10.6-1.fc29.x86_64
# systemctl status postgresql.service
# systemctl enable postgresql.service
# systemctl start postgresql.service
# systemctl status postgresql.service
--------
Directory "/var/lib/pgsql/data" is missing or empty.
Use "/usr/bin/postgresql-setup --initdb"
--------

ということなので、以下のコマンドを実行します。

# /usr/bin/postgresql-setup --initdb
--------
* Initializing database in '/var/lib/pgsql/data'
* Initialized, logs are in /var/lib/pgsql/initdb_postgresql.log
--------
# systemctl enable postgresql.service
# systemctl start postgresql.service
# systemctl status postgresql.service

立ち上がっていますよね。

# exit
$ exit

とすると、web サーバーに戻ります。

$ exit

で、ホストマシンに戻ります。

参考資料:

AWS でシステム構築するときに使うアイコンのガイドラインとなります。

AWS Simple Icons

今回参考にしたサイトになります。

https://www.udemy.com/aws-14days/

つづく。

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

EFSの作成メモ

概要

 EFSの作成備忘録です。

EFS作成

EFSの設定画面に行って、「ファイルシステムの作成」をクリックします。

WS000012.JPG

マウントしたいVPCを選びます。

WS000014.JPG

Availability Zoneを選びます。

 VPCを選んだ時点で、自動的に割り当てられますが、そのままにせず、アクセスしたいインスタンスのあるAvailability Zoneとセキュリティグループをしっかり選択します。

WS000015.JPG

【注釈】
 Availability ZoneごとにPrivate SubnetとPublic Subnetを選びます。
 つまり、Private SubnetとPublic Subnetの両方からアクセスできるEFSボリュームは作成できません。
 Private SubnetとPublic Subnetでデータを共有したい場合はプライベートアドレスやプライベートDNSを指定してscpやrcyncコマンドでやり取りするか、S3を使いましょう。
 セキュリティグループについても、同様です。

「次のステップ」をクリックします。

WS000023.JPG

タグの追加

 こちらは必要に応じて設定します。
 なくてもいいですが、用途が決まっているのであればしっかり管理の為に設定しておきましょう。
WS000016.JPG

ライフサイクル

 一定期間アクセスのないファイルを、維持費は安いけど、取り出すのに時間のかかる場所に移動するよ、という設定です。
 こちらは、要件に応じてですが、通常は最短で構いません。アクセスを頻繁に行うファイルは移動しない理屈ですので。
 私は14日で設定しました。
WS000018.JPG

スループットモード

 こちらも要件次第ですが、よほど厳しいスループットが要求される環境でもない限りはデフォルトのままで問題ありません。
WS000019.JPG

パフォーマンスモード

 こちらも要件次第ですが、よほど厳しいスループットが要求される環境でもない限りはデフォルトのままで問題ありません。
WS000020.JPG

暗号化

 大事なものを格納する場合は暗号化した方がいいでしょう。
WS000021.JPG

「次のステップ」をクリックします。

確認と作成

 作成内容を確認して、想定したとおりになっていれば、「ファイルシステムの作成」をクリックします。
WS000022.JPG
WS000023.JPG

作成完了

WS000026.JPG

EFSをマウントする。

 以下に記載しています。
 https://qiita.com/SSMU3/items/fe2f6b74ab363b39e2f6

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

[Rails]credentials.yml.encについてまとめる

TECH:EXPERTカリキュラムの最終課題であるメルカリクローンの作成が始まりました。
私はスクラムマスターの担当になりましたので本番環境へのデプロイ担当になりました。

応用カリキュラムのチャットスペースでは、Railsのバージョンは5.0.7.2です。
なので、カリキュラムの内容もsercret.ymlを使ってAWSのEC2にデプロイする内容でしたが、今回、最終課題メルカリで使うバージョンはRails5.2です。
Rails5.2以降は仕様が変更されてsercret.ymlがcredentials.yml.encになっています。
デプロイ環境を構築するのにだいぶ苦労してしまったので、今回はcredentials.yml.encについてまとめていきたいと思います。

credentials.yml.encとは

Rails5.2ではrails newをするとsercret.ymlの代わりにcredentials.yml.encが生成されるようになっています。
credentials.yml.encの役割は以下の通りです。

  • secret.yml、secret.yml.enc、ENV[‘SECRET_KEY_BASE’]の3つのファイルを一元化
  • 本番環境のみで使用する想定なので、環境毎に値を設定する必要がない
  • 復号にはmaster.keyが必要(credentials.yml.encと一緒に生成されます)

Rails newをするとcredentials.yml.encと一緒にconfig/master.keyが生成されます。
config/master.keyはgitignoreに最初から追加されているので設定する必要はありません。
念を押すのであれば、credentials.yml.encもgitignoreに追加してもいいかもしれません。

credentials.yml.encを編集する

credentials.yml.encは直接エディタから編集する事はできません。
ターミナルでエディタを指定して編集します。

<ターミナル>
$ EDITOR=vim(vim以外でも可) bin/rails credentials:edit

上記のコマンドを入力すると、ターミナルで下記の画面が表示されます。
※指定したエディタで開きます。今回はvimで指定したのでターミナルです。

<credentials.yml.enc>
# aws:
#   access_key_id: 123
#   secret_access_key: 345

# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

上記の画面にawsのaccess_key_idやsecret_access_key、その他APIキーなどを入力すれば暗号化されて保存されます。
デフォルトで付いているコメントアウトは外しましょう。

言うなれば、master.keyは鍵、credentials.yml.encは扉の鍵穴に相当します。
credentials.yml.encで暗号化した秘匿情報をmaster.keyを使って復号します。

capistranoによる自動デプロイ

手動でデプロイする場合はこれでOKなのですが、capistranoで自動デプロイをする場合は本番環境にshared/config/master.keyを作成して配置する必要があります。
scpコマンドを使って転送するのが良さそうです。
私は今回は本番環境のshared直下にconfigディレクトリを作成して直接master.keyをコピーしました。

[本番環境]master keyの存在を確認する設定

本番環境ではmaster.keyの指定漏れを防ぐためにconfig/environments/production.rbに
config.require_master_key = trueを有効化することで、本番環境にmaser.keyが存在しない場合エラーになるよう設定できます。

<config/environment/production.rb>
config.require_master_key = true

master.keyを紛失した場合

もし紛失してしまった場合はcredentials.ymlの復号ができなくなります。絶対に紛失はしないでください。

ですが、個人開発だったりチーム開発初期段階でcredentials.ymlに重要な記述がされていなかったりして捨てても良いのであれば再作成した方が良いです。
私は2回再作成しました笑

再作成の方法はcredentials.ymlを削除した後に、下記のコマンドを実行します。

<ターミナル>
$ sudo EDITOR=vim rails credentials:edit

コマンドを実行した時に、master.keyが存在していなければ作成されます。存在している場合はそのまま使われます。

今回は設定しませんでしたが、master.keyが共有できない環境では環境変数:RAILS_MASTER_KEYで指定できます。

master.keyがgitignoreにあらかじめ入っていたり以前より少し楽になった印象です。
開発環境やテスト環境で設定するような場面もありそう?
今回の実装を通して理解が深まりました。

参考
Rails5.2から追加された credentials.yml.enc のキホン
https://qiita.com/NaokiIshimura/items/2a179f2ab910992c4d39
Rails5.2から導入されたcredentials.yml.encを極める
https://qiita.com/yuuuking/items/53a37a2e998972be32b8#%E6%9C%AC%E7%95%AA%E7%92%B0%E5%A2%83master-key%E3%81%AE%E5%AD%98%E5%9C%A8%E3%82%92%E7%A2%BA%E8%AA%8D%E3%81%99%E3%82%8B%E8%A8%AD%E5%AE%9A
Rails5.2からsecrets.yml*が廃止されcredentials.yml.encに統合されるよ
https://qiita.com/daichirata/items/da40e205d273ae69fcfc
Rails 5.2でActiveSupport::MessageEncryptor::InvalidMessage
https://qiita.com/scivola/items/cc06ddbfd94d3118f693

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