- 投稿日:2019-10-12T23:49:07+09:00
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/goofysfstabを使って、自動マウントの設定を行います。
# 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で実行させるなどの工夫が必要です。
- 投稿日:2019-10-12T23:41:11+09:00
AWS EC2インスタンスの作成手順
はじめに
EC2インスタンスの作成手順をメモする
EC2インスタンスの作成
- AWS マネジメントコンソールにログイン
- 下記画像のサービス→EC2をクリック
- インスタンスの作成をクリック
- AMIを選択 Amazon Linux AMIを選択する
- タイプの選択 図のように上から2つ目の無料枠を選択しておく。その後画像右下の【確認と作成】をクリック
- インスタンス作成の確認画面
- ここでは右下にある【起動】をクリック。
- キーペアをダウンロードする
- modalが表示されるので、【新しいキーペアの作成】を選択し、【キーペア名】を任意で入力後、キーペアのダウンロードを行う。
- 【インスタンスの作成】をクリック
- インスタンス一覧からインスタンスを選択
- 取得したElastic IPアドレスを紐づける 上図の【アクション】から【アドレスの関連付け】を選択
- 【アドレスの関連付け】ページにあるインスタンスには先ほどメモした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
- 投稿日:2019-10-12T22:33:33+09:00
AWS EC2作成からSSH接続
前提条件
AWSアカウントを持っていて、AWSコンソールにアクセスできる
EC2の作成
- AWSコンソールにアクセス
- サービスよりEC2へ
- インスタンスの作成
- Amazon Linux AMI 2018.03.0 (HVM), SSD Volume Type - ami-92df37edを選択して、インスタンスを作成
※キーペアを作成した場合、保存場所を忘れないように!
接続するのに使います作成後のダッシュボード画面
- 作成したインスタンスを選択後、接続ボタンをクリック
- 接続方法が表示される
一部抜粋
インスタンスにアクセスするには: 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接続が完了
- 投稿日:2019-10-12T19:32:09+09:00
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) を使用します。
どのリージョンでも良いですが、東京リージョンはレスポンス早くて快適です。
※EC2設定はコチラの記事が分かり易いと思います。この記事は「Amazon Linux 2」で書かれているので、ここを「Ubuntu Server 18.04」に読み替えて下さい。アクセスキーID、シークレットアクセスキーを取得する
td-agentを使ってサーバーからAWSへログを送信するには、アクセスキーIDとシークレットアクセスキーが必要です。アクセスキーIDとシークレットアクセスキーは、AWSコンソールでユーザーを作成する事で取得出来ます。
ユーザーの追加
[セキュリティ、ID、およびコンプライアンス] > [IAM] をクリックします。
[ユーザーを追加] 画面で、ユーザー詳細の設定を行い、[次のステップ:アクセス制限] をクリックします。
- ユーザー名 td-agent-user
- アクセスの種類プログラムによるアクセス
にチェックを入れる
アクセス許可の設定画面では、[既存のポリシーを直接アタッチ]をクリックし、ポリシーのフィルタに「CloudWatchLogsFullAccess」と入力して、検索結果に表示されたら左端のチェックをONにして[次のステップ:タグ]をクリックします。
- アタッチするポリシー名 CloudWatchLogsFullAccess
※この権限があれば、CloudWatchにログ送信する事が出来ます(もっと機能を限定してもいいかもです)。後は、次の[ユーザータグの追加]で[次のステップ:確認]をクリックし、最後の[確認]画面で[ユーザーの作成]をクリックすれば完成です。
表示されるアクセスキー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.comtd-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-logsAWS環境変数の設定
/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]を選択して下さい。
[ログ]をクリックしてみましょう。まだ何もログをキャッチしていない為、ロググループが存在していません。
ログ送信
ではログ送信を行いましょう。認証ログ(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秒ほど時間が掛かる場合があるので注意して下さい。
ロググループに「logtest」が現れました!
「logtest」をクリックすると、ログストリーム「targetserver/var/log/auth.log」が表示されました。
ログストリーム「targetserver/var/log/auth.log」をクリックすると、auth.logの内容をCloudWatchLogsで確認する事が出来ました。
念のためサーバー側で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)と、様々な機能が簡単に使える様になって便利です。
最後に
もし不明点等あれば、なるべく分かり易く修正したいと思います。よろしくお願いします。
- 投稿日:2019-10-12T18:19:03+09:00
プライベートサブネット内のインスタンスにpublic IPを割り当てると外からpingは届くのか?
- 投稿日:2019-10-12T17:42:47+09:00
[Lambda][Angular] 知らないことを知りたい→Googleの検索補完を一覧にしたらいい?
動機と思いつき
- 知らない街で美味しいご飯に巡り会いたい
- レビューサイトを使いこなすのは難しい
- 他のひとが何を探しているかわかったらいいかも
- 検索語 + 「あ」〜「ん」をGoogleに渡して、なにが補完されるか一覧してみよう
さて、Google補完API
どうも公式なAPIではないようです。
を参考にしました。JSONじゃなくてXMLが返ってくるんですね。
構成
Google APIはJavaScriptでたたけばいいし、Angularで作ったフロントエンドだけで完結しようと思いましたができません。
(どうしてできなかったのかは忘れました)。
間にプロキシを挟むことにしました。稼働環境
AWSの鉄板構成です。
フロントエンドはS3に格納します。S3単体ではhttpsできないため、CloudFrnotを使います。
Google API呼び出しはLambdaで作りました。できあがったもの
ソースコード https://github.com/sengokyu/hokanchan
考察
- 漫然と検索語を入力すると、期待する結果を得られない。
- 狭い範囲の検索語に絞り込むと良い結果が得られる。
- 例)「銀座 ランチ」 → 「銀座 ランチ 天ぷら」
- 期待する結果が得られる地域と、得られない地域がある。
- 暮らす人々がどのくらいネットを使っているかによる?
ふりかえり
Keep
- RxJSでエラーが起きたかどうか関係なく最後に処理を挟むのであれば、
pipe(finalize(()=> { /* ... */ })
を使えばいい。Problem
- 欲張ってCloudFormationですべてデプロイしようとして時間がかかりました。
Try
- CloudFormationを親子関係にして、環境全体とLambdaに分ける。
その他
- .dev ドメインはブラウザ(Chrome/Firefox)がhttpsアクセスを強制します。httpへアクセスできなくて悩みました。
リンク
- 投稿日:2019-10-12T16:50:15+09:00
Slack, Twitterのアイコンを定期更新して同僚・フォロワーに今の天気を通知する
僕はずっといらすとやさんのグラサン太陽アイコンを使っているのですが、その時の天気に合わせて動的にアイコンを変えたら簡易天気速報みたいになって面白いんじゃないかとふと思い、AWSとGo(特に非同期処理)の勉強にもなりそうだったので実装してみました。
ざっくりとした処理の流れ
以下の処理を行うスクリプトをGoで書き、Lambda上で定期実行
- まず気象情報APIから現在の天気情報を取得
- 取得した天気情報を元にS3の画像を選択
- 選択した画像をSlack API/setPhoto, Twitter APIに投げてアイコンをアップデート
- Slack api/postMessageでアップデートの成功/失敗を会社ワークスペースの自分宛DMに通知
使用技術
- 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にはいらすとやから貰った画像を適当な名前をつけてアップロード
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へのリンクを通知します。
コードのアップロード
$ GOOS=linux go build main.go $ zip main.zip main # これでzipされたファイルをLambdaにアップロード感想
AWSとGo(特に非同期処理周り)の入門としてこのアプリを作ってみようと思ったのですが、実際に何かを作りながら勉強するとやっぱり楽しいし書籍とにらめっこするより効率がいいと思いました。
また、各種APIやSDKなど本来意図していなかった分野の技術についても理解が深まって有意義でした。
まだまだ初心者なのでおかしい部分などあれば指摘大歓迎です!ソースコード
- 投稿日:2019-10-12T16:50:15+09:00
Slack・Twitterのアイコンを定期更新して同僚・フォロワーに今の天気を通知する
僕はずっといらすとやさんのグラサン太陽アイコンを使っているのですが、その時の天気に合わせて動的にアイコンを変えたら簡易天気速報みたいになって面白いんじゃないかとふと思い、AWSとGo(特に非同期処理)の勉強にもなりそうだったので実装してみました。
ざっくりとした処理の流れ
以下の処理を行うスクリプトをGoで書き、Lambda上で定期実行
- まず気象情報APIから現在の天気情報を取得
- 取得した天気情報を元にS3の画像を選択
- 選択した画像をSlack API/setPhoto, Twitter APIに投げてアイコンをアップデート
- Slack api/postMessageでアップデートの成功/失敗を会社ワークスペースの自分宛DMに通知
使用技術
- 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にはいらすとやから貰った画像を適当な名前をつけてアップロード
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へのリンクを通知します。
コードのアップロード
$ GOOS=linux go build main.go $ zip main.zip main # これでzipされたファイルをLambdaにアップロード感想
AWSとGo(特に非同期処理周り)の入門としてこのアプリを作ってみようと思ったのですが、実際に何かを作りながら勉強するとやっぱり楽しいし書籍とにらめっこするより効率がいいと思いました。
また、各種APIやSDKなど本来意図していなかった分野の技術についても理解が深まって有意義でした。
まだまだ初心者なのでおかしい部分などあれば指摘大歓迎です!ソースコード
- 投稿日:2019-10-12T16:50:15+09:00
Slack, Twitterのアイコンを定期更新して同僚とフォロワーに今の天気を通知する
僕はずっといらすとやさんのグラサン太陽アイコンを使っているのですが、その時の天気に合わせて動的にアイコンを変えたら簡易天気速報みたいになって面白いんじゃないかとふと思い、AWSとGo(特に非同期処理)の勉強にもなりそうだったので実装してみました。
ざっくりとした処理の流れ
以下の処理を行うスクリプトをGoで書き、Lambda上で定期実行
- まず気象情報APIから現在の天気情報を取得
- 取得した天気情報を元にS3の画像を選択
- 選択した画像をSlack API/setPhoto, Twitter APIに投げてアイコンをアップデート
- Slack api/postMessageでアップデートの成功/失敗を会社ワークスペースの自分宛DMに通知
使用技術
- 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にはいらすとやから貰った画像を適当な名前をつけてアップロード
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へのリンクを通知します。
コードのアップロード
$ GOOS=linux go build main.go $ zip main.zip main # これでzipされたファイルをLambdaにアップロード感想
AWSとGo(特に非同期処理周り)の入門としてこのアプリを作ってみようと思ったのですが、実際に何かを作りながら勉強するとやっぱり楽しいし書籍とにらめっこするより効率がいいと思いました。
また、各種APIやSDKなど本来意図していなかった分野の技術についても理解が深まって有意義でした。
まだまだ初心者なのでおかしい部分などあれば指摘大歓迎です!ソースコード
- 投稿日:2019-10-12T16:38:47+09:00
DockerやECR, ECS, Fargateなど、コンテナ周りのAWS知識を効率的にキャッチアップしたい人のために
概要
私自身がコンテナや、コンテナ関連のAWSサービスについてはほぼ分からない状態だったのですが、そこからできる限り効率的に知識をキャッチアップしたくて学習したときの道のりです。
同じように困っていらっしゃる方のお役に立てばと思い、記事にしてみました。この道のりの通りに進んでいただければ、時間を無駄にすることなく、多少なりともスムーズに知識をキャッチアップできると思います。
主要な概念や全体像を理解するまでの道のり
いきなり詳細に踏み込んでも、つまりいきなりFargateなどのAWSサービスを使っても、すぐに迷子になることは目に見えていましたので、まずは全体感や重要な概念、用語を理解しようと思いました。
そこで色々と調べていると、次の記事を見つけました。ものすごく分かりやすかったです。
- 「それコンテナにする意味あんの?」迷える子羊に捧げるコンテナ環境徹底比較 (https://dev.classmethod.jp/cloud/aws/cmdevio2019-container/ )
- この記事で紹介されていた以下リンクも、後から読もうと思っています。
- Best practices for writing Dockerfiles(https://docs.docker.com/develop/develop-images/dockerfile_best-practices/ )
- [AWS Black Belt Online Seminar] Docker on AWS レポート [12-factor App](https://dev.classmethod.jp/cloud/aws/black-belt-docker-on-aws-2017/)
これを読んでよく分かったのは、以下の点です。
- レジストリとは、Docker Hubのようなもので、Dockerイメージを管理するところ。AWSだとECRがこれにあたる。
- それとは別次元で、以下がある。
- コントロールプレーン
- コンテナ数の管理など、コンテナの管理をするもの。データプレーンを起動する役割したりする。ECSがこれにあたる。
- データプレーン
- 実際にコンテナが動く場所で、EC2やFargateがこれにあたる。
ただ、上記の記事を読んでも、ECSのクラスター、サービス、タスク、コンテナの概念はよく分かりませんでした。うーむ、難しい・・。
また、それらとデータプレーン(EC2, Fargate)との違いについても、ほぼ理解できませんでした。
こういったトピックについては、以下の記事がとても分かりやすいです。
- Amazon EC2 Container Service(ECS)の概念整理(https://qiita.com/NewGyu/items/9597ed2eda763bd504d7)
DockerコンテナをFargateで動かすことの素晴らしさ
こうやって調査をしていきますと、Fargateというサービスの何が素晴らしいのか、という点が見えてきました。
DockerコンテナをFargateで動かすということは
- PaaS(Elastic Beanstalkなど)よりも自由度が高くなり
- IaaS(EC2など)よりも生産性が高くなる(DockerのインストールやOSセキュリティパッチの適用などから解放される)
ということです。一言でいうと「良い塩梅でめっちゃいい」ということですね。
EC2(つまりIaaS)でコンテナを動かすことはできます。しかし、それにはEC2インスタンスを構築し、Dockerという基盤をインストールする必要があります。そしてOSセキュリティパッチなどの面倒を見続けなければなりません。それは、ものすごく大変なことです。
そのあたりの大変さをAWSが引き受けてくれる、というのがFargateの素晴らしいところです。私たちは、Dockerイメージさえつくれば良いのです。
こういった点から、色々な制約はもちろんあるものの、マネージドの恩恵を最大限に受けるようにした方が得策だと感じてきました。
Dockerfileとdocker-composeの関係
Dockerについてまだ慣れていない私は、この二つの関係がよく分からず混乱していました。
この点については、以下の記事で分かりやすく解説されています。
- Dockerfileとdocker-compose(https://qiita.com/koka/items/3d3d4ee5680f92a0ad89)
実際に動かしてみるまでの道のり
主要な概念や全体像を理解できましたので、今度は実際にECR, ECS, Fargateを使って、Dockerイメージを動かしてみることにしました。
私の場合、自作したアプリを動かしてみたかった関係で、Spring Bootが動くコンテナをつくってみようと思いました。
その際、以下の記事を参考にさせていただきました。
- Docker上でSpring Bootを動かしてみる(https://qiita.com/tkani/items/ed56229330f00a333d5e)
こちらの記事を参考に、以下のDockerfileを、Mavenプロジェクト直下に作成しました。
DockerfileFROM 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オプションについては、以下の公式リファレンスで解説されています。
- http://docs.docker.jp/engine/reference/commandline/build.html#t
- http://docs.docker.jp/engine/reference/commandline/tag.html
しかし、これらを読んでもイマイチよく分かりません。他の記事を調べても「なるほど!そういうことか!」という思いに至ることができませんでした。
公式リファレンスや先人の解説記事の内容をつなぎ合わせると、イメージ名は以下のものだと分かってきました。
「イメージ名」とは?
イメージ名というのは、全世界のなかでイメージを一意に特定するための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
のようになります。
管理コンソールから見ると・・・
しっかり登録されています!よかった!
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のタスク定義でタスクロールを適切に設定すれば良いです。
私の場合は、この「タスクロール」に何のロールも設定されていませんでした。ecsTaskExecutionRoleはデフォルトで用意されているもののようで、ひとまずこのロールを設定します。
あとは、対象のAWSサービスを呼び出す権限を、このロールに付与すればOKです。
ecsTaskExecutionRoleのリンクから、ロールの設定画面に飛べます。
私の場合は、Fargateで動かすコンテナからDynamoDBを使いたいので、ここにAmazonDynamoDBFullAccessを付与してみました。(権限が強すぎるとは思いますが・・)
すると、先ほどのエラーも出ず、通常どおりに動作しました!
終わりに
以上、私のキャッチアップの道のりをご紹介しました。
aws cliの導入手順などは端折ってしまいましたが、誰かのお役に立てたらと思います。
ご覧いただき、ありがとうございました。
- 投稿日:2019-10-12T16:15:03+09:00
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
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上でいつでも変更可能(メッセージのカスタマイズから)のなので、ここでは変更なし。登録時とパスワードの再発行は、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】
サインアップ基本設定
今回は、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サインアップ追加機能
今回は未実装ですが、サービスが本格化してくるとどの機能も有用なものなので、今後の実装は検討中です。
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>リダイレクト先を一旦、テスト環境に設定。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ソーシャルログイン設定
今回は、まず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
補足情報
調べるのに地味に時間がかかったプチ情報と罠をご紹介。
検証済みのEメールアドレスでもサインイン可にする方法
amplify add api
を実行してから、amplify push
をする前にamplify/backend/auth/<プロジェクト名>/<プロジェクト名>-cloudformation-template.yml
のファイルに書きを追加する。xxx-cloudformation-template.ymlType: 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)の設定方法なども記事にしていけたらと思っています。
- 投稿日:2019-10-12T14:21:20+09:00
スクリプト実行ディレクトリを取得ためのシェルスクリプト
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 \lUbuntu 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'"エイリアス、
source
、bash -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
参考
- 投稿日:2019-10-12T14:21:20+09:00
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 \lUbuntu 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'"エイリアス、
source
、bash -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
参考
- 投稿日:2019-10-12T13:39:07+09:00
「IoT@Loft ハンズオン スマートファクトリー IoT基盤構築プロトタイピング」参加メモ
はじめに
先日、「IoT@Loftハンズオン スマートファクトリー IoT基盤構築プロトタイピング」というイベントに参加してきましたので、忘れないうちにポイントをメモしておきます。
なお当日の様子は、他に以下の方がQiitaに記事を投稿しています。
- IoT@Loft ハンズオン - スマートファクトリー IoT基盤構築プロトタイピング@目黒セントラルスクウェア 参加記
- AWS IoT@Loft スマートファクトリー IoT基盤構築プロトタイピング を受講したよ(2019年10月2日実施)
アーキテクチャ
今回作成するシステムのアーキテクチャは、以下のようになっています。
通常エッジ側はRaspberry Piなどを使用するのですが、今回はCloud9で代用します。
構築
それぞれのサービスを作成していきます。
作成する順番としては、末端のほうからになります。
(つないでいくため)Cloud9
エッジデバイスの代わりですので、linuxで作成します。
ハンズオンの時間が2時間程度でしたので、自動停止を4時間で設定しました。
Greengrass
Cloud9上でGreengrassが動作するようにします。
まず、今回使用するGreengrassのグループ、および、Coreを作成します。
指定した名前でモジュールが出来上がりますので、CoreのセキュリティリソースとGreengrass Coreソフトウェアをダウンロードします。
Greengrass Coreソフトウェアのダウンロードリンクは、下のほうにスクロールしないと出てこないので注意。
ダウンロードした2つのtar.zipファイルをCloud9にアップロードします。
その後、Cloud9にGreengrass用の環境をセットアップします。詳しくはこちらを参照してください。
※今回のハンズオンでは、これらを実行するスクリプトを用意してあり、それを実行するだけでしたCloud9再起動後、先ほどアップロードしたリソースとGreengrass Coreソフトウェアの展開を行います。
Greengrass Coreはルート「/」に展開、リソースは「/greengrass」に展開します。準備がすべて終わったら、Greengrassを再起動します。
Elasticsearch
本番環境ではありませんのでデプロイタイプは「開発およびテスト」にし、外部からアクセスできるように「パブリックアクセス」を選択します。ただし誰でもアクセスできるようにするのは問題ですので、アクセスポリシーを「特定のIPからのドメインへのアクセスを許可」にして、自分のPCのグローバルIPアドレスからだけアクセスできるようにします。
Kinesis Firehose
Elasticsearchが作成されたら、そこに接続するKinesis Firehoseを作成します。
ちなみに、Kinesis Firehoseを使用せずに、直接IoT CoreとElasticsearchをつなぐこともできるのですが、今回は、Elasticsearch側の負荷やデータロスト時の設定が簡単な方法を選んだとのことでした。
Elasticsearchへの送信間隔などを設定し、最後にロールを設定します。
IoT Core
いよいよ、エッジとクラウドをつなぎます。
送信元は特に指定する必要が無いので、どういったデータを受け取り、どこへ送るのかを設定します。
ここでは送受信のルールとして作成します。
ルールはSQL文で設定します。例SELECT device, value, parse_time("yyyy-MM-dd'T'HH:mm:ss", timestamp) AS timestamp FROM 'data/mine/#'ここではトピック名を「data/mine/」としています。
(後でも出てきます)また、送信先としてKinesis Firehoseを指定します。
IoT Analytics
受け取ったデータの分析を行います。
今回はクイック作成を利用します。
トピック名は、IoT Coreでの設定に合わせます。
IoT CoreとIoT Analyticsのつなぐ設定を何もしていないように見えますが、実はこのタイミング(チャネルを作成するとき)で自動的に、IoT Core側のルールができています。(便利!)
分析用データセットの作成
作成したデータセットを選択し、集計用の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 NULLLambda
センサー代わりにLambdaを利用します。
クラウド側でLambdaを作成しGreengrassにデプロイします。なお、今回はすでに作成済みをLambda関数(Python)を使用しました。
※送信するメッセージは「data/mine/sensore1」~「data/mine/sensor4」となっており、それぞれsinカーブ(異常値あり)の値となっています
Greengrassへの設定
その後、Greengrassに作成したLambda関数を指定します。
さらに、一部の設定を修正します。
また、トピック名を環境変数として設定します。
次に、Greengrassのサブスクリプションを設定します。
今回は、IoT Coreに送るメッセージと送らないメッセージをGreengrassで切り分けます。
判断はトピック名で行います。指定したトピック名のメッセージをLambdaから受け取り、IoT Coreに送るメッセージを対象とします。
最後に、これらの設定をGreengrassにデプロイします。
IoT Coreで受け取ったメッセージは、このようになっています。
Elasticsearch(可視化)
すでに作成済みのElasticsearchで、受け取ったデータを可視化します。
可視化はKibanaを使用します。
ここではKibanaの設定は省略します。
最終的にはこんな感じに可視化できます。
QuickSight
今回のハンズオンでは、時間の関係上、省略されました。
SageMaker
Notebookを使用して、異常値の判定をSageMakerで行います。
NotebookはIoT Analyticsの「ノートブック」から作成します。
(「ノートブック」はIoT Analyticsで、そこにつながっている「ノートブックインスタンス」がSageMaker)すでにいくつかサンプルのノートブックが用意されているので、今回は異常検出のノートブックを利用します。
ノートブックインスタンスが起動したら、ノートブックを表示します。
表示されているコードの一部を、今回のデータ用に変更します。
すると、ちゃんと今回のデータで異常値の検知ができるようになります。
SageMaker(機械学習)
同じくNotebookを使用して機械学習を行い、モデルをS3に出力します。
出力したモデルは、後ほどエッジで利用します。機械学習用のノートブックは、先ほどのノートブックインスタンスで実行します。
またノートブックは、すでに用意されているものを使用しました。Lambda(機械学習)
先ほどのS3に保存されたモデルを利用して、エッジ側で異常検知を行います。
ここで使用するLambda関数も、すでに用意されているものを使用しました。Greengrassへの設定
前回と同じように、作成したLambda関数をGreengerassグループに追加します。
さらに、今回は追加したLambda関数に機械学習のリソースを追加します。
先ほど保存したS3のモデルを指定します。
次に、今回もサブスクリプションの設定行います。
今回は、センサーから異常検知のLambda関数へのルートと、異常検知のLambda関数からIoT Coreへのルートの2つを設定します。
作成できたらデプロイします。最後に、IoT Coreのほうで、ちゃんと異常検知した場合のメッセージが届いているか確認します。
まとめ
やることがいっぱいあって大変ですが、一つ一つ意味を考えながら作成していくと、非常にわかりやすいハンズオンだったなと感じました。
- 投稿日:2019-10-12T10:31:59+09:00
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 httpdDB サーバーを Launch します。PostgreSQL をインストールします。
# atomic host upgrade # atomic host install postgresql vim postgresql-server # systemctl rebootdb サーバーを 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/
つづく。
- 投稿日:2019-10-12T07:28:10+09:00
EFSの作成メモ
概要
EFSの作成備忘録です。
EFS作成
EFSの設定画面に行って、「ファイルシステムの作成」をクリックします。
マウントしたいVPCを選びます。
Availability Zoneを選びます。
VPCを選んだ時点で、自動的に割り当てられますが、そのままにせず、アクセスしたいインスタンスのあるAvailability Zoneとセキュリティグループをしっかり選択します。
【注釈】
Availability ZoneごとにPrivate SubnetとPublic Subnetを選びます。
つまり、Private SubnetとPublic Subnetの両方からアクセスできるEFSボリュームは作成できません。
Private SubnetとPublic Subnetでデータを共有したい場合はプライベートアドレスやプライベートDNSを指定してscpやrcyncコマンドでやり取りするか、S3を使いましょう。
セキュリティグループについても、同様です。「次のステップ」をクリックします。
タグの追加
こちらは必要に応じて設定します。
なくてもいいですが、用途が決まっているのであればしっかり管理の為に設定しておきましょう。
ライフサイクル
一定期間アクセスのないファイルを、維持費は安いけど、取り出すのに時間のかかる場所に移動するよ、という設定です。
こちらは、要件に応じてですが、通常は最短で構いません。アクセスを頻繁に行うファイルは移動しない理屈ですので。
私は14日で設定しました。
スループットモード
こちらも要件次第ですが、よほど厳しいスループットが要求される環境でもない限りはデフォルトのままで問題ありません。
パフォーマンスモード
こちらも要件次第ですが、よほど厳しいスループットが要求される環境でもない限りはデフォルトのままで問題ありません。
暗号化
「次のステップ」をクリックします。
確認と作成
作成内容を確認して、想定したとおりになっていれば、「ファイルシステムの作成」をクリックします。
作成完了
EFSをマウントする。
以下に記載しています。
https://qiita.com/SSMU3/items/fe2f6b74ab363b39e2f6
- 投稿日:2019-10-12T01:46:16+09:00
[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 = truemaster.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