20201215のAWSに関する記事は30件です。

EKS超入門「INTRODUCTION TO AMAZON EKS」を3時間300円でやった話

はじめに

たった3時間、300円でEKSを味わえるチュートリアルです。
EKSについてちょっと知ってみたい方は迷わずやってみた方がいいです。

EKSを学ぶというよりは、EKSとEKSに関連するマネージドサービス(cloudFormation、IAM等)のつながりが見えてくるので、非常に良かったです。(普通にAWSの勉強にもなるので、是非おすすめ)

対象者

・Kubernetes について聞いたことがあるけどまだ触ったことがない方
・Kubernetes を知ってはいるけど、Amazon EKS は触ったことがない方 ← 自分はここに該当

環境

AWSアカウントがあれば大丈夫です

URL

INTRODUCTION TO AMAZON EKS
https://eks-for-aws-summit-online.workshop.aws/

実施手順

チュートリアル通りに実施すれば、詰まるところは正直なかったです。
とにかく、丁寧に書いてくれています。

強いて言えば、このチュートリアルはus-east-1で実施する内容となっているので、
東京リージョンで実施したい方が、そこだけ読み替えて進むと良いと思います。
私は東京リージョンでやりました。

感想

・eksctlをめちゃめちゃ使うと思ったが、クラスター構築くらいで、
 あとはいつも通り、kubectlだったのが一番最初の驚き。
・eksctlはCloudFormation を使って VPC や EKS クラスターのコントロールプレーンやワーカーノードの
 Auto Scaling Group などの AWS リソースを作成(これは単純に自分の備忘用)
・cloud9を初めて使ったが、VSCodeに何となく似ていて良かった。

今後

EKSWorkshopというものがあって、ここではeksの周辺システムについて、ハンズオン形式で学べるチュートリアルがあります。
以下はその一例ですが、EKS並びにkubernetersについて一通り学べそうな内容になっていて、かなり良さそうなので、
次回はこれをやっていきたいと思います。
・Using Spot Instances with EKS
・Autoscaling our Applications and Clusters
・Monitoring Using Prometheus and grafana

https://www.eksworkshop.com/

蛇足:チュートリアル完了後のbilling

私の場合は、集中的な時間をとって実施できなかったのですが、(おそらくスタートから完了まで10時間くらいかかりましたが、)
このくらいの金額で済みました。集中して時間が取れる方なら、これより安く済むはずなので、実施してみてください!

image.png

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

AWSアプリの修正から自動デプロイまでの流れ

AWS自動デプロイまでの処理が終わり、ブラウザからElastic IPでアクセスしたところ「We’re sorry , but ~」というエラーに悩まされました。

例えば、環境変数の指定の仕方に誤りがないか、インスタンスを再起動したりなど探していきました。

解決方法としては、しっかりとエラーログを確認して原因を推察し、解決する仮説を立てて一つずつ検証していきました。

原因が推察できなければ、Qiitaや質問サイトなどで情報を収集し、原因を特定していきました。

今回の原因は、masterでのcommit→pushのし忘れという初歩的なものでした。

細かいなミスで発生したエラーでも原因が特定できないと、解決にかなり時間がかかってしまうことを実感して、自分用マニュアルとしてQiitaにまとめました。

アプリの修正から自動デプロイまでの流れ

状況 自動デプロイ前にやること 必要性の有無
①ローカルでVSCodeを修正した場合 変更点をリモートリポジトリにcommit→pushする(ブランチを切っている場合はmergeまで実行 該当時のみ
②ローカルでデータベース関連の内容を修正した場合 本番環境で「rails db:drop RAILS_ENV=production」「rails db:create RAILS_ENV=production」を実行(※実行する際、「DISABLE_DATABASE_ENVIRONMENT_CHECK=1」というオプションが必要です) 該当時のみ
③Nginxを修正した場合 「sudo systemctl restart nginx」を実行 該当時のみ
④再度自動デプロイを実行する場合(本番環境でサーバー再起動をする場合) 一度プロセスをkillした上で「bundle exec cap production deploy」を実行 ①②の場合のみ

今回は頻繁に行うであろう
④再度自動デプロイを実行する場合(本番環境でサーバー再起動をする場合)をまとめました。

「EC2アプリ内:1つ目のターミナル」

①ディレクトリの移動

cd .ssh/

②EC2にログイン

ssh -i (○○.pem) ec2-user@(Elastic IP 3.140.60.122)

③アプリのディレクトリ

cd /var/www/(アプリ名)

④プロセスを確認

ps aux | grep unicorn (一番目の数字)

⑤プロセスをkill 特に何も表示されない

kill (一番目の数字)

スクリーンショット 2020-12-14 10.29.06.png
「ローカルアプリ内:2つ目のターミナル」

bundle exec cap production deploy

(自動デプロイ)2〜3分待ってエラーがなければOK!
____________________________2020-12-14_10.30.25.png
「ブラウザ」で無事にアクセスできれば成功!
3.140.60.122(例)
スクリーンショット 2020-12-15 22.50.15.png

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

EKSへのkubectl get svcで「error: You must be logged in to the server (Unauthorized)」

症状

マネジメントコンソールでEKSを作成した時に

> kubectl get svc
error: You must be logged in to the server (Unauthorized)

となって確認ができない。

解決方法

コンソールにIAMのユーザーでサインインしてクラスタを作成し、同じユーザーでkubectlを実行する。

> kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.100.0.1   <none>        443/TCP   46m

説明

Amazon EKS クラスターが作成されたら、クラスターを作成する IAM エンティティ (ユーザーまたはロール) は、system:masters アクセス許可が付与された管理者として Kubernetes RBAC 認証テーブルに追加されます。最初は、その IAM ユーザーのみが kubectl を使用して Kubernetes API サーバーを呼び出すことができます。

クラスタを作成した直後は、作成したユーザでのみ呼び出しが可能です。
サインインするときに IAMユーザーで入り、そのユーザーで操作を実施しましょう。

自分は同じユーザーだとすっかり勘違いをして、ルートユーザーでクラスタ作成を行っていました。
もし同じ症状の場合は、今一度の見直しを…。

他にもロールの設定不備でも同じ様なエラーが出るようなので、一つ一つ確認して解決されるとよいかと思います。

参考

https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/create-cluster.html#create-cluster-console

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

Lambda on Terraform Container

はじめに

この記事はterraform Advent Calendar 2020の15日目です。

もともとTerraformのテストについてつらつら書こうとしましたが、re:Invent2020でLambdaのコンテナサポートされました
今回re:Invent2020で発表された中で、比較的反響がでかかった発表じゃないでしょうか
この波に乗らねば、ということで、Terraformをコンテナ化してLambdaで実行できるようにしてみました
※決してLambdaをTerraformで構築する記事ではありません

構成

今回の構成は下記です
lambda_terraform.png

開発ではCloud9を利用して、内部でコンテナを使って確認を行い、本番ではECRにイメージをアップしてLambdaから利用するという形になります
今回は時間がなかったのでAPI Gatewayは未使用です

ユースケース

ユースケースとしては下記の2つほどあるかと思ってます

  • 共通実行基盤
  • 監視

共通実行基盤としては、よくTerraformの実行基盤で悩む人がいると思うのですが、こちらを今回作成するLambdaを共通基盤とするのも一つの案かと思います
今回はただコードを持ってきて叩いてますが、これをCodeCommitやGithubで管理してるコードを引っ張ってくるようにすれば、最新コードをとってデプロイできるかと思います

監視としては、よくコードとリソースが乖離している問題があると思いますが、今回のLambdaを利用すれば、乖離したら検知できるようにできます。今回の作成したAPIを叩けば追加・変更・削除のリソース数が取れるので、PrometheusでもDatadogでも利用すれば0じゃない時にアラートを出せるかと思います

リポジトリ

対象のリポジトリは下記です
https://github.com/Yusuke-Shimizu/terraform_on_lambda
記事読むのだるいって人はcloneして使ってください
Dockerfileと.envrcの値は各自修正してご利用ください(アクセスキーとシークレットキー書いたままプッシュしちゃダメだよ!)

コンテナイメージ

コンテナをLambdaに乗っける場合、下記のどちらかで構築する必要があります

ランタイムAPIは必要なAPIを作る必要があるので、柔軟性はありますが手間が多いです
ベースイメージの方が、FROMに入れて追加していくだけで簡単なため、今回はこちらを利用します

ベースイメージの選定

今回はPythonを利用してTerraformを実行していきます
Terraform自体がGoなので、Goでやろうとしたのですが、いかんせんGoの経験が少ないのと、Lambda on Goの情報も少ないので断念しました。。
Amazon ECR Public Galleryを参考に下記のようにベースイメージを決めました

FROM public.ecr.aws/lambda/python:3.8
...

ちなみに、このFROMの public.ecr.aws もre:Invent2020で発表されたECR Publicです
簡単にいうと、AWS版のDocker Hubです

Terraformのインストール

上記のPythonイメージはPythonの実行しか出来ないため、Terraformを実行できるようにする必要があります
そのため、下記のようにzipをインストールしてterraformの実行をできるようにします

ARG terraform_version="0.14.2"
ADD https://releases.hashicorp.com/terraform/${terraform_version}/terraform_${terraform_version}_linux_amd64.zip terraform_${terraform_version}_linux_amd64.zip
RUN yum -y install unzip wget
RUN unzip ./terraform_${terraform_version}_linux_amd64.zip -d /usr/local/bin/
RUN rm -f ./terraform_${terraform_version}_linux_amd64.zip
ENV TF_DATA_DIR /tmp

実行ディレクトリ

Lambdaは実行するとき、/var/taskディレクトリで実行するのですが、基本的にこのディレクトリでファイルを作成したり修正することはできません
参考:https://stackoverflow.com/questions/45608923/aws-lambda-errormessage-errno-30-read-only-file-system-drive-python-quic

そのため、最後の ENV TF_DATA_DIR /tmp でTFのデータ出力先を変更してます
参考:https://www.terraform.io/docs/commands/environment-variables.html

Dockerfileの残り

あとはAWSのキーや必要なファイルをコピーしてhandlerを実行するだけでDockerfileは完了です
AWSキーは本来はLambdaのロールを利用すれば良いのですが、ローカル検証も合わせて実行したいので、とりあえずで入れてます
本来はここもsts化するなり外部から環境変数を注入するタイプにしたほうが良いと思います

ENV AWS_ACCESS_KEY_ID 【アクセスキー】
ENV AWS_SECRET_ACCESS_KEY 【シークレットキー】

# copy files
COPY app.py ${LAMBDA_TASK_ROOT}
COPY main.tf ${LAMBDA_TASK_ROOT}

CMD [ "app.handler" ]

Python

Pythonコードとしては、handle関数をapp.pyに作成すれば良いです
やってることとしては、tfファイルを/tmpにコピーして、terraformを初期化してplanを実行してます
/tmpに移動する理由としては、 $ terraform init を実行したタイミングで、カレントディレクトリにファイルを作成するのですが、上記で説明したように/tmp以外でファイル作成が出来ないので、移動してから実行としています

def handler(event, context): 
    cmd('./', "cp ./main.tf /tmp/")
    cmd('/tmp/', "terraform init --upgrade")
    result = cmd('/tmp/', "terraform plan")
    last_result = extraction(result)
    return last_result

planの結果は下記でaddやchangeの数を正規表現でとってきて、dictで返すようにしてます

def extraction(plan):
    print(plan)
    change_state = {'add': 0, 'change': 0, 'destroy': 0}
    if "No changes" in plan:
        return change_state
    elif "Plan" in plan:
        line_extraction = re.findall("Plan.*", plan)
        result = "".join(line_extraction)

        change_state['add'] = int(re.findall('(\d)\sto\sadd', result)[0])
        change_state['change'] = int(re.findall('(\d)\sto\schange', result)[0])
        change_state['destroy'] = int(re.findall('(\d)\sto\sdestroy', result)[0])
        return change_state
    elif "Error" in plan:
        line_extraction = re.findall("Error.*", plan)
        result = "".join(line_extraction)
        return result
    else:
        result = "予期せぬエラーです。ログを確認して下さい。"
        return result

Terraform

Terraformのコードは簡単にS3バケットの作成にしました

provider "aws" {
  region  = "ap-northeast-1"
}

resource "aws_s3_bucket" "default" {
  bucket = "created-by-lambda"
  acl    = "private"
}

ローカル(Cloud9)での実行

下記のように実行して、コンテナを起動します

$ export REPOSITORY_NAME=【リポジトリ名】
$ docker build -t ${REPOSITORY_NAME} .
$ docker run --rm -p 9000:8080 ${REPOSITORY_NAME}

そして、ローカルの9000番ポートを叩くと、下記のようにS3が作成されるため、addに1が入った状態でかえってきました

$ curl -sd '{}' http://localhost:9000/2015-03-31/functions/function/invocations | jq .
{
  "add": 1,
  "change": 0,
  "destroy": 0
}

ECRへアップ

上記で設定した REPOSITORY_NAME の名前でECRのリポジトリを作成します
その後、下記でイメージをプッシュすると、ECRへイメージがlatestで上がっています

$ export AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account)
$ export REGISTRY_URL=${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${REGISTRY_URL}
$ docker tag ${REPOSITORY_NAME} ${REGISTRY_URL}/${REPOSITORY_NAME}
$ docker push ${REGISTRY_URL}/${REPOSITORY_NAME}

Lambdaで実行

Lambdaの作成

まず実行基盤のLambdaを作成します
といっても、コンソールからポチポチやっていけば良いです
自動化するならsamかCDKが良いかと思います(個人的にはCDK推し)

コンソールからだと、関数の作成からコンテナイメージを選択し、関数名適当に決めてコンテナイメージも「画像を参照」ボタンから対象のリポジトリのlatestを選択すればOKです

一点注意しないといけないのが、今回のTerraformの実行は時間がかかってメモリも多少食うので、下記のように設定してください
タイムアウト:1分
メモリ:4GB

実行

さて、準備が出来たので、ようやくLambdaからコンテナのTerraformを叩いてみましょう
こちらもコンソールから軽くテスト出来るので、こちらで試してみましょう

テストイベントは何でも良いので、デフォのものを利用して実行してみました
すると、下記のようにjsonでTerraformのplan結果が帰ってきました
スクリーンショット 2020-12-15 18.50.57.png
これを利用すれば、外部からこのAPIを叩いて、現状のリソースとの差分がないかを監視することが出来ます

さいごに

Lambdaがコンテナで実行できるようになったので、Terraformを入れて実行してみました
今後は、API Gatewayと連携してAPI化して、Terraformの監視をしたり、planだけじゃなくapplyを実行するアプリも作っていきたいですね

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

AWS アウトプットまとめ

これまで、1週間近くかけて学んできたAWS(S3)の要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

⑪AWSのアカウント作成
https://qiita.com/by-miwa30/items/ca356f6c021eb14fcfef

⑫S3で保存先を用意する
https://qiita.com/by-miwa30/items/dc068e273d8e1d0c1764

⑬EC2の初期設定
https://qiita.com/by-miwa30/items/09c972145fea0e919046

⑭本番環境でデータベースを作成
https://qiita.com/by-miwa30/items/b1a73dbbde0f1bbcfa74

⑮EC2のRailsを起動しよう(手動デプロイ)
https://qiita.com/by-miwa30/items/dd455dcb73d03e19012e

⑯環境変数の設定
https://qiita.com/by-miwa30/items/924a653faef7a03eba36

⑰本番環境Railsの起動エラー(手動)
https://qiita.com/by-miwa30/items/d7a37a9deccbe76c047e

⑱Webサーバーの設定
https://qiita.com/by-miwa30/items/31251da3c26a8129aa60

⑲デプロイを自動化
https://qiita.com/by-miwa30/items/d4b7b1fa3601bf1185ba

⑳IPアドレスにアクセスエラー
https://qiita.com/by-miwa30/items/fbc620dfe6e71d4a417c

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

Amplifyを使って爆速でサイト開発してみた後に読む記事

本記事は AWS Amplify Advent Calendar 2020の18日目の記事です。

はじめに

Amplifyを使って爆速でサイト開発をする系の記事は多く存在しますが、いざ本格的にサイト開発に入るとまだまだ参考になる記事は、多くないように思います。

今回の記事では、そんなこれからAmplifyを本格的に使おうと考えている方へ向けて、これまで書いてきたAmplify記事の総集編とたまった知見の発表的な感じで記事を書いていきます。

AWS自体を全く知らない状態からAmplifyを始めて1年半、本当にAmplifyにはお世話になっていますので、この記事がAmplifyを利用する人の一助になり、少しでも還元になれば幸いです。

購読対象

  • 爆速でサイトを作ってみたので、もっといろんな機能を使ってみたい
  • これからAmplifyを使って本格的にサイト開発を考えている
  • Amplifyの細かいバグにたくさんぶつかって楽しんでいる

Amplify初級〜中級程度の開発者が対象です。

各サービス知見まとめ

今回はAmplifyCLIを中心に、知っていると便利なことからドハマリ情報までまとめました。

Auth

このAuthのおかげで超簡単にAuth周りの設定を行うことができます。まだまだ基本的なことしか使えていないので、もっと深堀りしたいと思っているサービスNo1です。

【具体的な使い方の記事はこちら】
AWS amplify フレームワークの使い方Part1〜Auth設定編〜
AWS Amplify フレームワークの使い方Part2〜Auth実践編〜
AWS Amplify フレームワークの使い方Part13〜Auth 設定更新編〜

初回にしか設定できない項目がある

サインインの方法や属性の設定などは、初回のamplify pushしたあとは変更できませんので、よく考えてから始めましょう。
GUIからどんな項目があって、設定変更が可能なのかを見てみるとAmplifyで何ができるのかイメージしやすいと思います。

GUIで変更はケースバイケース

基本は、GUIでの変更は行わなずにAmlifyCLIを通して行うことが推奨されますが、個人的にAuthについてはバグ回避やGUIならわかるのにamplifyCLIでどうやっていいかわからない場合ちょっとばかし触ってもいいかなと個人的には思っています。

例えば、メッセージ>カスタマイズ>メールアドレスのカスタマイズは、GUIからおこなっても全く問題ありません。

本当はCloudFormationのテンプレートファイルに記載してPUSHするのが一番スマートだとは思っていますが、テンプレートファイルの記載方法を調べる時間をなかなか作れず、さくっとGUIでやってしまっているというのが実情です。

推奨はしませんが、自己責任のもとGUIから操作する選択肢も持っておいてはいいのではないでしょうか。

API

Amplifyを使いこなすための要サービスです。一番お世話になっていて、一番苦しんだこのAPI。使えば使うほど素晴らしさを感じています。

【具体的な使い方の記事はこちら】
AWS Amplify フレームワークの使い方Part3〜API設定編〜
AWS Amplify フレームワークの使い方Part4〜API実践編〜
AWS Amplify フレームワークの使い方Part5〜GraphQL Transform @model編〜
AWS Amplify フレームワークの使い方Part6〜GraphQL Transform @auth編〜
AWS Amplify フレームワークの使い方Part7〜GraphQL Transform @key編〜
AWS Amplify フレームワークの使い方Part8〜GraphQL Transform @connection編〜
AWS Amplify フレームワークの使い方Part16〜GraphQL Transform @function編〜

気軽にスキーマからテーブルを削除しない

何も紐付いていないテーブルの削除であれば何も問題有りませんが、functionに呼び出し権限を与えているなど何かしらamplify内で紐付いている場合、うっかり消してしまった日には、エラー祭りになり、APIを一度全て削除してやり直すしかなくなります。
スキーマからテーブルを削除するときは、amplify内でどこにも紐付いていないことを確認してから、削除するようにしましょう。

このあたり上手く連動して削除されるように早くなってくれないかな、、、。

APIと他のリソースを同時にpushするとgraphql内のqueryらが更新されない

これもいつかは改善されると思っていますが、知らないと地味にハマります。

複数のリソースの更新が重なった時は、$amplify push apiと実行して、APIリソースだけ先に更新しましょう。

複数の認証モードが利用できる

こちらも気づいたら対応していました。以下の4つの認証モードを併用が可能になっています。

  • API Key
  • IAM
  • Cognito User Pool
  • OpenID Connect

会員制のサイトの場合など、割と複数の認証モードは使えると便利なケースは多いと思います。
ただ使い方には、若干クセがあるように感じているので、また1記事くらいにまとめれたいいな、、、

@functionがすばらしい

@functionを知るまでは、Lambda関数については、管理はamplifyで行っていましたが、呼び出しについてはGUIから自作したAPIGatewayで行っていましたが、たった1つの設定でamplifyで呼び出しまで行えるようになるなんて、、、。
@authと併用すれば簡単に権限管理もできますので、本当に便利です。こういった機能が突然サービスに追加になっているので、こまめなチェックが必要だと改めて感じさせられました。

Function

始めの頃は、Lambdaのソースコード管理ってどうしよう、Serverless Frameworkとか使ってやるのかな?といろいろ思っていましたが、こちらを使えば一瞬でした。
アクセス権限も簡単に設定できますし、DynamoDBストリームのトリガー設定やレイヤーの作成など、どんどん使いやすくなっていっています。

【具体的な使い方の記事はこちら】
AWS Amplify フレームワークの使い方Part9〜Function 基礎編〜
AWS Amplify フレームワークの使い方Part11〜Function 権限管理編〜
AWS Amplify フレームワークの使い方Part14〜Lambda レイヤー編〜

関数削除の注意点

このfunctionを使いだした頃から、Amplifyで作成したリソースたちが、お互いのアクセス権限を付与しだします。
関数を削除する時は、必ず他のリソースに削除する関数へのアクセス権限が紐付いていないか確認しましょう。

ライブラリの手動インストール

私はNode.js環境でLambdaを実行していますが、package.jsonに必要なライブラリを記載して、追加した気になることがよくありました。基本的に自動ではインストールしてくれませんので、必ず自分でインストールしてからpushしましょう。(確か初回作成時のみ自動インストールしてくれた気がします。)

レイヤーが熱い

こちらも気づいたら追加されていたサービスの一つで、Lambda関数でのライブラリやコードの共通化を行うことができます。1関数5つまでの制限はありますが、それだけでも相当スッキリしたコードにできるので、本当にすばらしいですね。

私の場合は、割とGUIを使って細かいテストを繰り返し行うことが多いのですが、重たいライブラリを使っているとGUIからコード編集ができないので、ライブラリの共通化はテストのやりやすさにも役立っています。

素晴らしいこと満載のレイヤーですが、トラブルは割と起こりがちなので、気になる方は、上記の参考記事のレイヤー編をご確認ください。

DynamoDBストリームとの連携設定も簡単に

こちらも気づけば、functionの初回作成時にDynamoDBストリームとの紐付け設定ができるようになっておりました。
おそらくAmplifyCLI上では、初回時以外の設定変更項目は今のところない?(みつからない)ので、変更必要な場合はCloudformationファイルを変更する必要がありそうです。

Storage

Amplifyを通して簡単にCognitoユーザーのアクセス権限管理ができるS3を使うことができます。全然S3について知らない状態でもすぐに使えたので、ありがたい限りです。

【具体的な使い方の記事はこちら】
AWS Amplify フレームワークの使い方Part10〜Storage編〜

一定時間が経つとファイルが取得できなくなる

getして取得したURLにはtokenがついており、一定時間が経つと画像ファイルを取得できなくなります。
このURLを保存しておけば、わざわざidentityIdなんて知らなくてもicon画像取得し放題だ!と思ったもののしばらくすると取得できなくなるのでややハマりました。
これにより、identityIdと向き合うことになります。

protectedの場合、identityIdがわからないと取得できない

identityIdは各ユーザーに振られているuserIdとは別のcognitoが管理しているIDです。(ソーシャルログインをしてユーザープールにデータが作成されない人や未承認ユーザーを管理するためのもの?的なイメージ)
そんなあまり身近ではないidentityIdを使わずに、簡単に取得できる方法はないか?、と各方面で議論されているようですが、現状はまだ未実装のようです。
ただ、githubのissueでだいぶ議論されているようなので、そのうちひっそりと実装されている可能性はあるので、要チェックです。

identityIdの取得方法

とりあえずこれで取得できます。

const currentCredentials = await Auth.currentCredentials()
const identityId = currentCredentials.identityId

ENV

Amplifyを使って本気で開発をするのであれば、必須になるのがこちらです。開発環境と本番環境を簡単に分けられるだけでなく、各開発者ごとにバックエンドを簡単に複製できるので、めちゃくちゃ便利です。

【具体的な使い方の記事はこちら】
AWS Amplify フレームワークの使い方Part12〜ENV編〜

amplify env pull xxx(環境名)

多人数で同時開発する時は必ず必須のコマンドです。amplifyをローカル環境で何も触っていない時は、$amplify pullで強制的にローカルのリソースを上書きすればよいですが、触っている時にはそうは行きません。
そんな時に、この$ amplify env pull xxxでpullを行えば、現行のローカルのコードを残して、差分をローカルに上書きしてくれます。

知っていたら当たり前なことですが、このコマンドを知るまでは地味に苦労したことでした。

Hosting

気づいたら、AmplifyConsoleも選択ができるようになっていたこのHosting。私自身AmplifyConsoleを使ったGUIでの作業が中心で、今現在ほとんど触れていませんが、気づいたらどんどんできることが増えていそうなので、時が来たら一度向き合いたいと思っています。

WAFを使いたい

現状AmplifyConsoleでホスティングをした場合、CloudFrontの細かい設定は行えないため、WAFが利用できません。WAFを使いたい場合は、CloudFront+s3を選択してホスティングした上で、CloudFrontの設定を変更しましょう。

Amplify CLI全般

GUI上で各リソースを削除しない

これ絶対にやってはいけないやつです。Cloudformationで管理しているにもかかわらず、GUIで勝手に削除してしまうと、場合によってはすべて削除してAmplifyの設定を一からやり直す必要があります。Amplifyでaddしたものは必ずAmplifyでremoveすることを心掛けてください。

リソースにもよりますが、全く同じ名前で再度GUIから作成すると再認識してくれることがあるので、もしもの時は一度試してもいいかもしれません。

cloudformationのテンプレートに手を加えた時は気をつけて

自動生成されたcloudformationのテンプレートファイルに手書きでコードを足せば、amplifyCLIだけでは実現できない細かい設定まで対応できます。
が、その後にamplifyを使って更新するとその手書きしたコードは消えてしまいますので、注意が必要です。

だれかこの管理のベストプラクティスを知っている人がいたら教えていただきたい、、、。

消したはずのリソーストラブル

amplifyを通してちゃんとリソースを削除したはずなのに、なぜかamplify系ファイルの中にひっそりと残っていてエラーになることがあります。
その場合は、エラー分をよく読み、削除したはずのリソースなどで怒られているときは、その名前で検索をかけて、コード内から削除しましょう。私はこれで何度も救われました。

消した記憶のないコードトラブル

こちらは直近の記憶で起きたのはLambdaレイヤーで、amplify pullをした時などに、本来あるべきはずのコードが消えてしまい、エラーでpushがその後できなくなるということがありました。
この時は、team-provider-info.jsonの差分を確認し、何故か消えているコードをもとに戻してpushすれば直りましたが、原因は未だに不明です。

よくあることではありませんが、何が悪いのかわからないエラーに出会った時は、一度差分を確認してみるといいことがあるかもしれません。

amplify push xxx yyy

リソースを指定してpushができます。例えば、echoFunctionというLambda関数だけ更新したい時は、$ amplify push function echoFunctionと実行すれば、他のリソースは更新されずに、echoFunctionのみ更新されます。地味に知っていると便利です。

--yes (-y)

以下のコマンドに--yes(-y) とつけるだけで、各作業工程のデフォルト値を自動で選択して進めてくれます。
地味ですが、いい活躍をしてくれます。

amplify init
amplify configure project
amplify push
amplify publish
amplify pull

SSR対応

今年ついにAmplifyがSSRに対応しましたね。素晴らしい。
AWS Amplify フレームワークの使い方Part15〜SSR対応導入編〜

AmplifyコンソールもSSR対応が検討されているようなので、こちらのissueはずっと注視しています。
https://github.com/aws-amplify/amplify-console/issues/412

ハマった時はすぐに公式ドキュメントとissueを見る

こちらは当たり前なことではありますが、困ったら公式ドキュメントとissueを見ましょう。
Amplifyはかなりのハイペースで改善と新機能の追加が繰り返されているので、これまでできなかったことができるようになっていたり、誰かがとりあえずの解決策をissueのコメントに上げてくれていることがよくあります。

環境の移動後のpushには注意

基本的には、環境を移動(amplify env checkout)したあとに、pushすれば、別の環境で整えた環境がそのまま同じように出来上がります。
単純な作業後にpushする分には問題有りませんが、たとえばLSIを設定したいから、一度テーブルを削除して再度追加したテーブルがある場合、移動した別環境でも同じことをしなければpushできませんので、そのあたり地味に注意が必要です。

git push/pull と amplify push/pull

何人かで開発をしていると起こりがちなのが、gitとamplifyのどちらかのpush/pullをし忘れによる、過去へのタイムリープ。大事にはならないことが多いですが、昨日作ったはずのリソースが消えている時は、だいたいこれです。
amplifyのpushをする時は、必ずgit pullとamplify pull、gitにpushする時は、事前のamplify pushを忘れずに行いましょう。

それでも人は失敗をする

気をつけていても、バックエンドを壊してしまうことはあるかもしれません。今のところ知っている最速での復旧方法を以下の記事に書いていますので、参考までに。

Amplifyでpush前のバックエンドに強制的に戻す方法

おわりに

本当にどんどん新しいサービスがAmplifyに追加されており、どんどん便利になっていっています。もっとAmplifyの利用者が増えてより活発に開発が進み、さらに便利になることをとても期待しております。

よきAmplifyライフを!!

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

AWS(EC2)要点まとめ その3

これまで、1週間近くかけて学んできたAWS(S3)の要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

  • アプリケーションサーバー(カリキュラムではUnicorn)は、動的コンテンツを生成して処理結果をWebサーバーに返す
  • Webサーバー(カリキュラムではNginx)は、静的コンテンツのみをリクエストとしてクライアントに返す
  • Nginxを起動するには「sudo systemctl start nginx」コマンドを使う
  • データベースを再起動するには「sudo systemctl restart mariadb 」コマンドを使う
  • アプリケーションサーバー(カリキュラムではUnicorn)は、動的コンテンツを生成して処理結果をWebサーバーに返す
  • Webサーバー(カリキュラムではNginx)は、静的コンテンツのみをリクエストとしてクライアントに返す
  • Nginxを再起動するには「sudo systemctl restart nginx」コマンドを使う
  • 自動デプロイは「bundle exec cap production deploy」コマンドを使う スクリーンショット 2020-12-15 21.43.48.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS(EC2)要点まとめ その2

これまで、1週間近くかけて学んできたAWS(S3)の要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

  • 「プロセス」とは、PC上で動く全てのプログラムの実行時の単位
  • 「Swapファイル」とは、メモリの要領を一時的に増やすために準備されるファイル
  • 「sudo」とは、「全部の権限を持った上でコマンドを実行する」という役割のオプション
  • 「grepコマンド」とは、環境変数などを検索するときに使うコマンド
  • 「RAILS_ENV=production」とは、本番環境でコマンド実行する時につくオプション
  • 「アセットファイル」とは、画像・CSS・JavaScript等を管理しているファイル
  • 「psコマンド」とは、現在動いているプロセスを確認するためのコマンド
  • 「killコマンド」とは、現在動いているプロセスを停止させるためのコマンド
  • 「lessコマンド」とは、ファイルの中身を確認する時に使うコマンド
  • 手動デプロイは「unicorn_rails」コマンドを使う
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS(EC2)要点まとめ

a

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

AWS(EC2)要点まとめ その1

これまで、1週間近くかけて学んできたAWS(S3)の要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

  • EC2インスタンス」とは、仮想サーバーの
  • 「AMI」とは、サーバーのデータを丸ごと保存したデータの
  • 「キーペア」とは、インスタンスへ接続する際に必要な秘密鍵の
  • 「Elastic IP」とは、AWSから割り振られた固定のパブリックIPアドレス
  • 「ポート」とは、1つのサーバーと複数のサーバーを繋ぐ役割をするもの

  • MySQL」とは、データの編集・削除などを誰でも無償で利用できるサービス

  • 「MariaDB」とは、MySQLの派生として開発されているオープンソースソフトウェア

  • MariaDBを起動するには、「sudo systemctl start mariadb」コマンドを使う

  • MariaDBの状態を確認するには、「sudo systemctl status mariadb」コマンドを使う

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

AWS(S3)要点まとめ

これまで、1週間近くかけて学んできたAWS(S3)の要点をまとめたので、アウトプットしたいと思います。
間違いがあれば、ご指摘いただければと思います。

  • AWSとは「Amazon Web Servises」の略で、通販で有名なAmazonが提供しているクラウドコンピューティングサービスの総称
  • Amazon S3とは画像などが保存できる、ストレージサービス
  • AWSを利用する際は、情報の漏洩などの防止のために、二段階認証とIAMユーザーを設定する
  • 「git-secrets」とは、pushしたコードをチェックするツール
  • バケットポリシーで、IAMユーザーからのアクセスのみを許可することで、セキュリティ向上につながる
  • 「aws-sdk-s3」というGemを用いると、S3を使用できる スクリーンショット 2020-12-15 21.24.13.png
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

⑳IPアドレスにアクセスエラー(自動デプロイ)ーポートフォリオ

下図のように、ブラウザ上で「We’re sorry , but ~」が表示された場合されました。とりあえずは、EC2のターミナルに接続
スクリーンショット 2020-12-15 21.06.29.png
自動デプロイ後は、currentディレクトリの中にproduction.logが入ってくることに注意

# 自身のアプリケーションのディレクトリに移動
[ec2-user@ip-172-31-23-189~]$ cd /var/www/アプリケーション名

# currentディレクトリに移動
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ cd current

# logディレクトリに移動
[ec2-user@ip-172-31-23-189 current]$ cd log

# logの中に入っているファイルの情報を表示
[ec2-user@ip-172-31-23-189 log]$ ls
production.log  unicorn.stderr.log  unicorn.stdout.log

# production.logの最新のログを10行分表示
[ec2-user@ip-172-31-23-189 log]$ tail -f production.log 

このログを参考にエラーが発生している箇所を探す

私の場合はローカルで修正して、エラーが発生しているのに、本番環境でエラーが起きたと思い込んでいました。落ち着いて、コードを修正したことを思い出し、ローカルでエラーが無くなったことを確認して、コミット→プッシュしたら本番環境でも正常に挙動を確認できました。

いろんな原因が考えられるので、チェックリストを作りました。

チェックリスト

  • ローカルでは問題なく動いているか
  • ローカル→GiuHubへのpushのし忘れはないか(mergeまで確認)
  • GitHubからEC2への反映(git pull origin master)のし忘れはないか
  • EC2サーバー側でエラーログの内容を確認し、原因を見つけたか
  • カリキュラム通りの記載ができているか
  • Nginxは正しく起動しているか
  • EC2サーバー側の環境変数は正しく設定できているか
  • EC2インスタンスの再起動を行ってみる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

⑲デプロイを自動化ーポートフォリオ

手動で行っていたデプロイ作業(「unicorn_railsコマンド」を使ってサーバーを立ち上げる手段)を、ローカルのターミナルからのコマンド1つで行えるようにします。そのために、「自動デプロイツール」と呼ばれるものを利用します。今回は、数ある自動デプロイツールの中でも、もっともポピュラーなCapistranoを実際に導入

Capistrano
「Capistrano」とは、自動デプロイツールと呼ばれるものの一種です。

自動デプロイツールのメリット
自動デプロイツールを利用することによって、デプロイ時に必要なコマンド操作が1回で済むようになります。これにより、手動デプロイする場合に起こりがちな下記の問題を解消できます。

  • コマンドの打ち間違い、手順の間違いが発生する可能性がある
  • 手順が多く、煩わしい

自動デプロイの準備

group :development, :test do
  gem 'capistrano'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano3-unicorn'
end
bundle install
bundle exec cap install

生成されたファイル ↓

Capfile
「Capfile」では、Capistrano関連のライブラリのうちどれを読み込むかを指定できます。
Capistranoの機能を提供するコードはいくつかのライブラリ(Gem)に分かれています。そのため、Capistranoを動かすにはいくつかのライブラリを読み込む必要があります。

production.rb
「production.rb」は、デプロイについての設定を書くファイルです。
GitHubへの接続に必要なsshキーの指定、デプロイ先のサーバのドメイン、AWSサーバへのログインユーザー名、サーバにログインしてからデプロイのために何をするか、といった設定を記載します。
また、「deploy.rb」「staging.rb」も同様の役割をするファイルです。

#サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
app_path = File.expand_path('../../../', __FILE__)  # 「../」が一つ増えている

#アプリケーションサーバの性能を決定する
worker_processes 1

#アプリケーションの設置されているディレクトリを指定
working_directory "#{app_path}/current"  # 「current」を指定

#Unicornの起動に必要なファイルの設置場所を指定
pid "#{app_path}/shared/tmp/pids/unicorn.pid"  # 「shared」の中を参照するよう変更

#ポート番号を指定
listen "#{app_path}/shared/tmp/sockets/unicorn.sock"  # 「shared」の中を参照するよう変更

#エラーのログを記録するファイルを指定
stderr_path "#{app_path}/shared/log/unicorn.stderr.log"  # 「shared」の中を参照するよう変更

#通常のログを記録するファイルを指定
stdout_path "#{app_path}/shared/log/unicorn.stdout.log"  # 「shared」の中を参照するよう変更

Nginxの設定ファイル

 sudo vim /etc/nginx/conf.d/rails.conf
  # Unicornと連携させるための設定
  server unix:/var/www/アプリケーション名/shared/tmp/sockets/unicorn.sock;
}

# {}で囲った部分をブロックと呼ぶ。サーバの設定ができる
server {
  # このプログラムが接続を受け付けるポート番号
  listen 80;
  # 接続を受け付けるリクエストURL ここに書いていないURLではアクセスできない
  server_name Elastic IP;

  # クライアントからアップロードされてくるファイルの容量の上限を2ギガに設定。デフォルトは1メガなので大きめにしておく
  client_max_body_size 2g;

# 接続が来た際のrootディレクトリ
  root /var/www/アプリケーション名/current/public;

# assetsファイル(CSSやJavaScriptのファイルなど)にアクセスが来た際に適用される設定
  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
    root /var/www/アプリケーション名/current/public;
  }

Nginxの設定を変更したら、忘れずに再読込・再起動

[ec2-user@ip-172-31-25-189 ~]$ sudo systemctl reload nginx
[ec2-user@ip-172-31-25-189 ~]$ sudo systemctl restart nginx

データベースの起動を確認

[ec2-user@ip-172-31-25-189 ~]$ sudo systemctl status mariadb 

以下のように、「active」と表示されれば起動しています。
スクリーンショット 2020-12-15 20.52.43.png

自動デプロイを実行

% bundle exec cap production deploy

以下のように表示されれば自動デプロイ成功!
スクリーンショット 2020-12-15 20.58.13.png

ブラウザからElastic IPでアクセス
3.140.60.122
これで、EC2実装完了!

スクリーンショット 2020-12-15 21.02.23.png

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

RDSインスタンスを夜間と休日に止めて節約するLambda&スケジュールをCloudFormationで一撃デプロイする

下記のCloudFormationテンプレートで
- RDSを起動するLambda
- RDSを停止するLambda
- Lambdaを起動するCloudWatchスケジュール
- 必要なIAMロール
を一撃で展開できます。

入力パラメータ

RDSInstanceName : RDSインスタンス名
RDSRegion : RDSのリージョン
RDSStopCron : RDS停止スケジュールのcron式
RDSStartCron : RDS起動スケジュールのcron式

CloudFormationテンプレート

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  RDSInstanceName:
    Type: String
    Default: your instance name
  RDSRegion:
    Type: String
    Default: ap-northeast-1
  RDSStopCron:
    Type: String
    Default: 'cron(00 15 * * ? *)' # 毎日午前15時00分(UTC)= JST深夜0時
  RDSStartCron:
    Type: String
    Default: 'cron(00 00 ? * MON-FRI *)' # 月曜~金曜午前0時00分(UTC)= JST午前9時

Resources:
  StopRdsFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub rds-stop
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt RDSOperationRole.Arn
      Runtime: python3.8
      Timeout: 10
      Environment:
        Variables:
          RDS_REGION: !Ref RDSRegion
          RDS_INSTANCE_NAME: !Ref RDSInstanceName
      Code:
        ZipFile: |
          import boto3
          import os
          region = os.environ['RDS_REGION']
          instance = os.environ['RDS_INSTANCE_NAME']
          def handler(event, context):
              rds = boto3.client('rds', region_name=region)
              rds.stop_db_instance(DBInstanceIdentifier=instance)
              print('started instance: ' + instance)

  RdsStopSchedule:
    Type: AWS::Events::Rule
    Properties:
      Description: rds stop schedule
      ScheduleExpression: !Ref RDSStopCron
      State: ENABLED
      Targets:
        - Arn: !GetAtt StopRdsFunction.Arn
          Id: ScheduleEvent1Target

  RdsStopLambdaEvent:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref StopRdsFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt RdsStopSchedule.Arn

  StartRdsFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub rds-start
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt RDSOperationRole.Arn
      Runtime: python3.8
      Timeout: 10
      Environment:
        Variables:
          RDS_REGION: !Ref RDSRegion
          RDS_INSTANCE_NAME: !Ref RDSInstanceName
      Code:
        ZipFile: |
          import boto3
          import os
          region = os.environ['RDS_REGION']
          instance = os.environ['RDS_INSTANCE_NAME']
          def handler(event, context):
              rds = boto3.client('rds', region_name=region)
              rds.start_db_instance(DBInstanceIdentifier=instance)
              print('started instance: ' + instance)

  RdsStartSchedule:
    Type: AWS::Events::Rule
    Properties:
      Description: rds start schedule
      ScheduleExpression: !Ref RDSStartCron
      State: ENABLED
      Targets:
        - Arn: !GetAtt StartRdsFunction.Arn
          Id: ScheduleEvent1Target

  RdsStartLambdaEvent:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref StartRdsFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt RdsStartSchedule.Arn

  RDSOperationRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub rds-operation-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub rds-operation-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - !Sub arn:aws:rds:${RDSRegion}:156658371598:db:${RDSInstanceName}
                Effect: Allow
                Action:
                  - rds:StopDBInstance
                  - rds:StartDBInstance

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

⑱Webサーバーの設定ーポートフォリオ

Rack
「Rack」とは、翻訳プログラムになります。Rackが翻訳をすることにより、アプリケーションサーバーとアプリケーション本体がコミュニケーションを取ることができ、処理結果をWebサーバーに返すことができます。

アプリケーションサーバーの役割

  • Webサーバから渡されてきた情報を、アプリケーションサーバー内で処理
  • 処理結果を、Webサーバに返す処理

Webサーバーの役割

  • 静的なコンテンツをレスポンスとしてクライアントに返す処理
  • 動的なコンテンツ生成をアプリケーション本体に依頼する処理
  • アプリケーションサーバから返ってくる処理結果をレスポンスとしてクライアントに返す処理

Nginx(エンジン・エックス)
「Nginx」とは、Webサーバーの一種です。ユーザーのリクエストに対して静的コンテンツのみ取り出し処理を行い、動的コンテンツの生成はアプリケーションサーバに依頼します。

スクリーンショット 2020-12-12 19.33.15.png

Nginxを導入

[ec2-user@ip-172-31-25-189 ~]$ sudo amazon-linux-extras install nginx1
[ec2-user@ip-172-31-25-189 ~]$ sudo vim /etc/nginx/conf.d/rails.conf
upstream app_server {
  # Unicornと連携させるための設定
  server unix:/var/www/アプリケーション名/tmp/sockets/unicorn.sock;
}

# {}で囲った部分をブロックと呼ぶ。サーバの設定ができる
server {
  # このプログラムが接続を受け付けるポート番号
  listen 80;
  # 接続を受け付けるリクエストURL ここに書いていないURLではアクセスできない
  server_name Elastic IP;

  # クライアントからアップロードされてくるファイルの容量の上限を2ギガに設定。デフォルトは1メガなので大きめにしておく
  client_max_body_size 2g;

# 接続が来た際のrootディレクトリ
  root /var/www/アプリケーション名/public;

# assetsファイル(CSSやJavaScriptのファイルなど)にアクセスが来た際に適用される設定
  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @unicorn;

  location @unicorn {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }

  error_page 500 502 503 504 /500.html;
}

Unicornの設定を変更

config/unicorn.rb
listen "#{app_path}/tmp/sockets/unicorn.sock"

次は、GitHubの変更点を本番環境へ反映

# 開発中のアプリケーションに移動
[ec2-user@ip-172-31-25-189 ~]$ cd /var/www/開発中のアプリケーション

# GitHubの内容をEC2に反映させる
[ec2-user@ip-172-31-23-189 <レポジトリ名>]$ git pull origin master

IPアドレスにアクセスしてもエラーが出る時
「Railsが起動しない」「ブラウザで確認するとエラーが表示されている」などの問題がある場合、まずは「エラーログ」を確認する必要があります。

「502 but gateway」と出た時の対処方法
こちらのエラーはnginxのlogの確認が必要になります。

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$  sudo less /var/log/nginx/error.log

チェックリスト

  • ローカル→GiuHubへのpushのし忘れはないか(mergeまで確認)
  • GitHubからEC2への反映(git pull origin master)のし忘れはないか
  • EC2サーバー側でエラーログの内容を確認し、原因を見つけたか
  • カリキュラム通りの記載ができているか
  • Nginxは正しく起動しているか
  • EC2インスタンスの再起動を行ってみる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

機械学習でコードレビューするAmazon CodeGuruでPythonがサポートされたので試してみた

はじめに

この記事は ハンズラボ Advent Calendar 2020 22日目の記事です。

こんにちは、@sr-mtmtです。年の瀬ですね。
2020年に買ったものの中でお気に入りは 絶品レンジでパスタ でした。
洗濯機がぐるぐるしてるところを見るのが好きな人は、この商品でレンジの中のヴォルケーノを見つめるのも好きなことでしょう。

さて今年もAdvent Calendar、やってくぞい

re:InventのKeynoteで気になったものを試したい

re:Inventにて、Andy Jassy氏のKeynoteで「機械学習でコードレビューするAmazon CodeGuruでPythonがサポートされた」ときいたので、試してみたいと思います。

前から気になっていたんですが、これまではJavaしか対応していなかったので、Java使っていない弊チームでは試せず。
Javaに比べるとなんとなくPythonの解析って難しそうですがどんなかんじなんでしょう。

Amazon CodeGuruとは

Code GuruにはReviewerとProfilerとあって、Profilerの方はコストチェック、Reviewerの方はバグなどをチェックします。

今回試すのはReviewerです。

Amazon CodeGuru Reviewer は、機械学習でコード内の問題を検出し、その推奨される修正方法を提案してくれるサービスです。
コードの品質に関する問題を、次のように大きく 9 つのカテゴリで識別するらしい。

  • AWS のベストプラクティス: AWS API (ポーリング、ページ区切り、など) の使用方法を修正
  • 同時実行: 機能上の障害を起こしている同期不良、もしくは、パフォーマンスを低下させている過剰な同期などを検出
  • デッドロック: 同時実行されるスレッド間の割り当てをチェック
  • リソースリーク: リソースの処理方法 (データベース接続の解放など) を修正
  • 機密情報のリーク: 個人識別情報 (ログインしているクレジットカードの詳細など) の漏洩を検出
  • 一般的なコード上のバグ: Lambda 関数読み出し時にクライアントを作成していないなどの、発見しづらい問題を検出
  • クローンコード: 統合することでコードの保守性を高められる可能性のある、重複コードを特定
  • 入力検証: 信頼されないソースから送られる、不適当な形態の、もしくは悪意のあるデータをチェック

やってみた

0. 前提

  • AWSアカウントはある
  • レビューしたいソースコードを含むリポジトリをGithubに持っているがCodeCommitには置いてない
  • IAMなどの設定はデフォルトで問題ないリポジトリを選択

1. リポジトリの関連付け

image.png

Reviewerを選択。

image.png
前のリリースのときの情報を完全に忘れてて別にCodeCommitからじゃなくても関連付けできることが判明。\うれしい/
GitHub、GitHub Enterprise、Bitbucket、AWS CodeCommitの中から好きなものを選択できます。

image.png
組織に紐づくリポジトリへのアクセスは矢印の「Grant」を押しておかないと許可されないので注意。
今回はお試しなので私の個人アカウントでやってますがこのあたりを丁寧に準備すれば権限細かく設定できそうなのもいいですね。

これでGithubアカウントに接続したので、さっきグレーアウトしていた「リポジトリの場所」に候補のリポジトリがわさーっと出てくるようになりました。
適当なリポジトリを選択して関連付けます。

2. 分析してみる

分析方法ですが2種類あります。

コードレビュータイプ 自動でレビューされるか レビュー結果はどこで見られるか レビュー対象
リポジトリの分析 No

CodeGuru Reviewerのマネジメントコンソール、またはAWS CLIまたはAWS SDKを使用して分析を実行する必要があります
CodeGuru Reviewerのマネジメントコンソール、またはAWS CLIまたはAWS SDKを使用して確認する ブランチ内のすべてのコード
プルリクエスト Yes

リポジトリを関連付けた後、プルリクエストを行うたびに自動でコードレビューが行われます
CodeGuru Reviewerのマネジメントコンソール、またはAWS CLIまたはAWS SDKを使用して確認する方法の他に、リポジトリのソースプロバイダ(Githubなど)のPRコメントからも確認可能 プルリクエストの範囲。今回変更されたコードのみ

image.png
プルリクエストがあがれば↑ここに追加されていくようです。
今はちょうどいいPRがないので「リポジトリの分析」の方でコード全体の分析をやってみます。

image.png
「リポジトリの分析を作成」を押します。

image.png
「コードとセキュリティの推奨事項」の方はさっき関連付けたリポジトリとはまた別の話になります。
別途S3にzip化したコードをアップロードして、それと関連付けた上で分析することになるので今回は「コードの推奨事項」を試したいと思います。

image.png
分析中・・・

image.png
小さいリポジトリだからか数分で完了!
さて、なにが上がってくるのかな・・・?

image.png
なんもない :joy: :joy: :joy:
いや、いいことですけどね。
寂しいのでもう少し大きいリポジトリでも試したのですがやはり何も見つかりませんでした・・・。
パフォーマンス低下とかどのくらいのことから指摘されるんだろう・・・。

image.png
ちなみにプルリクエストの方にCodeGuruから指摘事項があった場合、こんなかんじでプルリクコメントにそのまま推奨事項が役に立つものだったかフィードバックが送れるそうです。
(公式ドキュメントの画像を拝借)

ここで指摘されてる推奨事項は
"This code might not produce accurate results if the operation returns paginated results instead of all results. Consider adding another call to check for additional results."
このコードは、操作がすべての結果ではなくページ分割された結果を返す場合、正確な結果が得られない可能性があります。追加の結果をチェックするために別の呼び出しを追加することを検討してください。 (deepl翻訳)
となっていますね。AWS APIの使用方法に関する指摘かな。

おまけ)リポジトリの関連付け解除

image.png
リポジトリを紐付けっぱなしにしていると無料枠超えたときにお金がかかるので必要に応じて解除しましょう。

解除すると元には戻せないので改めて新規で関連付ける必要があります。
image.png

解除したリポジトリは初期表示画面からは消えるのですが、左上の「リポジトリ(3)」となっているように、完全に消えるわけではなく、
関連付けが解除されたリポジトリから確認することが可能です。(どうして)

料金プラン

Amazon CodeGuru がサポートされている AWS リージョンで 90 日間無料でお試し可能です。
下記は東京リージョンの場合。

無料枠について

リポジトリの全体の分析は、各支払者アカウントごとに毎月 30,000 行のコードの分析までが無料となります。
プルリクエストの分析は、90 日間の無料トライアルが利用可能。

リポジトリ全体の分析

無料利用枠以上の利用だと毎月 1,500,000 行のコードの分析までは、コード 100 行ごとに 0.50USD。
1,500,000 行以上のコードを分析した場合 コード 100 行ごとに 0.40USD。
リポジトリ全体または選択したコードブランチのソースコードを分析することができます。
選択したリポジトリまたはソースコードブランチ内のコードの全行が、リポジトリ分析の実行ごとに分析されます。

プルリクエスト

90 日間の無料トライアル後、コード 100 行ごとに 0.75USD。

あとがき

思った以上に設定簡単でしたね。何かしらで詰まるかなと思って身構えてたんですがあまりにも簡単。
CodeCommit以外からも関連付けられるのが特にいいですね。
コードへの指摘事項が出てこず残念でしたが大きな問題はないのかなとちょっとほっとできました。

明日は23日目! @fasahina さんです!

FYI

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

新機能QuickSight Qとは??

はじめに

この記事は株式会社ナレッジコミュニケーションが運営する Amazon AI by ナレコム Advent Calendar 2020 の 15日目にあたる記事になります。

現在開催中のre:Invent2020にて発表された新サービス「QuickSight Q」について、調べたことを簡単に書いていきます。

そもそも「QuickSight」とは?

QuickSightとは、AWSが提供するBIツールであり、RDSやS3など様々なデータソースと接続し可視化&分析することができます。
機械学習ベースの異常検知、予測、自動ナラティブ(サマリ文章作成)機能が備わっており、専門家でなくても非常に使いやすい仕様となっているのが特徴です。

本題「QuickSight Q」とは?

QuickSight Qとは、QuickSightに新たに搭載された機械学習(ML)を利用した自然言語クエリ(NLQ ※Natural Language Queryの略)機能のことです。
これによりQuickSightを使用して日常の言語でデータに関する質問を尋ねることで、数秒で正確な回答を得ることが出来るようになりました。
例えば、「昨年と比較して売上高の伸びはどうなった?」や「昨年と比較して最も売上高が伸びた製品は?」といった質問に対して、
Qが自然言語処理を行って質問の意図を解釈し、QuickSightで数値、グラフ、テーブルの形で質問に対する回答を返すことができます。
以下の例では「show monthly gaming revenue for california for last 3 years(過去3年間のカリフォルニアのゲーム収益を表示して!)」というNLQを投げています。
image.png

image.png

従来であれば主要な指標や監視したい項目ごとにダッシュボードを作成するのが一般的であったため、意思決定者からダッシュボードに見つからない質問がされた際はその都度新たにダッシュボードを作成する必要がありましたが、このQ機能によってこのようなシーンにも即座に対応が可能となります。

使用に関して

Qのプレビューは、米国東部 (バージニア北部)、米国西部 (オレゴン)、米国東部 (オハイオ) および 欧州 (アイルランド) で利用可能とのことですので、現状対応している言語は英語のみだと思われます。(そもそも日本語の自然言語処理は難しいので日本語対応は少し先になるかもしれません、、、)
Qの使用を開始するにはまずはQuickSightを使用しているという前提が必要となりますので、まだ使用していないという方は以下から以下を参考にQuickSightを始めましょう。
https://docs.aws.amazon.com/ja_jp/quicksight/latest/user/example-create-an-analysis.html
QuickSight Qの詳細な使用手順に関しては以下URL記載の内容を参照いただければと思います。
https://aws.amazon.com/jp/blogs/news/amazon-quicksight-q-to-answer-ad-hoc-business-questions/

終わりに

本記事ではQuickSightの追加機能である、QuickSight Qについて簡単にご紹介いたしました。
日常で使用している言語(現時点では英語のみですが)を使用してデータ活用ができるという非常に魅力的な機能でした。
そのうち音声で同様のことができる日も近いかもしれません。
BIツールに悩まれている方はぜひ、QuickSightを検討してみてはいかがでしょうか?

参考記事
https://aws.amazon.com/jp/blogs/news/amazon-quicksight-q-to-answer-ad-hoc-business-questions/

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

ECS + CodePipline でコンテナ管理

アプリケーションを作る際にコンテナを使う人も多くなってきたのではないでしょうか。
今回は、CodeCommitにDockerfileをアップロードしたらBuildして自動でECSのタスクが更新される仕組みをコード化したいと思います。

インフラをコードで管理するためにCloudFormationを使用します。awslabsが公開しているコードを参照し、Cloudformationで作成します。いずれTerraFormでも作りたいと思います。

awslabs
https://github.com/awslabs/ecs-refarch-continuous-deployment

今回作成するアーキテクチャは以下のようになります。

qiita-cfn.png

EC2かFargateかはスタックを作る際に選択可能な形となります。なお、スタック作成時はAWS公式のdocker image(php-sample)を使用しますので、他のimageを使用したい場合にはDockerfileをCodeCommitにアップロードしてください。

(なお、PiplineにGithubを使用したいという方はawslabsのコードをご参照ください。)

ディレクトリ構造は下記となります。templatesフォルダにある5つのyamlはS3にアップロードしてご使用ください。

├── ecs-refarch-continuous-deployment.yaml
├── tempaltes
    ├── vpc.yaml
    ├── load-balancer.yaml
    ├── ecs-cluster.yaml
    ├── service.yaml
    └── deployment-pipline.yaml

まずはVPCからです。

---
AWSTemplateFormatVersion: 2010-09-09


Parameters:
  Name:
    Type: String

  VpcCIDR:
    Type: String

  Subnet1CIDR:
    Type: String

  Subnet2CIDR:
    Type: String


Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      Tags:
        - Key: Name
          Value: !Ref Name

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref Name

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

  Subnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 0, !GetAZs ]
      MapPublicIpOnLaunch: true
      CidrBlock: !Ref Subnet1CIDR
      Tags:
        - Key: Name
          Value: !Sub ${Name} (Public)

  Subnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [ 1, !GetAZs ]
      MapPublicIpOnLaunch: true
      CidrBlock: !Ref Subnet2CIDR
      Tags:
        - Key: Name
          Value: !Sub ${Name} (Public)

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Ref Name

  DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  Subnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref Subnet1

  Subnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref RouteTable
      SubnetId: !Ref Subnet2


Outputs:
  Subnets:
    Value: !Join [ ",", [ !Ref Subnet1, !Ref Subnet2 ] ]
  VpcId:
    Value: !Ref VPC

次にLoadBalancer(ALB)です。NLBのTemplateもありますので、見たいという方はコメントしてください!

Parameters:
  LaunchType:
    Type: String
    Default: Fargate
    AllowedValues:
      - Fargate
      - EC2

  Subnets:
    Type: List<AWS::EC2::Subnet::Id>

  VpcId:
    Type: String


Conditions:
  EC2: !Equals [ !Ref LaunchType, "EC2" ]


Resources:
  SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      GroupDescription: !Sub ${AWS::StackName}-alb
      SecurityGroupIngress:
        - CidrIp: "0.0.0.0/0"
          IpProtocol: tcp
          FromPort: 80
          ToPort: 80
      VpcId: !Ref VpcId

  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Subnets: !Ref Subnets
      SecurityGroups:
        - !Ref SecurityGroup

  LoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn: LoadBalancer
    Properties:
      VpcId: !Ref VpcId
      Port: 80
      Protocol: HTTP
      Matcher:
        HttpCode: 200-299
      HealthCheckIntervalSeconds: 10
      HealthCheckPath: /
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 2
      TargetType: !If [ EC2, "instance", "ip" ]
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 30

  ListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      ListenerArn: !Ref LoadBalancerListener
      Priority: 1
      Conditions:
        - Field: path-pattern
          Values:
            - /
      Actions:
        - TargetGroupArn: !Ref TargetGroup
          Type: forward


Outputs:
  TargetGroup:
    Value: !Ref TargetGroup

  ServiceUrl:
    Description: URL of the load balancer for the sample service.
    Value: !Sub http://${LoadBalancer.DNSName}

  SecurityGroup:
    Value: !Ref SecurityGroup

次にCluseterです。今回はAMIはマッピングを利用しました。SSMを使用して最新のAMIを取得してくることもできますが、リージョン別に自動選択したかったので採用しませんでした。

Parameters:
  LaunchType:
    Type: String
    Default: Fargate
    AllowedValues:
      - Fargate
      - EC2

  InstanceType:
    Type: String
    Default: t2.micro

  ClusterSize:
    Type: Number
    Default: 2

  Subnets:
    Type: List<AWS::EC2::Subnet::Id>

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup::Id

  VpcId:
    Type: AWS::EC2::VPC::Id

  KeyName:
    Description: The EC2 Key Pair to allow SSH access to the instance
    Type: "AWS::EC2::KeyPair::KeyName"


Conditions:
  EC2: !Equals [ !Ref LaunchType, "EC2" ]

Mappings:
  AWSRegionToAMI:
    ap-south-1:
      AMI: ami-00491f6f
    eu-west-3:
      AMI: ami-9aef59e7
    eu-west-2:
      AMI: ami-67cbd003
    eu-west-1:
      AMI: ami-1d46df64
    ap-northeast-2:
      AMI: ami-c212b2ac
    ap-northeast-1:
      AMI: ami-872c4ae1
    sa-east-1:
      AMI: ami-af521fc3
    ca-central-1:
      AMI: ami-435bde27
    ap-southeast-1:
      AMI: ami-910d72ed
    ap-southeast-2:
      AMI: ami-58bb443a
    eu-central-1:
      AMI: ami-509a053f
    us-east-1:
      AMI: ami-28456852
    us-east-2:
      AMI: ami-ce1c36ab
    us-west-1:
      AMI: ami-74262414
    us-west-2:
      AMI: ami-decc7fa6

Resources:
  EC2Role:
    Type: AWS::IAM::Role
    Condition: EC2
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role

  InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Condition: EC2
    Properties:
      Path: /
      Roles:
        - !Ref EC2Role

  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Ref AWS::StackName

  AutoScalingGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Condition: EC2
    Properties:
      VPCZoneIdentifier: !Ref Subnets
      LaunchConfigurationName: !Ref LaunchConfiguration
      MinSize: !Ref ClusterSize
      MaxSize: !Ref ClusterSize
      DesiredCapacity: !Ref ClusterSize
      Tags: 
        - Key: Name
          Value: !Sub ${AWS::StackName} - ECS Host
          PropagateAtLaunch: true
    CreationPolicy:
      ResourceSignal:
        Timeout: PT15M
    UpdatePolicy:
      AutoScalingRollingUpdate:
        MinInstancesInService: 1
        MaxBatchSize: 1
        PauseTime: PT15M
        WaitOnResourceSignals: true

  LaunchConfiguration:
    Type: AWS::AutoScaling::LaunchConfiguration
    Condition: EC2
    Metadata:
      AWS::CloudFormation::Init:
        config:
          commands:
            01_add_instance_to_cluster:
                command: !Sub echo ECS_CLUSTER=${Cluster} > /etc/ecs/ecs.config
          files:
            "/etc/cfn/cfn-hup.conf":
              mode: 000400
              owner: root
              group: root
              content: !Sub |
                [main]
                stack=${AWS::StackId}
                region=${AWS::Region}
            "/etc/cfn/hooks.d/cfn-auto-reloader.conf":
              content: !Sub |
                [cfn-auto-reloader-hook]
                triggers=post.update
                path=Resources.ContainerInstances.Metadata.AWS::CloudFormation::Init
                action=/opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource LaunchConfiguration
          services:
            sysvinit:
              cfn-hup:
                enabled: true
                ensureRunning: true
                files:
                  - /etc/cfn/cfn-hup.conf
                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
    Properties:
      ImageId: !FindInMap [ AWSRegionToAMI, !Ref "AWS::Region", AMI ]
      InstanceType: !Ref InstanceType
      IamInstanceProfile: !Ref InstanceProfile
      SecurityGroups:
        - !Ref SecurityGroup
      KeyName: !Ref KeyName
      UserData:
        "Fn::Base64": !Sub |
          #!/bin/bash
          yum install -y aws-cfn-bootstrap
          /opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource LaunchConfiguration
          /opt/aws/bin/cfn-signal -e $? --region ${AWS::Region} --stack ${AWS::StackName} --resource AutoScalingGroup

Outputs:
  ClusterName:
      Value: !Ref Cluster

次にServiceです。

Parameters:
  Cluster:
    Type: String

  DesiredCount:
    Type: Number
    Default: 1

  LaunchType:
    Type: String
    Default: Fargate
    AllowedValues:
      - Fargate
      - EC2

  TargetGroup:
    Type: String

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup::Id

  Subnets:
    Type: List<AWS::EC2::Subnet::Id>


Conditions:
  Fargate: !Equals [ !Ref LaunchType, "Fargate" ]

  EC2: !Equals [ !Ref LaunchType, "EC2" ]


Resources:
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /ecs/${AWS::StackName}
      RetentionInDays: 14

  FargateService:
    Type: AWS::ECS::Service
    Condition: Fargate
    Properties:
      Cluster: !Ref Cluster
      DesiredCount: !Ref DesiredCount
      TaskDefinition: !Ref TaskDefinition
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref SecurityGroup
          Subnets: !Ref Subnets
      LoadBalancers:
        - ContainerName: simple-app
          ContainerPort: 80
          TargetGroupArn: !Ref TargetGroup

  EC2Service:
    Type: AWS::ECS::Service
    Condition: EC2
    Properties:
      Cluster: !Ref Cluster
      DesiredCount: !Ref DesiredCount
      TaskDefinition: !Ref TaskDefinition
      LaunchType: EC2
      LoadBalancers:
        - ContainerName: simple-app
          ContainerPort: 80
          TargetGroupArn: !Ref TargetGroup

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Sub ${AWS::StackName}-simple-app
      RequiresCompatibilities:
        - !If [ Fargate, "FARGATE", "EC2" ]
      Memory: 512
      Cpu: 256
      NetworkMode: !If [ Fargate, "awsvpc", "bridge" ]
      ExecutionRoleArn: !Ref TaskExecutionRole
      ContainerDefinitions:
        - Name: simple-app
          Image: amazon/amazon-ecs-sample
          Essential: true
          Memory: 256
          MountPoints:
            - SourceVolume: my-vol
              ContainerPath: /var/www/my-vol
          PortMappings:
            - HostPort: 80
              ContainerPort: 80
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-region: !Ref AWS::Region
              awslogs-group: !Ref LogGroup
              awslogs-stream-prefix: !Ref AWS::StackName
        - Name: busybox
          Image: busybox
          EntryPoint:
            - sh
            - -c
          Essential: true
          Memory: 256
          VolumesFrom:
            - SourceContainer: simple-app
          Command:
            - /bin/sh -c "while true; do /bin/date > /var/www/my-vol/date; sleep 1; done"
      Volumes:
        - Name: my-vol


Outputs:
  Service:
    Value: !If [ Fargate, !Ref FargateService, !Ref EC2Service ]

次にパイプラインの構築です。

Parameters:
  CodeCommitRepositoryName:
    Type: String

  Cluster:
    Type: String

  Service:
    Type: String


Resources:
  Repository:
    Type: AWS::ECR::Repository
    DeletionPolicy: Retain

  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource: "*"
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - ecr:GetAuthorizationToken
                  - secretsmanager:GetSecretValue
              - Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:GetObjectVersion
              - Resource: !Sub arn:aws:ecr:${AWS::Region}:${AWS::AccountId}:repository/${Repository}
                Effect: Allow
                Action:
                  - ecr:GetDownloadUrlForLayer
                  - ecr:BatchGetImage
                  - ecr:BatchCheckLayerAvailability
                  - ecr:PutImage
                  - ecr:InitiateLayerUpload
                  - ecr:UploadLayerPart
                  - ecr:CompleteLayerUpload

  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Resource: "*"
                Effect: Allow
                Action:
                  - ecs:DescribeServices
                  - ecs:DescribeTaskDefinition
                  - ecs:DescribeTasks
                  - ecs:ListTasks
                  - ecs:RegisterTaskDefinition
                  - ecs:UpdateService
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuilds
                  - iam:PassRole

  ArtifactBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain

  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          phases:
            pre_build:
              commands:
                - $(aws ecr get-login --no-include-email)
                - TAG="$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | head -c 8)"
                - IMAGE_URI="${REPOSITORY_URI}:${TAG}"
            build:
              commands:
                - docker build --tag "$IMAGE_URI" .
            post_build:
              commands:
                - docker push "$IMAGE_URI"
                - printf '[{"name":"simple-app","imageUri":"%s"}]' "$IMAGE_URI" > images.json
          artifacts:
            files: images.json
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/docker:17.09.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}
      Name: !Ref AWS::StackName
      ServiceRole: !Ref CodeBuildServiceRole

  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: App
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration:
                RepositoryName: !Ref CodeCommitRepositoryName
                BranchName: master
              RunOrder: 1
              OutputArtifacts:
                - Name: App
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              InputArtifacts:
                - Name: App
              OutputArtifacts:
                - Name: BuildOutput
              RunOrder: 1
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: ECS
              Configuration:
                ClusterName: !Ref Cluster
                ServiceName: !Ref Service
                FileName: images.json
              InputArtifacts:
                - Name: BuildOutput
              RunOrder: 1


Outputs:
  PipelineUrl:
    Value: !Sub https://console.aws.amazon.com/codepipeline/home?region=${AWS::Region}#/view/${Pipeline}

以上5つのファイルをS3にアップロードしてください。
最後にまとめのテンプレートです。

Parameters:
  LaunchType:
    Type: String
    Default: Fargate
    AllowedValues:
      - Fargate
      - EC2
    Description: >
      The launch type for your service. Selecting EC2 will create an Auto
      Scaling group of t2.micro instances for your cluster. See
      https://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_types.html
      to learn more about launch types.

  TemplateBucket:
    Type: String
    Description: >
      The S3 bucket from which to fetch the templates used by this stack.

  CodeCommitRepositoryName:
    Type: String

  ClusterSize:
    Type: Number
    Default: 2

  DesiredCount:
    Type: Number
    Default: 1

  KeyName:
    Description: The EC2 Key Pair to allow SSH access to the instance
    Type: "AWS::EC2::KeyPair::KeyName"

Metadata:
  AWS::CloudFormation::Interface:
    ParameterLabels:
      CodeCommitRepositoryName:
        default: "CodeCommitRepositoryName"
      LaunchType:
        default: "Launch Type"
    ParameterGroups:
      - Label:
          default: Cluster Configuration
        Parameters:
          - LaunchType
      - Label:
          default: CodeCommit Configuration
        Parameters:
          - CodeCommitRepositoryName
      - Label:
          default: Stack Configuration
        Parameters:
          - TemplateBucket
      - Label:
          default: TaskDesiredCount
        Parameters:
          - DesiredCount

Resources:
  Cluster:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "https://s3.amazonaws.com/${TemplateBucket}/ecs-cluster.yaml"
      Parameters:
        LaunchType: !Ref LaunchType
        SecurityGroup: !GetAtt LoadBalancer.Outputs.SecurityGroup
        Subnets: !GetAtt VPC.Outputs.Subnets
        VpcId: !GetAtt VPC.Outputs.VpcId
        ClusterSize: !Ref ClusterSize
        KeyName: !Ref KeyName

  DeploymentPipeline:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "https://s3.amazonaws.com/${TemplateBucket}/deployment-pipeline.yaml"
      Parameters:
        Cluster: !GetAtt Cluster.Outputs.ClusterName
        Service: !GetAtt Service.Outputs.Service
        CodeCommitRepositoryName: !Ref CodeCommitRepositoryName

  LoadBalancer:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "https://s3.amazonaws.com/${TemplateBucket}/load-balancer.yaml"
      Parameters:
        LaunchType: !Ref LaunchType
        Subnets: !GetAtt VPC.Outputs.Subnets
        VpcId: !GetAtt VPC.Outputs.VpcId

  VPC:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "https://s3.amazonaws.com/${TemplateBucket}/vpc.yaml"
      Parameters:
        Name: !Ref AWS::StackName
        VpcCIDR: 10.215.0.0/16
        Subnet1CIDR: 10.215.10.0/24
        Subnet2CIDR: 10.215.20.0/24

  Service:
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: !Sub "https://s3.amazonaws.com/${TemplateBucket}/service.yaml"
      Parameters:
        Cluster: !GetAtt Cluster.Outputs.ClusterName
        LaunchType: !Ref LaunchType
        TargetGroup: !GetAtt LoadBalancer.Outputs.TargetGroup
        SecurityGroup: !GetAtt LoadBalancer.Outputs.SecurityGroup
        Subnets: !GetAtt VPC.Outputs.Subnets
        DesiredCount: !Ref DesiredCount

Outputs:
  ServiceUrl:
    Description: The sample service that is being continuously deployed.
    Value: !GetAtt LoadBalancer.Outputs.ServiceUrl

  PipelineUrl:
    Description: The continuous deployment pipeline in the AWS Management Console.
    Value: !GetAtt DeploymentPipeline.Outputs.PipelineUrl

以上となります。最後までご覧いただきありがとうございます。

参照記事
https://qiita.com/chisso/items/3c4dd1af0382d4978288
https://github.com/awslabs/ecs-refarch-continuous-deployment

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

CloudWatchアラームをLambdaでGoogle Chatに投稿する

概要

CloudWatch メトリクスアラームを使用し AWS リソースの監視を開始する上で、通知を Google Chat にも投稿する必要が出てきたため、Lambda ファンクションを使用し、 SNS をトリガーにして Google Chat 連携を行いました。
本記事では、AWS CDK を利用して SNS トピックの作成 および、lambda リソースの作成を行い、 SNS のテストトピックを発行し、 Google Chatに通知が来るまで確認してみたいと思います。
実際の運用では、監視対象の CloudWatch メトリクスアラーム と 上記で作成した SNSトピックの紐付けを行うことで、アラーム発砲から Google Chat 通知までの一連の流れを実現することができます。

前提

・ Goole Chatのチャットルームを作成し、Webhook の URLを取得している
・ AWS CDK デプロイ環境が整っている

AWS CDK にて SNS/Lambda リソースの作成

実装は以下になります。
CDK context を利用して、 Webhook の URL を渡してあげて、 Lambda の環境変数に展開しています。。

DevopsAlarmStack.ts
import * as cdk from "@aws-cdk/core";
import * as sns from "@aws-cdk/aws-sns";
import * as iam from "@aws-cdk/aws-iam";
import * as lambda from "@aws-cdk/aws-lambda";
import * as subscriptions from "@aws-cdk/aws-sns-subscriptions";

export class DevopsAlarmStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const webhookURL = this.node.tryGetContext("webhookURL") || "";

    // lambda用ロール作成
    const lambdaIamRole = new iam.Role(
      this,
      "test-iam-role-for-lambda-devops-alarm",
      {
        roleName: "test-iam-role-for-lambda-devops-alarm",
        assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
        managedPolicies: [
          iam.ManagedPolicy.fromAwsManagedPolicyName(
            "service-role/AWSLambdaBasicExecutionRole"
          ),
        ],
      }
    );

    // SNS トピック作成
    const myTopic = new sns.Topic(this, "test-alarm-topic", {
      displayName: "test-alarm-topic",
      topicName: "test-alarm-topic",
    });

    // 通知用 lambda 作成
    const myFunction = new lambda.Function(
      this,
      "test-lambda-alarm-handler",
      {
        code: lambda.Code.fromAsset("../src", {
          exclude: ["*.ts", "cdk.out"],
        }),
        handler: "AlarmHandler.handler",
        functionName: "test-lambda-alarm-handler",
        runtime: lambda.Runtime.NODEJS_12_X,
        role: lambdaIamRole,
        environment: {
          HANGOUT_WEBHOOK_URL: `${webhookURL}`,
        },
      }
    );

    // SNS と lambda の紐付け
    myTopic.addSubscription(new subscriptions.LambdaSubscription(myFunction));
  }
}

Lambda 内関数の実装

Lambda 内の実装は以下になります。
Lambda で展開された環境変数から HANGOUT_WEBHOOK_URL を取得し、利用しています。
SNS から渡ってきたイベントを、見やすいように整形して、Google Chat Webhook API を利用し、通知してあげます。

AlarmHandler.ts
const fetch = require("node-fetch");

const webhookURL = process.env.HANGOUT_WEBHOOK_URL;

export const handler = async (event: any) => {
  const text = get_text(get_message_from_event(event));

  console.log(text);
  await fetch(webhookURL, {
    method: "POST",
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
    },
    body: JSON.stringify({
      text: text,
    }),
  }).then((response: any) => {
    console.log(response);
  });
};

function get_message_from_event(event: any) {
  return JSON.parse(event.Records[0].Sns.Message);
}

function get_text(message: any) {
  const alarm_name = "AlarmName" in message ? message["AlarmName"] : "";
  const old_state = "OldStateValue" in message ? message["OldStateValue"] : "";
  const new_state = "NewStateValue" in message ? message["NewStateValue"] : "";
  const new_state_reason =
    "NewStateReason" in message ? message["NewStateReason"] : "";
  return (
    "*" +
    alarm_name +
    ":* " +
    old_state +
    "" +
    new_state +
    "\n```" +
    new_state_reason +
    "```"
  );
}

SNS テストトピックの発行

AWS コンソールにて下記の json メッセージを発行します。
[Amazon SNS] - [トピック] - [上記のCDKにて作成したトピックを選択] - [メッセージの発行]

message.json
{
    "AlarmName": "sample-error",
    "AlarmDescription": "sampleでエラーが発生しました。",
    "AWSAccountId": "xxxxxxxxxxxx",
    "NewStateValue": "ALARM",
    "NewStateReason": "Threshold Crossed: 1 datapoint [2.0 (29/11/17 01:09:00)] was greater than or equal to the threshold (1.0).",
    "StateChangeTime": "2017-11-29T01:10:32.907+0000",
    "Region": "Asia Pacific (Tokyo)",
    "OldStateValue": "OK",
    "Trigger": {
        "MetricName": "sample-metric",
        "Namespace": "LogMetrics",
        "StatisticType": "Statistic",
        "Statistic": "SUM",
        "Unit": null,
        "Dimensions": [],
        "Period": 60,
        "EvaluationPeriods": 1,
        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
        "Threshold": 1,
        "TreatMissingData": "- TreatMissingData:                    NonBreaching",
        "EvaluateLowSampleCountPercentile": ""
    }
}

※ サンプルのJSONデータは、https://qiita.com/onooooo/items/f59c69e30dc5b477f9fd を参考にさせていただきました。

Google Chat での確認

Google Chat に以下のような形で、通知を取得することができました。

スクリーンショット 2020-12-15 18.36.01.png

まとめ

AWS CDK を利用して SNS トピックの作成 および、lambda リソースの作成を行い、 SNS のテストトピックを発行し、 Google Chatに通知が来るまでを確認してみました。普段 Google Chat を利用されている方は、AWSリソースに何か生じた際にすぐ通知が来てくれるので便利だと思います。

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

⑰本番環境Railsの起動エラー(手動)ーポートフォリオ

「Railsが起動しない」「ブラウザで確認するとエラーが表示されている」などの問題がある場合、まずは「エラーログ」を確認する必要があります。

ターミナルで「unicorn_rails」が起動しない時の対処法

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ RAILS_SERVE_STATIC_FILES=1 unicorn_rails -c config/unicorn.rb -E production -D
master failed to start, check stderr log for details

このように、unicorn_railsを実行した際に「master failed to start, check stderr log for details」と出た場合、unicornのエラーログを確認する必要があります。

「check stderr log for details」というのは、「詳細(detail)はstderr logを確認(check)しましょう」という意味です。

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ less log/unicorn.stderr.log

スクリーンショット 2020-12-15 18.32.15.png

ログファイルは「一番下から最新のログ」が表示されます。”「shiftキー」+「G」”を実行すると、一番下まで一瞬で移動できます。

ブラウザに「We’re sorry, but 〜」と表示されている時の対処法

このような表示が出ている場合、まずは「production.log」を確認する必要があります。

production.log
「production.log」とは、サーバーログを記録する場所で、いわば「EC2内での出来事を記録している場所」です。ローカルにおいてrails sコマンドでアプリケーションを実行したときも、さまざまなログが表示されます。

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ less log/production.log

スクリーンショット 2020-12-15 18.30.56.png

tail -fコマンド
「tail -fコマンド」とは、最新のログを10行分だけ表示してくれるコマンドです。
スクリーンショット 2020-12-15 18.34.28.png

チェックリスト

  • ローカル→GiuHubへのpushのし忘れはないか(mergeまで確認)
  • GitHubからEC2への反映(git pull origin master)のし忘れはないか
  • EC2サーバー側でエラーログの内容を確認し、原因を見つけたか
  • カリキュラム通りの記載ができているか
  • データベースは正しく起動しているか
  • EC2サーバー側の環境変数は正しく設定できているか
  • EC2インスタンスの再起動を行ってみる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[無料] AWS EC2 + RDS + Flask でWebアプリを作り HTTPS 化する [データベース編 その1]

Webアプリで使うためのリレーショナルデータベースを AWS RDS で作成します。私はスキーマレスなNoSQLにしか興味ない、という人は読み飛ばして下さい。そのうち無料で使えるキーバリューストア型データベースの DynamoDB との比較記事も書くかもしれません。

インスタンス作成

Amazon RDS のページに入り、真ん中らへんの Create database します。
Screen Shot 2563-12-15 at 14.26.03.png
Standard create で良いです。
Screen Shot 2563-12-15 at 14.28.18.png
エンジンを選びます。主要なものは全て揃っています、MySQL、MariaDB、PostgreSQL、Microsoft SQL Server の4つが無料で、Oracle と Amazon Aurora は無料枠対象外です。Aurora は MySQL/Postgre 完全互換で、本家よりも数倍速いとの謳い文句ですので、興味がある方は試してみると良いかもしれません。ただし、バージョンが限られることに気をつけて下さい。私はリレーショナルデータベースをあえて正規化せずに NoSQL のように運用することがあるのですが、その際に MySQL8.0 の JSON_TABLE という関数を多用します(そのうち余裕があれば転置インデックスの話もします)。ですが Amazon Aurora でサポートしているのは MySQL5.7 までですので、オリジナルの MySQL をエンジンとして選ばざるを得ません。window 関数なんかも確か 5.7 では使えないはずです。まあ一般的な CRUD のみであればバージョンはそこまで気にする必要はありません。今回は MySQL8.0 を選択します。
Screen Shot 2563-12-15 at 15.00.07.png
Free tier を選びます。
Screen Shot 2563-12-15 at 15.02.31.png
データベース名、ユーザー名、パスワードなどを決めます。DBインスタンスは無料利用枠では選択不可です。
Screen Shot 2563-12-15 at 15.03.18.png
Strage は 20GB もあれば十分ではないかと思います。
Screen Shot 2563-12-15 at 15.05.16.png
Public accessYes にしておかないと、EC2以外の外部からアクセスできなくなります。全て作り終わった後に設定をアクセス不可に戻せば良いです。
Screen Shot 2563-12-15 at 15.43.15.png
セキュリティーグループは その2 で設定済の default を選べば良いです。一番右下の Create database します。
Screen Shot 2563-12-15 at 15.08.16.png
データベースの作成には少し時間がかかります。ステータスが Available になれば使えます。
Screen Shot 2563-12-15 at 15.11.06.png
データベース名を押すと詳細が出ます。このエンドポイントと、先ほど入力したユーザー名・パスワードでデータベースにアクセスできます。
Screen Shot 2563-12-15 at 15.22.28.png

接続例

MySQL Workbench

Hostname に先ほどのエンドポイントを入力し、Username と Password を入れて Test Connection し、Success と出れば接続できています。
Screen Shot 2563-12-15 at 15.55.47.png
外部からでも問題なく使えます。
Screen Shot 2563-12-15 at 16.02.07.png

VScode

MySQL Workbench のような統合GUIは、設定をいじったりER図を書くときなどには非常に重宝しますが、コーディングしながら、VScode内からそのままクエリを投げたいときもあります。この MySQL 用の Extension を導入します。
Screen Shot 2563-12-15 at 16.10.04.png
左側にデータベースのアイコンが追加されます。上の➕から接続を追加できます。
Screen Shot 2563-12-15 at 16.11.20.png
同様にエンドポイント、ユーザー名、パスワードを入れて接続します。
Screen Shot 2563-12-15 at 16.13.13.png
.sql ファイルならシンタックスハイライトもされます。F9キーでクエリを実行できます。し、右側のフィールドで実行することも可能です。便利すぎます。
Screen Shot 2563-12-15 at 16.24.10.png

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

DynamoDBが簡単にExportできるようになった!

本記事はハンズラボ Advent Calendar 2020の17日目です。

はじめに!

こんにちは。AWS絶賛勉強中の@bassaaaaaです。

業務でDynamoDBテーブルを全件取得し、ゴニョゴニョする必要がありました。
RDBなら普通にExportできますが、DynamoDBは最近までできなかったみたいです。
というわけで、最近出たDynamoDB Export1をaws-cliからやってみたいと思います。

前置きはいいから!という人はaws-cliでDynamoDB Exportをやってみる

昔は大変だった!

DynamoDB Exportは2020/11/9から使えるようになりました。
それまでは以下のような方法でExportを行う必要がありました。2

  1. AWS Data Pipelineを使う
  2. Amazon EMRを使う
  3. AWS Glueを使う
  4. 全件取得クエリ

1~3はなんとなく、面倒そうです。
4は一発で終わり簡単そうです。
これらと比べて何が良いんでしょうか??

いろいろ心配しなくていい!

公式ドキュメント3にはこのように書かれています。

Exporting a table does not consume read capacity on the table, and has no impact on table performance and availability.
テーブルをエクスポートしても読み込みキャパシティは消費されず、テーブルのパフォーマンスや可用性に影響はありません。

どういうこと?

DynamoDBはデータの読み書きに :moneybag: がかかります。
その読み書き性能はテーブル毎に設定するキャパシティユニットにより決まります。
値が大きいほど、性能が良く:moneybag: もかかります。

  • 読み取りキャパシティユニット(RCU)
    • 1RCUは1秒間に4KBまでの項目を1回読み取れます。4
  • 書き込みキャパシティユニット(WCU)
    • 1WCUは1秒間に1KBまでの項目を1回書き込めます。4

100RCUなら1秒間に4KB*100個の項目を同時に読み取れますよー、みたいな感じです。
この性能を超えたリクエストが来た場合、スロットリングエラーが発生し、読み込み/書き込みリクエストが失敗します。

これは、上記1~4の方法でも起きる可能性があります。
Exportが失敗するのは別に良いとして、Exportが食い潰したキャパシティにより、本来成功したはずの本番処理が失敗するのは避けたいです。

なので、上記1~4の方法でExportする場合は、事前にキャパシティをあげ、Export中はスロットルエラーが発生してないか監視する必要があったのです。
また、キャパシティをあげるため:moneybag:もかかり、上記リスクもあるので、
本当にExportする必要があるのか?など、要らぬ心配も必要だったはずです(ただのExportなのに)

DynamoDB Exportはここら辺の心配をしなくて良いのです:tada: :confetti_ball:

DynamoDB Exportの仕組み

DynamoDB Exportは内部的にPITR (Point In Time Recovery) を使用しています。
PITRはDynamoDBテーブルの自動バックアップ機能で、過去35日間の任意の時点(1秒単位)へ復元できます。
というわけで、DynamoDB Export使用前にPITRを有効化する必要があります。

ExportデータはDynamoDB JSON形式かION形式のgz圧縮でS3に保存されます。
出力ディレクトリ構造は以下のような感じです。
Exportデータはサイズによって複数分割されることもあるみたいです。

s3://
  └ hogehoge                                       # バケット名(指定)
    └ fugafuga                                     # prefix(指定)
      └ ...                                        # prefix(指定)
        └ AWSDynamoDB                              # 固定
          └ 01234567890abc-1234abc                 # ランダム文字列
            ├ ...                                  # manifestなど
            └ data                                 # 固定
              └ abcdefghijklmnopqrstuvwxyz.json.gz # Exportデータ(JSON or IONを指定)

aws-cliでDynamoDB Exportをやってみる

DynamoDB Exportはマネジメントコンソールからでもできます。
「どのテーブルをどのバケットに」を指定するだけなので、簡単です。
が、今後のために一連の処理をaws-cliを使ってshellで書いてみます。

実行環境

  • macOS Catalina (10.15.7)
  • aws-cli/2.1.5

※ 本番環境でのaws-cliのVerUpは要注意

ざっくり流れ

  • PITRを有効化する
  • Exportリクエスト
  • Export状況問合せ
  • PITRの設定を元に戻す
  • S3からExportデータをダウンロードする

以下、変数など適当に補完してください

PITRの有効化

dynamodb_export.bash
# PITR設定取得
aws dynamodb describe-continuous-backups \
    --table-name $TABLE_NAME > $tmp-init-setting

# 設定値取得
cat $tmp-init-setting | jq -r ".ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus" > $tmp-init-status

# 有効でない場合は有効にする
if [ "$(cat $tmp-init-status)" = "DISABLED" ]; then
    aws dynamodb update-continuous-backups \
        --table-name $TABLE_NAME \
        --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true > $tmp-changed-setting
fi

レスポンス

$tmp-init-setting
{
    "ContinuousBackupsDescription": {
        "ContinuousBackupsStatus": "ENABLED",
        "PointInTimeRecoveryDescription": {
            "PointInTimeRecoveryStatus": "DISABLED"
        }
    }
}
$tmp-changed-setting
{
    "ContinuousBackupsDescription": {
        "ContinuousBackupsStatus": "ENABLED",
        "PointInTimeRecoveryDescription": {
            "PointInTimeRecoveryStatus": "ENABLED",
            "EarliestRestorableDateTime": "2020-12-07T15:06:44+09:00",
            "LatestRestorableDateTime": "2020-12-07T15:06:44+09:00"
        }
    }
}

Exportリクエスト

dynamodb_export.bash
# DynamoDB Exportリクエスト
# TABLE_ARN  arn:aws:dynamodb:REGION:ACCOUNT:table/tablename
# s3://S3_BUCKET/S3_PREFIX
# EXPORT_FORMAT  DYNAMODB_JSON / ION
aws dynamodb export-table-to-point-in-time \
    --table-arn $TABLE_ARN \
    --s3-bucket $S3_BUCKET \
    --s3-prefix $S3_PREFIX \
    --export-format $EXPORT_FORMAT > $tmp-request

# ExportArnを取得
export_arn=`cat $tmp-request | jq -r '.ExportDescription.ExportArn'`

# Export先S3ディレクトリ名を取得
# ExportArnの最後の/以降
export_dir=`cat $export_arn | sed -e "s#\(.*\)\/\(.*\)#\2#"`

レスポンス

$tmp-request
{
    "ExportDescription": {
        "ExportArn": "arn:aws:dynamodb:REGION:ACCOUNT:table/tablename/export/01234567890abc-1234abc",
        "ExportStatus": "IN_PROGRESS",
        "StartTime": "2020-12-04T10:46:23.577000+09:00",
        "TableArn": "arn:aws:dynamodb:REGION:ACCOUNT:table/tablename",
        "TableId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "ExportTime": "2020-12-04T10:46:23.577000+09:00",
        "ClientToken": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "S3Bucket": "hogehoge",
        "S3Prefix": "fugafuga",
        "S3SseAlgorithm": "AES256",
        "ExportFormat": "DYNAMODB_JSON"
    }
}

Export状況問合せ

dynamodb_export.bash
# Export状況取得
while :
do
    # ステータスがCOMPLETEDになるまで60秒ごとに問い合わせる
    aws dynamodb list-exports | 
    jq -r ".ExportSummaries | map(select(.ExportArn == \"$export_arn\")) | .[].ExportStatus" > $tmp-status

    cat $tmp-status
    if [ "$(cat $tmp-status)" = "COMPLETED" ]; then
        break
    fi
    sleep 60
done

PITRの設定を元に戻す

dynamodb_export.bash
if [ "$(cat $tmp-init-status)" = "DISABLED" ]; then
    aws dynamodb update-continuous-backups \
        --table-name $TABLE_NAME \
        --point-in-time-recovery-specification PointInTimeRecoveryEnabled=false > $tmp-last-setting
fi

S3からExportデータをダウンロードする

# S3からコピーする
aws s3 cp s3://$S3_BUCKET/$S3_PREFIX/AWSDynamoDB/$export_dir/data/ ./ --recursive

Export結果

Exportデータはこんな感じです。

{"Item": {"id": {"S": "01"},"name": {"S": "fuga"}}}
{"Item": {"id": {"S": "02"},"name": {"S": "hoge"}}}
...

おまけ

DynamoDB JSON --> CSV

cat export.json | jq -r ".Item | [.id.S, .name.S] | @csv"
"01","fuga"
"02","hoge"

(参考)Export時間

いくつかのテーブルでExport時間を見てみました。
Export自体は1.5〜4分で終わりました。実際には、リクエストしてExportが開始するまでの時間がかかるので、5〜10分以内といった感じです。

テーブル 項目数 テーブルサイズ Export時間
1 4 25B 1.5〜4分(複数回実施)
2 3万 6MB 2分
3 120万 180MB 2分
4 200万 300MB 2分5秒

AWSブログ1によると、

完了時間は、テーブルのサイズと、テーブル内でデータがどのように均一に分散されているかに左右されます。エクスポートの大部分は 30 分以内に完了します。10 GiB までの小さなテーブルの場合、数分で完了します。テラバイトのオーダーの非常に大きなテーブルの場合、数時間かかる場合があります。

らしいです。

お金 :moneybag: のお話

上で「いろいろ心配しなくて良い」と書きました。
確かにキャパシティユニットとか本番への影響は心配しなくて良いです。
ただ、やっぱりお金はかかります。
具体的には、下記3つです。

  • PITRのお金
    • 毎月 0.228USD/GB (月中に一回でも有効化すれば課金?)有効化してた時間のみ課金5
  • DynamoDB Exportのお金
    • 0.114USD/GB
  • Export先S3バケットのお金

ただ、なんだかんだ楽ですし、以前までの方法と比べると安いもんです。

最後に

DynamoDB ExportはDynamoDBとS3だけで完結してるのが良いですね。
RDBのExport並みにお手軽です。

最後までお読みいただきありがとうございました。


  1. 「新機能 – Amazon DynamoDB テーブルデータを Amazon S3 のデータレイクにエクスポート。コードの記述は不要」(https://aws.amazon.com/jp/blogs/news/new-export-amazon-dynamodb-table-data-to-data-lake-amazon-s3/

  2. 「DynamoDB テーブルを Amazon S3 にバックアップするにはどうすればよいですか ?」 (https://aws.amazon.com/jp/premiumsupport/knowledge-center/back-up-dynamodb-s3/

  3. 「Exporting DynamoDB table data to Amazon S3」(https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/DataExport.html

  4. 実際は色々モードあり。詳細は「読み込み/書き込みキャパシティーモード」( https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html

  5. PITRの料金は「その月のPITRを有効化していた時間の1時間あたりの平均テーブル容量(GB-Month)×料金表に記載の単価」です。東京リージョンで10GBのテーブルをExportのため1時間だけPITRを有効化した場合の料金は、((10GB * 1h * 1日) / (24h * 30日)) * 0.228USD/GB = 0.003USDとなります。 

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

⑯環境変数の設定ーポートフォリオ

データベースのパスワードなどセキュリティのためにGitHubにアップロードすることができない情報は「環境変数」を利用して設定します。

secret_key_base
「secret_key_base」とは、Cookieの暗号化に用いられる文字列です。Railsアプリケーションを動作させる際は必ず用意する必要があります。

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ rake secret

sudo
「sudo」とは、「全部の権限を持った上でコマンドを実行する」という役割のオプションです。「/etc以下のファイル」は強い権限がないと書き込み・保存ができないため、コマンドの頭に「sudo」をつけています。

[ec2-user@ip-172-31-23-189 ~]$ sudo vim /etc/environment
DATABASE_PASSWORD='データベースのrootユーザーのパスワード'
SECRET_KEY_BASE='さきほど作成したsecret_key_base'

# 「AWSに画像をアップロードする」でダウンロードしたCSVファイルを参考に値を入力
AWS_ACCESS_KEY_ID='ここにCSVファイルのAccess key IDの値をコピー'
AWS_SECRET_ACCESS_KEY='ここにCSVファイルのSecret access keyの値をコピー'

# Basic認証で設定したユーザー名とパスワードを入力
BASIC_AUTH_USER='設定したユーザー名'
BASIC_AUTH_PASSWORD='設定したパスワード'

入力を終えたら「escキー」→「:wq」の順で実行し、保存

接続し直したら、「envコマンド」と「grepコマンド」を組み合わせて、さきほど設定した環境変数が適用されているか確認

[ec2-user@ip-172-31-23-189 ~]$ env | grep SECRET_KEY_BASE
SECRET_KEY_BASE='secret_key_base'

[ec2-user@ip-172-31-23-189 ~]$ env | grep DATABASE_PASSWORD
DATABASE_PASSWORD='データベースのrootユーザーのパスワード'

[ec2-user@ip-172-31-23-189 ~]$ env | grep AWS_SECRET_ACCESS_KEY
AWS_SECRET_ACCESS_KEY='Secret access key'

[ec2-user@ip-172-31-23-189 ~]$ env | grep AWS_ACCESS_KEY_ID
AWS_ACCESS_KEY_ID='Access key ID'

[ec2-user@ip-172-31-23-189 ~]$ env | grep BASIC_AUTH_USER
BASIC_AUTH_USER='設定したユーザー名'

[ec2-user@ip-172-31-23-189 ~]$ env | grep BASIC_AUTH_PASSWORD
BASIC_AUTH_PASSWORD='設定したパスワード'

ポートを解放
立ち上げたばかりのEC2インスタンスはsshでアクセスすることはできますが、HTTPなどの他の通信方法では一切つながらないようになっています。そのため、サーバーとして利用するEC2インスタンスは事前にHTTPがつながるように「ポート」を開放する必要があります。

セキュリティグループ

スクリーンショット 2020-12-15 16.47.27.png
スクリーンショット 2020-12-15 16.46.38.png

本番環境でRailsを起動
本番環境でRailsを起動するには「unicorn_railsコマンド」を使います。

config/database.yml
username: root
  password: <%= ENV['DATABASE_PASSWORD'] %>
  socket: /var/lib/mysql/mysql.sock
[ec2-user@ip-172-31-23-189 <リポジトリ名>] git pull origin master

RAILS_ENV=production
「RAILS_ENV=production」とは、本番環境でコマンド実行する時につくオプションです。
実行しようとしているコマンドは、「RAILSのENV(環境)がproduction(本番環境)ですよ」という意味です。

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ rails db:create RAILS_ENV=production
Created database '<データベース名>'
[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ rails db:migrate RAILS_ENV=production

アセットファイル
「アセットファイル」とは、画像・CSS・JavaScript等を管理しているファイルです。このアセットファイルを圧縮し、そのデータを転送する処理を「コンパイル」と言います。この作業を行わないと、本番環境でCSSが反映されずにビューが崩れてしまったり、エラーでブラウザが表示されない、などの問題が生じてしまいます。

[ec2-user@ip-172-31-23-189 <リポジトリ名>]$ rails assets:precompile RAILS_ENV=production
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RDSとServerlessFrameworkでおみくじを作ろう

はじめに

AWSの使っておみくじアプリを作るよ(そんなのRDSを使わなくてもよくね?という話はナシで)

環境

node.js v12.18.3

作ってみる

DBインスタンスの作成

ここを参考にRDSを使ってDBインスタンスを作成していく

DBの設定

- データベース作成方法:簡単作成
- 設定:MySQL
- インスタンスサイズ:無料利用枠
- DBインスタンス識別子:aws-web-service
- マスターユーザー名:admin
- マスターパスワード:各自おまかせ
- パブリックアクセス可能:あり

作成したDBインスタンスのエンドポイントとマスターユーザーを使ってDBインスタンスにアクセス。

console
mysql -h mysql-80.xxx.{リージョン}.rds.amazonaws.com -P 3306 -u admin -p

RDSをパブリックアクセス可能にしてるのでセキュリティ的にはゆるいけど、まあすぐ消すのでいいかなと。

DBインスタンスにデータを追加

DB作成

console
$ create database omikuji
$ use omikuji
$ create table omikuji.kuji (id int, result varchar(3));

$ insert into kuji values (0,'大吉');
$ insert into kuji values (1,'中吉');
$ insert into kuji values (2,'小吉');

mysql> select * from kuji;
+------+--------+
| id   | result |
+------+--------+
|    0 | 大吉    |
|    1 | 中吉    |
|    2 | 小吉    |
+------+--------+

これで完成

コードを書いていく

node.jsとsequelizeを使ってコマンドライン上でおみくじの結果を出力するようにする。

コマンドラインからディレクトリやファイルを作成したり、serverlessとかsequelizeをインストールしてみたりする。
ファイル構成についてだが、handler.jsにメインの処理を書いて、DBインスタンスのアクセスに必要な情報を.envに置くという構成にする。

console
# ディレクトリの作成
$ mkdir omikuji

# npm初期化
$ npm init -y

# パッケージインストール
$ npm install --save serverless
$ npm install --save sequelize
$ npm install --save dotenv
$ npm install --save mysql2

# serverlessの実行環境作成
$ serverless create --template aws-nodejs 

# ファイルの作成
$ touch .env    // 環境変数用のファイル
$ touch kuji.js // sequelize モデル定義に使うファイル

# ディレクトリ構成は以下の通り
.env                    handler.js              node_modules            package.json
.gitignore              kuji.js                 package-lock.json       serverless.yml

とりあえず、serverlessがちゃんと動くかどうかテストしてみる。.envファイルがあることで警告は出ているが、ちゃんと動いていることが確認できたので、あとはhelloの中身をおみくじができるようにソースコードを変更してあげればよさそう。
※.envによる警告メッセージは話の本筋とは関係ないので、いったん無視していきます。

sls invoke local --function hello
Serverless: Running "serverless" installed locally (in service node_modules)
Serverless: Deprecation warning: Detected ".env" files. Note that Framework now supports loading variables from those files when "useDotenv: true" is set (and that will be the default from next major release)
            More Info: https://www.serverless.com/framework/docs/deprecations/#LOAD_VARIABLES_FROM_ENV_FILES
{
    "statusCode": 200,
    "body": "{\n  \"message\": \"Go Serverless v1.0! Your function executed successfully!\",\n  \"input\": \"\"\n}"
}

ソースコードの修正
.envとhandler.jsを以下のようにする

.env
DATABASE_NAME = "omikuji"
USERNAME = "admin"
PASSWORD = "your_db_password"
HOST =  "mysql-80.xxx.{リージョン}.rds.amazonaws.com"

handler.js
const { Sequelize } = require('sequelize');
require('dotenv').config();

async function hello() {

  const sequelize = new Sequelize(
    process.env.DATABASE_NAME, 
    process.env.USERNAME, 
    process.env.PASSWORD, {
      host: process.env.HOST,
      dialect: "mysql"
  });

  try {
    await sequelize.authenticate();
    console.log('Connection has been established successfully.');
  } catch (error) {
    console.error('Unable to connect to the database:', error);
  } finally {
    sequelize.close();
  }

};

module.exports = { hello }

これで sls invoke local --function helloを実行すると以下のようになるはず。
DBの接続そのものはうまくいったみたいなので、データを取得できるようにしていく。

console
sls invoke local --function hello
Serverless: Running "serverless" installed locally (in service node_modules)
Executing (default): SELECT 1+1 AS result
Connection has been established successfully.

おみくじ機能を実装

おみくじするためのデータ取得処理を作成していく。
コードは以下の通り。

handler.js
const { Sequelize, DataTypes } = require('sequelize');
require('dotenv').config();
const range = 3;

async function hello() {

  // Sequelizeインスタンスの作成
  const sequelize = new Sequelize(
    process.env.DATABASE_NAME, 
    process.env.USERNAME, 
    process.env.PASSWORD, {
      host: process.env.HOST,
      dialect: "mysql",
      logging: false,
  });

  // 0 ... range - 1 の範囲で乱数を生成
  const random = Math.floor( Math.random() * range);

  const Kuji = require("./kuji")(sequelize, DataTypes);

  try {
    const result = await Kuji.findOne({
      where: {
        id: random
      },
      attributes: ["result"],
      raw: true
    })

    return result.result;
  } catch (error) {
    console.error('Unable to connect to the database:', error);
  } finally {
    sequelize.close()
  }
};

module.exports = { hello }

try文でreturnを返したら後の処理ってどうなるんだろうと思ったら、きちんとfinally句は実行してくれるっぽい。(ソース)

kuji.js
module.exports = (sequelize, DataTypes) => {
    return sequelize.define(
      "kuji",
      {
        id: {
          type: DataTypes.INTEGER,
          primaryKey: true,
        },
        result: {
          type: DataTypes.CHAR,
        },
      },
      {
        timestamps: false,
        freezeTableName: true,
      }
    );
};

これで実行してみると...

console
# 1回目
sls invoke local --function hello
Serverless: Running "serverless" installed locally (in service node_modules)
大吉

# 2回目
sls invoke local --function hello
Serverless: Running "serverless" installed locally (in service node_modules)
中吉

できたできた。
あとはこれをlambdaにアップしよう。

Serverless Frameworkのserverless-layersプラグイン

lambdaにアップする前にserverless-layersプラグインを入れて、ライブラリをレイヤーにしてしまおう。

console
$ npx sls plugin install --name serverless-layers

そしたら、レイヤーにするライブラリを格納するためのS3バケットを作成して、そこに格納するようにserverless.ymlに記述していく。あと、デプロイ時にリージョン指定するのも面倒なのであらかじめ設定しておく。

serverless.yml
service: omikuji
frameworkVersion: '2'

provider:
  name: aws
  runtime: nodejs12.x
  region: {お使いのリージョン} # リージョン指定

functions:
  hello:
    handler: handler.hello

custom:
  serverless-layers:
    layersDeploymentBucket: hogehoge # 作成したバケット

plugins:
  - serverless-layers

デプロイ

console
# AWSの認証情報をcredentialsに書き込む
$ serverless config credentials --provider aws --key aws_access_key_id --secret aws_secret_access_key

# デプロイする
$ npx sls deploy 

これでデプロイが完了したので動かしてみる

console
$ sls invoke --function hello
Serverless: Running "serverless" installed locally (in service node_modules)
"小吉"

できた。データベース使っておみくじ作るのは意味わからんが、ひとまずRDSとserverless frameworkを使ってちょっとしたアプリを作れたのでよしとする。

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

⑮EC2のRailsを起動しよう(手動デプロイ)ーポートフォリオ

EC2サーバーのssh鍵のペアを作成

[ec2-user@ip-172-31-23-189 ~]$ ssh-keygen -t rsa -b 4096

catコマンドで、公開鍵が含まれているファイルid_rsa.pubの中身をターミナル上に表示します。

[ec2-user@ip-172-31-23-189 ~]$ cat ~/.ssh/id_rsa.pub

そして、以下のように表示される公開鍵の情報をすべて(「ssh-rsa」から「末尾の文字」まで)コピーします。

次に、コピーした公開鍵をGitHubに登録します。
スクリーンショット 2020-12-15 15.19.06.png

GitHubに鍵を登録できたら、ssh接続できるか以下のコマンドで確認

[ec2-user@ip-172-31-23-189 ~]$ ssh -T git@github.com

アプリケーションサーバーの設定
ブラウザからの「リクエスト」を受け付けRailsアプリケーションを実際に動作させるソフトウェアのことです。つまり、「rails sコマンド」です。
全世界に公開するEC2サーバー上でもアプリケーションサーバを動かす必要があります。そのために必要なツールが「Unicorn」と呼ばれるものです。

Unicorn
全世界に公開されるサーバ上で良く利用されるアプリケーションサーバーです。rails sコマンドの代わりに「unicorn_railsコマンド」で起動することができます。

プロセスを確認
スクリーンショット 2020-12-15 16.07.00.png

Unicornなどのアプリケーションサーバを起動するとアプリケーションサーバのプロセスが生まれます。アプリケーションサーバのプロセスがあれば、リクエストを受け付けブラウザにレスポンスを返すことができます。

Unicornをインストール

group :production do
  gem 'unicorn', '5.4.1'
end
bundle install

unicorn.rb」を作成

config/unicorn.rb
#サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
app_path = File.expand_path('../../', __FILE__)

#アプリケーションサーバの性能を決定する
worker_processes 1

#アプリケーションの設置されているディレクトリを指定
working_directory app_path

#Unicornの起動に必要なファイルの設置場所を指定
pid "#{app_path}/tmp/pids/unicorn.pid"

#ポート番号を指定
listen 3000

#エラーのログを記録するファイルを指定
stderr_path "#{app_path}/log/unicorn.stderr.log"

#通常のログを記録するファイルを指定
stdout_path "#{app_path}/log/unicorn.stdout.log"

#Railsアプリケーションの応答を待つ上限時間を設定
timeout 60

#以下は応用的な設定なので説明は割愛

preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true

check_client_connection false

run_once = true

before_fork do |server, worker|
  defined?(ActiveRecord::Base) &&
    ActiveRecord::Base.connection.disconnect!

  if run_once
    run_once = false # prevent from firing again
  end

  old_pid = "#{server.config[:pid]}.oldbin"
  if File.exist?(old_pid) && server.pid != old_pid
    begin
      sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
      Process.kill(sig, File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH => e
      logger.error e
    end
  end
end

after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

リモートリポジトリに「commit→push」
masterブランチにmergeしてから以下のコマンドを実行

EC2にローカルの全内容を反映

#mkdirコマンドで新たにディレクトリを作成
[ec2-user@ip-172-31-23-189 ~]$ sudo mkdir /var/www/

#作成したwwwディレクトリの権限をec2-userに変更
[ec2-user@ip-172-31-23-189 ~]$ sudo chown ec2-user /var/www/

次に、GitHubから「リポジトリURL」を取得し、クローンします。

スクリーンショット 2020-12-15 16.23.53.png

URLがコピーできたら、コードをクローンします。

[ec2-user@ip-172-31-23-189 ~]$ cd /var/www/
[ec2-user@ip-172-31-23-189 www]$ git clone コピーしたURLを貼り付ける
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Athenaでパーティション射影を使ってみた

Athenaを使う機会があったので、パーティション機能の使い方を簡単ですが備忘録としてまとめてみました。
S3上のファイルとの関連も書いています。

Athenaとは

Amazon S3上のファイルをデータとして、SQLでアクセスできるAWSサービスです。テーブルを作成するときにS3上のパスを指定すると、そのパス以下のファイルがデータとして扱われます。

パーティション機能を使うとSELECTクエリ等でパーティション・プルーニングが働き、
パーティションキーに対するWHERE条件に基づいて不要なデータ探索をスキップできます。

データのイメージ

今回使用するデータは3つの列を持つテーブルです。

日付(tm) 識別子(id) 値(value)
2020-12-01 sensor1 100.0
2020-12-02 sensor1 101.0
2020-12-02 sensor2 102.0
2020-12-03 sensor1 103.0
2020-12-03 sensor2 104.0

テーブル定義

以下のSQLでテーブルを作成しました。

CREATE EXTERNAL TABLE tbl1  (id string, value double)
PARTITIONED BY (tm date)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'
WITH SERDEPROPERTIES ('serialization.format' = ',', 'field.delim' = ',')
LOCATION 's3://test-bucket/tbl1/'
TBLPROPERTIES (
  'has_encrypted_data'='false',
  'projection.enabled' = 'true',
  'projection.tm.type' = 'date',
  'projection.tm.range' = 'NOW-3YEARS,NOW+3YEARS',
  'projection.tm.format' = 'yyyy-MM-dd',
  'storage.location.template' = 's3://test-bucket/tbl1/${tm}'
);

日付列(tm)はパーティション列にするので、残りの識別子列(id)と値列(value)をEXTERNAL TABLEの列として定義します。
今回データの場所は、s3://test-bucket/tbl1/ としました。

パーティション列の指定はPARTITIONED BYで行い、設定はTBLPROPERTIESで指定します。
データの範囲'projection.tm.range'を指定しないと、INSERT時に以下のエラーが出たので、
INVALID_TABLE_PROPERTY: Must provide a range to build a projected temporal partition column!
今回は本日から前後3年間を指定しました。

テーブル作成後、以下のSQLを実行し、パーティションを認識させておきます。
(あらかじめS3上にファイルがなければ実行不要)

MSCK REPAIR TABLE tbl1;

データの登録

通常のINSERT文でデータを登録できます。今回は以下のSQLを実行しました。

INSERT INTO tbl1(id, value, tm) VALUES('sensor1', 100.0, DATE '2020-12-01');
INSERT INTO tbl1(id, value, tm) VALUES('sensor1', 101.0, DATE '2020-12-02');
INSERT INTO tbl1(id, value, tm) VALUES('sensor2', 102.0, DATE '2020-12-02');
INSERT INTO tbl1(id, value, tm) VALUES('sensor1', 103.0, DATE '2020-12-03'),('sensor2', 104.0, DATE '2020-12-03');
INSERT INTO tbl1(id, value, tm) VALUES('sensor1', 105.0, DATE '2020-12-03'),('sensor2', 106.0, DATE '2020-12-04');

登録されたデータの確認

SELECT文でデータを取得できますが、S3上のファイルの中身を確認してみました。

テーブル作成時に指定した場所(s3://test-bucket/tbl1/)にファイルが作成されています。
パーティションごとにディレクトリができていて、その下に.gzファイルがあります。

ファイル構成は以下のようになっていました。
.gzファイルを展開するとカンマ区切りのテキストファイルになっていることがわかります。

2020-12-01/xxx.gz:
sensor1,100.0

2020-12-02/yyy.gz:
sensor1,101.0

2020-12-02/zzz.gz:
sensor2,102.0

2020-12-03/www.gz:
sensor1,103.0
sensor2,104.0

2020-12-03/www.gz:
sensor1,105.0

2020-12-04/www.gz:
sensor2,106.0

INSERT文ごとにファイルがファイルが作成されていますね。異なるパーティションの場合はさらに別ファイルに分割されています。

intervalを設定してみた

intervalはパーティションキーの値の間隔を指定できる機能です。さっそく使ってみます。

date列に対して、"日にち"でインターバル

先程のCREATE文に対して、projection.datehour.intervalとprojection.datehour.interval.unitを指定し、インターバルを3日に設定しました。

CREATE EXTERNAL TABLE tbl2 (id string, value double)
PARTITIONED BY (tm date)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'
WITH SERDEPROPERTIES ('serialization.format' = ',', 'field.delim' = ',')
LOCATION 's3://test-bucket/tbl2/'
TBLPROPERTIES (
  'has_encrypted_data'='false',
  'projection.enabled' = 'true',
  'projection.tm.type' = 'date',
  'projection.tm.range' = 'NOW-3YEARS,NOW+3YEARS',
  'projection.tm.format' = 'yyyy-MM-dd',
  'projection.datehour.interval' = '3',
  'projection.datehour.interval.unit' = 'DAYS',
  'storage.location.template' = 's3://test-bucket/tbl2/${tm}'
);

それでは、本日(12/2)、明日、明後日のデータを登録してみます。インターバルは3日だけど、明日・明後日のデータは登録できるのか?登録できた場合、S3上のディレクトリ構成はどうなっているか気になり、確認しました。

INSERT INTO tbl2(id, value, tm) VALUES('sensor1', 100.0, DATE '2020-12-02');
INSERT INTO tbl2(id, value, tm) VALUES('sensor2', 100.0, DATE '2020-12-03');
INSERT INTO tbl2(id, value, tm) VALUES('sensor3', 100.0, DATE '2020-12-04');

INSERTできました。
S3上はどうなっているかというと、s3://test-bucket/tbl2に3つのディレクトリが作成され、その下にそれぞれ.gzファイルがありました。

2020-12-02/xxx.gz:
sensor1,100.0

2020-12-03/yyy.gz:
sensor2,100.0

2020-12-04/zzz.gz:
sensor3,100.0

date列に対して、"月"でインターバル

今度はインターバルを1月に設定しました。パーティションキーの列には日にちのデータが格納されています。

4つのデータを登録してみます。本日、明日、1ヶ月前、1ヶ月と1日前のデータです。
インターバルより細かいデータはどう扱われるでしょうか?

INSERT INTO tbl3(id, value, tm) VALUES('sensor1', 100.0, DATE '2020-12-02');
INSERT INTO tbl3(id, value, tm) VALUES('sensor2', 100.0, DATE '2020-12-03');
INSERT INTO tbl3(id, value, tm) VALUES('sensor3', 100.0, DATE '2020-11-02');
INSERT INTO tbl3(id, value, tm) VALUES('sensor4', 100.0, DATE '2020-11-01');

結論としては、"日"でのインターバルと同じでした。
以下のように、日付ごとにディレクトリが作成されていました。

2020-11-01/xxx.gz:
sensor4,100.0

2020-11-02/yyy.gz:
sensor3,100.0

2020-12-02/zzz.gz:
sensor1,100.0

2020-12-03/www.gz:
sensor2,100.0

おわりに

パーティション射影機能を使ってみました。
CREATE EXTERNAL TABLEのTBLPROPERTIESに設定を記述するだけで簡単にパーティションが作れました。
S3上には、パーティションのインターバルに関係なくstorage.location.templateで指定した場所にファイル分割されてデータが格納されるようです。

参考文献

https://docs.aws.amazon.com/athena/latest/ug/partition-projection.html
https://dev.classmethod.jp/articles/20200627-amazon-athena-partition-projection/

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

Lambda関数でEmbulkコマンドを実行してみる

はじめに

この記事は BeeX Advent Calendar 2020 の12/16の記事です。

==

前の記事でLambdaのコンテナイメージでの実行を試してみました。

今回はそれに加えて、最近よく使っているEmbulkコマンドを使えないか試してみました。

環境

PC:Windows 10
Docker:Docker version 19.03.13, build 4484c46d9d

前提

  • Dockerの初期設定は完了済み
  • Python3はインストール済み(Lambdaランタイムと同じかそれ以上)
  • AWSアカウントは作成済み
  • Amazon ECRリポジトリは作成済み

実際の操作

lambda functionの作成

@shiro01さんの記事を参考にLambda Functionを作成します。
今回はEmbulkの実行の確認なのでシンプルにヘルプコマンドを実行してみます。

lambda_function.py
import subprocess

def lambda_handler(event, context):
    cmd = ['/usr/bin/embulk','help']
    out = subprocess.run(cmd,shell=True , stdout=subprocess.PIPE)
    print(out.stdout.decode())

1つ注意点として、subprocess.runの引数に"shell=Treu"を追加しています。
これを追加しないとOSErrorが発生します。

参考

Dockerfileの作成

続いてDockerfileを作成します。
Embulkは事前にインストールして実行ファイルを作成しておきます。
DockerfileのCOPYでEmbulkとlambda_functionをコンテナ上にコピーします。

FROM amazon/aws-lambda-python:3.7

COPY lambda_function.py ./
COPY embulk /usr/bin

RUN chmod +x /usr/bin/embulk

CMD [ "lambda_function.lambda_handler" ]

イメージをBuildする

以下のコマンドでイメージをビルドします。

$ cd [DockerFile格納先DIR]
$ docker build -t lambda_embulk .

イメージが作成されます。

$ docker images
REPOSITORY    TAG       IMAGE ID       CREATED              SIZE
lambda_embulk latest    472005da7cf7   About a minute ago   980MB

ローカルテスト

以下のコマンドでローカルでLambdaを実行することが可能です。

$ docker run -p 9000:8080 lambda_embulk:latest
time="2020-12-15T06:11:57.95" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)"

別のウィンドウを開いて、以下のコマンドを実行します。
curlコマンドのレスポンスとしてはnullが返ってきます。

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d''{}'
null

コンテナを実行しているウィンドウを確認すると実行された結果がコンソールに表示されています。
"embulk help"コマンドが実行されているので、ローカルテストは問題なさそうです。

$ docker run -p 9000:8080 lambda_embulk:latest
time="2020-12-15T06:13:53.604" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)"
START RequestId: 0ed577b3-6a07-447a-bccb-c03d255c0201 Version: $LATEST
time="2020-12-15T06:13:57.601" level=info msg="extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory"
time="2020-12-15T06:13:57.601" level=warning msg="Cannot list external agents" error="open /opt/extensions: no such file or directory"
Embulk v0.9.23
Usage: embulk [-vm-options] <command> [--options]
Commands:
   mkbundle   <directory>                             # create a new plugin bundle environment.
   bundle     [directory]                             # update a plugin bundle environment.
   run        <config.yml>                            # run a bulk load transaction.
   cleanup    <config.yml>                            # cleanup resume state.
   preview    <config.yml>                            # dry-run the bulk load without output and show preview.
   guess      <partial-config.yml> -o <output.yml>    # guess missing parameters to create a complete configuration file.
   gem        <install | list | help>                 # install a plugin or show installed plugins.
   new        <category> <name>                       # generates new plugin template
   migrate    <path>                                  # modify plugin code to use the latest Embulk plugin API
   example    [path]                                  # creates an example config file and csv file to try embulk.
   selfupdate [version]                               # upgrades embulk to the latest released version or to the specified version.

VM options:
   -E...                            Run an external script to configure environment variables in JVM
                                    (Operations not just setting envs are not recommended nor guaranteed.
                                     Expect side effects by running your external script at your own risk.)
   -J-O                             Disable JVM optimizations to speed up startup time (enabled by default if command is 'run')
   -J+O                             Enable JVM optimizations to speed up throughput
   -J...                            Set JVM options (use -J-help to see available options)
   -R--dev                          Set JRuby to be in development mode

Use `<command> --help` to see description of the commands.

END RequestId: 0ed577b3-6a07-447a-bccb-c03d255c0201
REPORT RequestId: 0ed577b3-6a07-447a-bccb-c03d255c0201  Init Duration: 1.03 ms  Duration: 591.61 ms     Billed Duration: 600 mMemory Size: 3008 MB     Max Memory Used: 3008 MB

ECRリポジトリへのPush

AWSコンソールからリポジトリを作成します。
image.png

以下のコマンドで作成したECRリポジトリにイメージをPushします。
ここら辺は前回の記事をほぼ同じです。
Embulkが少々サイズがあるので、前回よりPushに時間がかかります。

$ docker tag lambda_embulk:latest XXXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-embulk
$ docker images
REPOSITORY                                                                     TAG       IMAGE ID       CREATED          SIZE
941996685139.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-embulk                latest    472005da7cf7   11 minutes ago   980MB
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin XXXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com
Login Succeeded
$ docker push XXXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-embulk:latest
The push refers to repository [XXXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-embulk]
5f70bf18a086: Pushed
65f9fe7cdd01: Pushed
1965e83122e7: Pushed
701bdcbf3b47: Pushed
6e660533f001: Pushed
069cd8bd11dd: Pushed
6e191121f7ea: Pushed
d6fa53d6caa6: Pushed
1fb474cee41c: Pushed
b1754cf6954d: Pushed
464c816a7003: Pushed
latest: digest: sha256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX size: 2624

無事にアップロードできました。
image.png

Lambda作成・実行

AWSのコンソールからLambda関数を作成します。
image.png

Lambda関数が作成されました。
image.png

実行結果確認

timeoutでエラーが出ました。
image.png

Lambdaの基本設定でタイムアウト値が3秒になっていたので、とりあえず3分に変更します。
image.png

少し時間はかかりましたが、正常終了しました。
出力内容を見る限りEmbulkのhelpコマンドも実行できているみたいです。
image.png

さいごに

これが良いかどうかは別として、無事にLambdaでEmbulkを実行することができました。
多分実運用するならLambdaのスペックとかdiffファイルどこで保持するとか色々検討すべきことはありますが、とりあえずs3に置いてそこから小規模の処理を動かすとかならいけるんじゃないですかね?知らんけど。

ただEmbulkの処理ってLambdaの実行時間に収まることあまりないから、その辺りは難しいところかもしれません。

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

作成済のAmazon WorkSpacesのパブリックIPの探し方

定期的に忘れてパニクるので、衝動的にメモ。

  1. WorkSpacesコンソールから、プライベートIPをコピーする。
  2. EC2コンソール>ネットワークインターフェイスに進む。
  3. 上部の検索窓にプライベートIPを入力してフィルタリングし、該当するENIをみつける。
  4. ENIの詳細画面から、パブリックIP/DNSを探す。

ENIの管理画面が、VPCコンソールではなくEC2コンソールにあるのが罠。

ちなみに:ENIとは

Elastic Network Interfaceの略。
https://qiita.com/TaigoKuriyama/items/a1fcbadd10fa7ba41a04
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/using-eni.html

ひとことでいうと、仮想ネットワークカード (NIC) 的なもの。
EC2などにも自動で生成されて割り当てられている。

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

Chime SDKを使ってZoomライクなアプリを作ろうとしたら意外と苦戦した話

概要

この記事は CA21 Advent Calendar 2020 Day16の記事です。

タイトルの通り、Chime SDKを用いてZoomっぽいアプリを作ろうとしたらハマりどころが結構多くて苦戦したお話です。

本記事ではChime SDKについて、以下のことを説明します。

  1. Chime SDKとは
  2. 実装時に役に立つドキュメント
  3. 実装時にハマるポイント
  4. 通話アプリの実装にChime SDKを使うべきかどうか

Amazon Chime SDKを使うことになった経緯

インターン先で開発しているサービスで、これまでZoomを利用していた通話機能部分を内製化する要件が発生しました。
最初はZoom SDKを使おうとしていましたが、UI変更が困難であること, ZOOMに依存した構造になってしまうこと(たとえば使用に際してZoomのアカウントが必要となること)を懸念し、 UIの拡張性の高いChime SDKの使用を決めました。
スクリーンショット 2020-12-15 14.51.04.png

Amazon Chime SDKについて

Amazon Chime SDKとは

簡単に言えば「通話アプリに必要な裏側の基盤をまるっとサポートしてくれる開発キット」です。
通常zoomのような通話アプリを作ろうとすると、WebRTCなど特殊な技術の勉強が必要となります。また、人数が増えた時の拡張性などを考慮したサーバー構築なども容易ではありません。
Chime SDKを使えばそういった煩雑な構築を完全にAWSに委ねることができます。また逆にUIに関しては余計なサポートがないため, 自分の思い通りのUIを作ることも容易です

Amazon Chime SDKを用いた通話アプリの構築

本記事では構築に関しては詳しく言及しませんが、以下記事が非常に参考になりました

amazon-chime-sdk-js(公式)
使用ケースごとにサンプルコードを書いてくれていてかなり親切. ここを見ればたいていの機能は構築できる。
Amazon Chime SDKを試してみた
日本語の記事の中では構築方法についてしっかり解説してくれている良記事。

はまった点

このように便利に思えるChime SDKですが、実際に開発に使ってみるとかなりはまりどころがありました。本記事では自分がはまった問題点とその解決策を紹介します。

部屋の保存時間が限られる

開発中に「一度作ったはずの部屋が一定時間後に消えてしまう(アクセスできなくなる)」という事象が発生しました。
サポートに問い合わせてみたところ、以下の条件を満たすと、作成したmeeting roomが自動削除されてしまうことがわかりました

1. 誰もAudio接続しないまま5分が経過した場合
2. 1人のみが接続した状態で30分以上が経過した場合
3. 1人のみがscreen shareをした状態で30分以上が経過した場合
4. meeting開始から24時間がたった場合

すなわち、Chime SDKではZoomのような部屋の作り置きができないということです。
これを解決するには、以下図のように、前もって別のDBにmeeting情報を保存しておき、開始時に初めてChimeでroom作成する、などの工夫が必要です。
スクリーンショット 2020-12-15 15.05.07.png

SSL化しないとうまく動かない

デプロイ後、以下のコードで本番環境のみエラーが発生する事象に遭遇しました。

//videoデバイスの設定
this.videoInputDevices = await meetingSession.audioVideo.listVideoInputDevices();

こちらもサポートに問い合わせてみたところ、Chime SDKを用いたアプリはhttpsプロトクルを用いた環境でないとうまく動作しないということがわかりました。
(上記コードが内部的にnavigator.mediaDevicesを呼び出しており、httpプロトコル化ではこれが未定義になるのが原因らしい)
だが、なぜlocalhostだと動いてしまうのかは謎...教えて強い人...!

自分以外の参加者の「ビデオオフ」と「退出」の判別ができない

Chime SDKでは参加者の画面On Offの検知をするために以下のようなobserverを定義します。

const observer = {
        videoTileDidUpdate: async (tileState) => {
          //videoをつけた時の処理
        },
        videoTileWasRemoved: (tileId) => {
          //videoを消した時の処理
        }
      }
meetingSession.audioVideo.addObserver(observer)

ところが上記observer内のvideoTileWasRemovedは該当の参加者が退出した際にも、ビデオをオフにした際にも反応してしまいます。すなわちobserver単体だと、退出とビデオオフの状態区別ができないのです...(この仕様のせいか、Amazon Chimeの公式アプリは、カメラをオフ専用のUIが用意されていません)

これを解決するためには、退出検知用に以下のようなコードを書く必要があります。

      //入退出処理
      const callback = async (presentAttendeeId, present) => {
        if (present) {
         //入室時処理
        } else {
          //退出処理
        }
      }
      meetingSession.audioVideo.realtimeSubscribeToAttendeeIdPresence(
        callback
      )

このコードは参加者の入退室時のみ駆動し、定義した動作を行ってくれます。
つまり、observerのvideoTileWasRemovedにひっかかったときは問答無用で画面オフとみなす, 退出検知のコードに引っかかった時は退出とみなすという区別ができるわけですね。(画面オフ専用のobserverもできればつくってほしいけど...)

録画の仕方が特殊

ここが現状最大のWeak Pointだと思うのですが、Chime SDKは画面および音声の録画機能を標準サポートしていません。

代替策として、公式が録画APIのデモを提供しているのでこちらを利用することになります。
ただ、この録画APIの仕組みは「指定したURLの画面および音声をキャプチャする」ことですので、実際の通話アプリとは別に録画用のページを用意する必要があります。

余談ですが、このデモではec2のt3.xlargeが2台たちあがります。放置しているとお財布にかなりの打撃が入るので、READMEにも書いている通り、使用後は必ず全て削除するようにしましょう。 (私はここに1ヶ月気づかず数万円ふっとばしました :moneybag:

結論 「通話アプリにAmazon Chimeを使うのは有効か?」

なんだかんだと文句は書いてきましたが、基本的には工夫すれば通話アプリに必要なほとんどの機能は実装できるため、かなりつかえるSDKだと感じました。
特に以下のような場合はまよわずChime SDKを使っていいのではないかと思います。

  1. 特殊なUIの通話アプリを作りたい(UIのカスタマイズが自由なSDKを使いたい)
  2. バックエンドの仕様やスペックは丸投げでいい(むしろまるなげしたい)
  3. 他のアプリに依存しない通話アプリを作りたい

コロナの影響で、通話アプリdeveloperの需要が高まると考えられる今日この頃、皆様も是非一度試してみてはいかがでしょうか?

PR

内定先のCyberAgentでは22卒のエンジニア採用を行っています!
https://www.cyberagent.co.jp/careers/news/detail/id=25511
皆様のご応募お待ちしております!

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

⑭本番環境でデータベースを作成ーポートフォリオ

データベースの種類

  • 階層型データベース
  • ネットワーク型データベース
  • リレーショナルデータベース

この中でもっとも利用されているのが「リレーショナルデータベース」です。
エクセルの表のような形で情報を整理し、管理できます。そして、このリレーショナルデータベースを管理するソフトウェアは「リレーショナル・データ・ベース・マネジメント・システム(RDBMS)」と呼ばれます。
そんなRDBMSの中でも代表的なものの1つが「MySQL」です。

MySQL
データベースの作成、編集、削除などを行うことができます。オープンソースソフトウェアとして公開されており、誰でも無償で利用できます。
Ruby on Railsと合わせ利用することができます。

MariaDB
「MariaDB」とは、MySQLの派生として開発されているオープンソースソフトウェアです。MySQLとの互換性があります。今回デプロイカリキュラムで使用しているAmazon Linux 2ではMariaDBを使用することになっています。

基本的にMariaDBとMySQLは同様のものと考える

MariaDBをインストール

[ec2-user@ip-172-31-25-189 ~]$ sudo yum -y install mysql56-server mysql56-devel mysql56 mariadb-server mysql-devel
[ec2-user@ip-172-31-25-189 ~]$ sudo systemctl start mariadb

データベースのrootパスワードの設定
yumでインストールしたMariaDBには、デフォルトで「root」というユーザーでアクセスできるようになっていますが、パスワードは設定されていません。なので、パスワードを設定する必要があります。

[ec2-user@ip-172-31-25-189 ~]$ sudo /usr/bin/mysql_secure_installation
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む