20210726のAWSに関する記事は16件です。

AWS Cloud9 でAmazon Linux 2でLaravelの設定

Amazon Linux2 ではEPELが使えない EPELが何かは知りません。調べたら追記します! とりあえず仕様している教材に amazon linuxを使ってください。linux 2ではEPELが使えないので。 みたいに書いてあったけど、 AWSのCloud9でワークスペースを作成する際に、 Amazon Linux 2 (recommended) と合ったので、せっかくなら推奨版が使いたいなということで、環境を少しアレンジ。 まああとは自分で調べてEPELが使えればいいだろうという考え。というか推奨版なんだから当然使えるっしょ!の乗りです。 手動でEPELを有効にする この通りにやれば大丈夫っぽい。 sudo amazon-linux-extras install epel -y phpのバージョン確認 $ php -v PHP 7.2.24 (cli) (built: Oct 31 2019 18:27:08) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies 2021/07/26の段階では7.2.24が標準でインストールされるらしい。 amazon linuxの方だと5系になるのかな?? composeのインストール $ curl -sS https://getcomposer.org/installer | php All settings correct for using Composer Downloading... Composer (version 2.1.5) successfully installed to: /home/ec2-user/environment/composer.phar Use it: php composer.phar # このままだとcomposerのパスが通ってない? $ composer bash: composer: command not found # composer.pharを移動してパスを通す $ sudo mv composer.phar /usr/local/bin/composer # 再確認 $ composer ______ / ____/___ ____ ___ ____ ____ ________ _____ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/ / /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ / \____/\____/_/ /_/ /_/ .___/\____/____/\___/_/ /_/ Composer version 2.1.5 2021-07-23 10:35:47 Usage: command [options] [arguments] Options: -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n, --no-interaction Do not ask any interactive question --profile Display timing and memory usage information --no-plugins Whether to disable plugins. -d, --working-dir=WORKING-DIR If specified, use the given directory as working directory. --no-cache Prevent use of the cache -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: about Shows a short information about Composer. archive Creates an archive of this composer package. browse Opens the package's repository URL or homepage in your browser. cc Clears composer's internal package cache. check-platform-reqs Check that platform requirements are satisfied. clear-cache Clears composer's internal package cache. clearcache Clears composer's internal package cache. config Sets config options. create-project Creates new project from a package into given directory. depends Shows which packages cause the given package to be installed. diagnose Diagnoses the system to identify common errors. dump-autoload Dumps the autoloader. dumpautoload Dumps the autoloader. exec Executes a vendored binary/script. fund Discover how to help fund the maintenance of your dependencies. global Allows running commands in the global composer dir ($COMPOSER_HOME). help Displays help for a command home Opens the package's repository URL or homepage in your browser. i Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json. info Shows information about packages. init Creates a basic composer.json file in current directory. install Installs the project dependencies from the composer.lock file if present, or falls back on the composer.json. licenses Shows information about licenses of dependencies. list Lists commands outdated Shows a list of installed packages that have updates available, including their latest version. prohibits Shows which packages prevent the given package from being installed. reinstall Uninstalls and reinstalls the given package names remove Removes a package from the require or require-dev. require Adds required packages to your composer.json and installs them. run Runs the scripts defined in composer.json. run-script Runs the scripts defined in composer.json. search Searches for packages. self-update Updates composer.phar to the latest version. selfupdate Updates composer.phar to the latest version. show Shows information about packages. status Shows a list of locally modified packages. suggests Shows package suggestions. u Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file. update Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file. upgrade Upgrades your dependencies to the latest version according to composer.json, and updates the composer.lock file. validate Validates a composer.json and composer.lock. why Shows which packages cause the given package to be installed. why-not Shows which packages prevent the given package from being installed. いけた! Laravelプロジェクトを作成 $ composer create-project laravel/laravel ./laravel-quest "5.5.*" --prefer-distCreating a "laravel/laravel" project at "./laravel-quest" Installing laravel/laravel (v5.5.28) - Downloading laravel/laravel (v5.5.28) - Installing laravel/laravel (v5.5.28): Extracting archive Created project in /home/ec2-user/environment/./laravel-quest > @php -r "file_exists('.env') || copy('.env.example', '.env');" Loading composer repositories with package information Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - laravel/framework[v5.5.0, ..., v5.5.50] require ext-mbstring * -> it is missing from your system. Install or enable PHP's mbstring extension. - Root composer.json requires laravel/framework 5.5.* -> satisfiable by laravel/framework[v5.5.0, ..., v5.5.50]. - なんか問題が発生している。。。 laravel/framework[v5.5.0, ..., v5.5.50] require ext-mbstring * -> it is missing from your system. Install or enable PHP's mbstring extension. extensionが無い、もしくは有効になっていないということですね。 ちなみにこの状態でもLaravelプロジェクトは作成されています。 しかし起動しようとするとできません。 $ php artisan serve --port=80 PHP Warning: require(/home/ec2-user/environment/laravel-quest/vendor/autoload.php): failed to open stream: No such file or directory in /home/ec2-user/environment/laravel-quest/artisan on line 18 PHP Fatal error: require(): Failed opening required '/home/ec2-user/environment/laravel-quest/vendor/autoload.php' (include_path='.:/usr/share/pear:/usr/share/php') in /home/ec2-user/environment/laravel-quest/artisan on line 18 autoload.phpがないって言われてるんですね。 よくわかりませんが、serverが入ってないから起動できないということだと思います。 なので手動で入れる。 WebサーバApacheはamazon-linux-extrasにないらしい ここのとおりにやれば大丈夫でした。 # aparchのインストール $ sudo yum install -y httpd Loaded plugins: extras_suggestions, langpacks, priorities, update-motd amzn2-core | 3.7 kB 00:00:00 242 packages excluded due to repository priority protections Package httpd-2.4.48-2.amzn2.x86_64 already installed and latest version Nothing to do # 入ってるやん! # サーバの確認 $ httpd -v Server version: Apache/2.4.48 () // 入ってます! Server built: Jun 25 2021 18:53:37 # php関連のインストール $ sudo yum install php php-mbstring php-pdo php-gd php-xml Loaded plugins: extras_suggestions, langpacks, priorities, update-motd 242 packages excluded due to repository priority protections Package php-pdo-7.2.24-1.amzn2.0.1.x86_64 already installed and latest version Package php-xml-7.2.24-1.amzn2.0.1.x86_64 already installed and latest version Resolving Dependencies --> Running transaction check ---> Package php.x86_64 0:7.2.24-1.amzn2.0.1 will be installed ---> Package php-gd.x86_64 0:7.2.24-1.amzn2.0.1 will be installed --> Processing Dependency: libXpm.so.4()(64bit) for package: php-gd-7.2.24-1.amzn2.0.1.x86_64 --> Processing Dependency: libX11.so.6()(64bit) for package: php-gd-7.2.24-1.amzn2.0.1.x86_64 ---> Package php-mbstring.x86_64 0:7.2.24-1.amzn2.0.1 will be installed --> Processing Dependency: libonig.so.2()(64bit) for package: php-mbstring-7.2.24-1.amzn2.0.1.x86_64 --> Running transaction check ---> Package libX11.x86_64 0:1.6.7-3.amzn2.0.2 will be installed --> Processing Dependency: libX11-common >= 1.6.7-3.amzn2.0.2 for package: libX11-1.6.7-3.amzn2.0.2.x86_64 --> Processing Dependency: libxcb.so.1()(64bit) for package: libX11-1.6.7-3.amzn2.0.2.x86_64 ---> Package libXpm.x86_64 0:3.5.12-1.amzn2.0.2 will be installed ---> Package oniguruma.x86_64 0:5.9.6-1.amzn2.0.4 will be installed --> Running transaction check ---> Package libX11-common.noarch 0:1.6.7-3.amzn2.0.2 will be installed ---> Package libxcb.x86_64 0:1.12-1.amzn2.0.2 will be installed --> Processing Dependency: libXau.so.6()(64bit) for package: libxcb-1.12-1.amzn2.0.2.x86_64 --> Running transaction check ---> Package libXau.x86_64 0:1.0.8-2.1.amzn2.0.2 will be installed --> Finished Dependency Resolution Dependencies Resolved ====================================================================================================== Package Arch Version Repository Size ====================================================================================================== Installing: php x86_64 7.2.24-1.amzn2.0.1 amzn2extra-lamp-mariadb10.2-php7.2 2.9 M php-gd x86_64 7.2.24-1.amzn2.0.1 amzn2extra-lamp-mariadb10.2-php7.2 179 k php-mbstring x86_64 7.2.24-1.amzn2.0.1 amzn2extra-lamp-mariadb10.2-php7.2 501 k Installing for dependencies: libX11 x86_64 1.6.7-3.amzn2.0.2 amzn2-core 606 k libX11-common noarch 1.6.7-3.amzn2.0.2 amzn2-core 165 k libXau x86_64 1.0.8-2.1.amzn2.0.2 amzn2-core 29 k libXpm x86_64 3.5.12-1.amzn2.0.2 amzn2-core 57 k libxcb x86_64 1.12-1.amzn2.0.2 amzn2-core 216 k oniguruma x86_64 5.9.6-1.amzn2.0.4 amzn2-core 127 k Transaction Summary ====================================================================================================== Install 3 Packages (+6 Dependent packages) Total download size: 4.7 M Installed size: 16 M Is this ok [y/d/N]: y Downloading packages: (1/9): libX11-common-1.6.7-3.amzn2.0.2.noarch.rpm | 165 kB 00:00:00 (2/9): libXau-1.0.8-2.1.amzn2.0.2.x86_64.rpm | 29 kB 00:00:00 (3/9): libXpm-3.5.12-1.amzn2.0.2.x86_64.rpm | 57 kB 00:00:00 (4/9): libX11-1.6.7-3.amzn2.0.2.x86_64.rpm | 606 kB 00:00:00 (5/9): oniguruma-5.9.6-1.amzn2.0.4.x86_64.rpm | 127 kB 00:00:00 (6/9): libxcb-1.12-1.amzn2.0.2.x86_64.rpm | 216 kB 00:00:00 (7/9): php-gd-7.2.24-1.amzn2.0.1.x86_64.rpm | 179 kB 00:00:00 (8/9): php-7.2.24-1.amzn2.0.1.x86_64.rpm | 2.9 MB 00:00:00 (9/9): php-mbstring-7.2.24-1.amzn2.0.1.x86_64.rpm | 501 kB 00:00:00 ------------------------------------------------------------------------------------------------------ Total 10 MB/s | 4.7 MB 00:00:00 Running transaction check Running transaction test Transaction test succeeded Running transaction Installing : oniguruma-5.9.6-1.amzn2.0.4.x86_64 1/9 Installing : libXau-1.0.8-2.1.amzn2.0.2.x86_64 2/9 Installing : libxcb-1.12-1.amzn2.0.2.x86_64 3/9 Installing : libX11-common-1.6.7-3.amzn2.0.2.noarch 4/9 Installing : libX11-1.6.7-3.amzn2.0.2.x86_64 5/9 Installing : libXpm-3.5.12-1.amzn2.0.2.x86_64 6/9 Installing : php-gd-7.2.24-1.amzn2.0.1.x86_64 7/9 Installing : php-mbstring-7.2.24-1.amzn2.0.1.x86_64 8/9 Installing : php-7.2.24-1.amzn2.0.1.x86_64 9/9 Verifying : libX11-1.6.7-3.amzn2.0.2.x86_64 1/9 Verifying : php-mbstring-7.2.24-1.amzn2.0.1.x86_64 2/9 Verifying : libxcb-1.12-1.amzn2.0.2.x86_64 3/9 Verifying : libX11-common-1.6.7-3.amzn2.0.2.noarch 4/9 Verifying : php-7.2.24-1.amzn2.0.1.x86_64 5/9 Verifying : libXpm-3.5.12-1.amzn2.0.2.x86_64 6/9 Verifying : libXau-1.0.8-2.1.amzn2.0.2.x86_64 7/9 Verifying : php-gd-7.2.24-1.amzn2.0.1.x86_64 8/9 Verifying : oniguruma-5.9.6-1.amzn2.0.4.x86_64 9/9 Installed: php.x86_64 0:7.2.24-1.amzn2.0.1 php-gd.x86_64 0:7.2.24-1.amzn2.0.1 php-mbstring.x86_64 0:7.2.24-1.amzn2.0.1 Dependency Installed: libX11.x86_64 0:1.6.7-3.amzn2.0.2 libX11-common.noarch 0:1.6.7-3.amzn2.0.2 libXau.x86_64 0:1.0.8-2.1.amzn2.0.2 libXpm.x86_64 0:3.5.12-1.amzn2.0.2 libxcb.x86_64 0:1.12-1.amzn2.0.2 oniguruma.x86_64 0:5.9.6-1.amzn2.0.4 Complete! # 以下を実行してautoload.phpを作成する $ composer install # 以下でサーバが起動できます。。。が!ブラウザは開けません! $ php artisan serve --port=8080 No application encryption key has been specified.が発生してブラウザが開けない https://qiita.com/ponsuke0531/items/197c76fcb9300d7c5f36 ここのとおりにやったら大丈夫です。 $ cat .env | grep ^APP_ APP_NAME=Laravel APP_ENV=local APP_KEY= APP_DEBUG=true APP_LOG_LEVEL=debug APP_URL=http://localhost # 以下を実行するとAPP_KEYに自動的に追記される $ php artisan key:generate $ cat .env | grep ^APP_ APP_NAME=Laravel APP_ENV=local APP_KEY=base64:dvObQrZ9xEdYJaaaaaaaaaa/uFU3ADAsKtxxxxxxxxx= APP_DEBUG=true APP_LOG_LEVEL=debug APP_URL=http://localhost # 設定ファイルのキャッシュを再作成する $ php artisan config:cache Configuration cache cleared! Configuration cached successfully! これでブラウザが起動できるはず! やっとスタートラインです!頑張りましょう!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【5日目】AWS認定ソリューションアーキテクト合格までの道

ネットワークサービス アクセス制御 セキュリティグループとネットワークACL インバウンド通信とアウトバウンド通信を設定するファイアウォール機能 項目 セキュリティグループ ネットワークACL 適用範囲 インスタンス単位 サブネット単位 デフォルトの動作 インバウンド:全拒否 アウトバウンド:全許可 インバウンド:全許可 アウトバウンド:全許可 ルールの評価 全てのルールに適用 ルールの番号順で適用 ステータス ステートフル ステートレス セキュリティグループ 特徴 インバウンド/アウトバウンドの種別/プロトコル/ポート範囲/アクセス元/アクセス先IPなどのルールのみ定義する EC2インスタンスに複数のセキュリティグループの適用が可能 セキュリティグループの設定追加/変更は即時反映 ステートフルな制御 ルールで許可された通信の戻りの通信も自動的に許可される ネットワークACL 特徴 VPC内に構成したサブネットごとに一つのネットワークACLを設定可能 VPC作成時にデフォルトのネットワークACLが準備されている 新規にネットワークACL(カスタムネットワークACL)を作成できるが、その時の初期設定は全てのトラフィックを拒否 異なるサブネット間の通信の許可または拒否を明示した制御が可能 ステートレスな制御 アウトバウンドとインバウンド通信それぞれに対する通信制御の設定が必要 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

俺流AWS Lambdaデプロイ+IaCのベストプラクティスを考える

概要 Lambdaをコード管理したいのですが、Lambdaのベストプラクティス で良い感じのものがないと思い困り調べてみました。 以下のデプロイ方法がありますが、どれも個人的にはいまいちなため悩んでいました。 AWS CLIでのデプロイする 独自のデプロイツール(Lambdaroll)を使う https://tech.toreta.in/entry/2020/12/05/000000 ServerlessFrameworkを使う AWS CDKでデプロイする https://zenn.dev/faycute/articles/be5599fc093511 課題感 サーバレス関連のツールはどうしても構成管理ツールと紐づいてしまっているのでどうしたものかなぁと思っていました。 例えば、AWS CDKでもServerlessFrameworkでもバックエンドではCloudFormationで動いてしまっています。このようなツールのメリットは1リポジトリでインフラの構成管理とコード管理が完結することがメリットだと思っています。ただ、既存インフラ環境のIaCにTerraformを使っている場合、TerraformとCloudFormationのダブルスタンダードになってしまい個人的には気持ち悪いなと思っています。 それを解消する方法をこの記事では考えていきたいです。 Lambdaリソースの基本設計 設計方針 Lambdaのデプロイ方法はソースコードをzipで固めてS3に置くか、ECRにDocker Imageをプッシュするかを現在は選ぶことができます。 保守性が高く、どの環境でもちゃんとアプリケーションのデバッグができるという点ではDocker Imageの方がいいので、LambdaコードはECRにプッシュすることを選びます。 ちなみにサーバレスアプリケーションを作るときは、アプリケーションごとにリポジトリを作る方が良いとAWSの中の人に聞いたことがありますが、今回の用途としてはヘッダー書替え用やIP制限等のLambdaを扱いたいため1リポジトリでディレクトリを分けてデプロイします。 Lambdaリソースの管理方針 インフラリソースはTerraformで管理 Lambdaへのデプロイはソースコード側で行う CI/CDはGitHub Actionsで行う インフラリソースをTerraform管理にする ここではLambda Functionの管理とIAM、ECRの管理をします。 # IAM resource "aws_iam_role" "lambda_basic" { name = "lambda-basic-role" path = "/service-role/" assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json } data "aws_iam_policy_document" "lambda_assume_role" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["lambda.amazonaws.com", "edgelambda.amazonaws.com"] } } } resource "aws_iam_role_policy_attachment" "lambda_basic" { role = aws_iam_role.lambda_basic.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } # ECR resource "aws_ecr_repository" "lambda1" { name = "lambda-function1" image_tag_mutability = "IMMUTABLE" image_scanning_configuration { scan_on_push = true } } # Lambda Function resource "aws_lambda_function" "lambda1" { function_name = "lambda-function1" role = aws_iam_role.lambda_basic.arn package_type = "Image" image_uri = "${aws_ecr_repository.lambda1.repository_url}:latest" timeout = 60 lifecycle { ignore_changes = [image_uri] } } # CloudWatch Logs ## ref https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/monitoring-cloudwatchlogs.html resource "aws_cloudwatch_log_group" "lambda1" { name = "/aws/lambda/${aws_lambda_function.lambda1.function_name}" retention_in_days = 30 } Lambdaコードを作成する 以下のようなディレクトリにコードを配置します。 言語はnode.jsにします。 $ tree . └── lambda-function1 ├── Dockerfile ├── index.js ├── package-lock.json └── package.json 各ファイルの中身は以下の通りです。 { "name": "lambda-container-image-example", "license": "MIT" } npm install して package-lock.json は作っておいてください。 index.js のコード内容は以下の通り。 exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; const headerNameSrc = 'X-Amz-Meta-Last-Modified'; const headerNameDst = 'Last-Modified'; if (headers[headerNameSrc.toLowerCase()]) { headers[headerNameDst.toLowerCase()] = [ headers[headerNameSrc.toLowerCase()][0], ]; console.log(`Response header "${headerNameDst}" was set to ` + `"${headers[headerNameDst.toLowerCase()][0].value}"`); } callback(null, response); }; Dockerfileはこの通り # AWS ベースイメージを使用 FROM public.ecr.aws/lambda/nodejs:14 # ソースコードを関数のルートディレクトリにコピーします。 # 関数のルートディレクトリは `LAMBDA_TASK_ROOT` 環境変数を上書きすることで変更することができます ( デフォルトは `/var/task` ) 。 COPY index.js package.json package-lock.json /var/task/ RUN npm install # CMD にハンドラを設定します。 # Node.js の場合は `{ファイル名(拡張子なし)}.{関数名}` のように指定します。 # 今回は `index.js` の `handler` 関数をハンドラとして用意しているので以下のようになります。 CMD ["index.handler"] GitHub Actionsのworkflowを作成する Lambdaのソースコードおいてあるリポジトリでこちらのコードを設定してください。 GitHub Actionsで特定のディレクトリで操作された時にデプロイされるように設定してます。 name: "Deploy" on: push: branches: - main paths: - "lambda-function1/**" jobs: lambda: name: "Deploy Lambda" runs-on: ubuntu-latest defaults: run: shell: bash working-directory: ./lambda-function1/ steps: - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 1 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: docker build & docker push & lambda update run: | docker build -t XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }} . docker push XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }} aws lambda update-function-code --function-name lambda-function1 --image-uri XXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-function1:${{ github.sha }} Tips GitHub Actionsのマーケットプレイスにあるものは使えない GitHubActionsの公式にLambdaデプロイ用のコードがありますが、これはimage対応していないのでエラーになります。 参考資料: lambda-action 大元はこちらのコードですが、ファイルのみの対応になってしまっています。 image_uri だけ指定してデプロイを試みても400エラーになります。 参考資料: drone-lambda 結論 ServerlessFrameworkのようにそのフレームワークだけ使えばインフラのことを管理しなくても良いのですが、元々のインフラ設計がサーバーレスを中心とした設計でない場合、余計なインフラリソースが勝手に作られて運用が辛い経験がありました。 この方法を使えば、TerraformとLambdaのデプロイを分離できますし、ServerlessFrameworkでできるような余計なインフラリソースが増えてしまう心配がありませんし、不要になれば、Terraformから削除すればAWS環境も綺麗な状態を保てるのでお勧めです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS】RDS for PostgreSQLの「Collate」と「Ctype」を「ja_JP.UTF-8」に変更してみました

はじめに RDS for PostgreSQL 作成後に、デフォルトのCollateとCtypeはen_US.UTF-8になっていますが、en_US.UTF-8をja_JP.UTF-8に変更することが本記事の主旨です。 ちなみに、今回使用しているEC2とRDSの情報は下記となります。 ・EC2: Amazon Linux 2 ・RDS for PostgreSQLエンジンバージョン:12.6 作業手順 1.postgresql接続ミドルウェアをインストールします ※バージョンはRDS postgresqlに準じてインストールすること $ sudo amazon-linux-extras install epel -y $ sudo amazon-linux-extras install postgresql12 2.dbtestDBの現状確認 下記ご覧のように、現在dbtestのCollateとCtypeはen_US.UTF-8になっています。 dbtest=> \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+-------------+----------+-------------+-------------+----------------------------- dbtest | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | postgres | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin template0 | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin + | | | | | rdsadmin=CTc/rdsadmin template1 | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/XXXXXXXXXXX + | | | | | XXXXXXXXXXX=CTc/XXXXXXXXXXX (5 rows) 3.dbtestにテスト用のデータをインポートします dbtest=> CREATE TABLE user_tbl(name VARCHAR(20), signup_date DATE); CREATE TABLE dbtest=> INSERT INTO user_tbl(name, signup_date) VALUES('田中', '2021-07-26'); INSERT 0 1 dbtest=> INSERT INTO user_tbl(name, signup_date) VALUES('鈴木', '2021-07-26'); INSERT 0 1 dbtest=> INSERT INTO user_tbl(name, signup_date) VALUES('太郎', '2021-07-26'); INSERT 0 1 dbtest=> SELECT * FROM user_tbl; name | signup_date ------+------------- 田中 | 2021-07-26 鈴木 | 2021-07-26 太郎 | 2021-07-26 (3 rows) 4.dbtestDBのDumpファイルをエクスポートします [root@ip-XXX-XXX-XXX-XXX ~]# pg_dump -h <RDSエンドポイント> -U<RDSユーザネーム> <DB名> > /tmp/db_bk.dump 5.Dumpファイルがエクスポートされるかどうかを確認します [root@ip-XXX-XXX-XXX-XXX ~]# cd /tmp [root@ip-XXX-XXX-XXX-XXX tmp]# ll db_bk.dump -rw-r--r-- 1 root root 1213 Jul 26 09:35 db_bk.dump 6.dbtestからpostgresへ移動 dbtest=> \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+-------------+----------+-------------+-------------+----------------------------- dbtest | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | postgres | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin template0 | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin + | | | | | rdsadmin=CTc/rdsadmin template1 | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/XXXXXXXXXXX + | | | | | XXXXXXXXXXX=CTc/XXXXXXXXXXX (5 rows) dbtest=> \c postgres Password: psql (12.7, server 12.6) SSL connection (protocol: TLSv1.2, cipher: XXXX-XXXX-XXXX-XXXX-XXXX, bits: 256, compression: off) You are now connected to database "postgres" as user "XXXXXXXX". postgres=> 7.既存のdbtestを削除します postgres=> DROP DATABASE dbtest; DROP DATABASE postgres=> \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+-------------+----------+-------------+-------------+----------------------------- postgres | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin template0 | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin + | | | | | rdsadmin=CTc/rdsadmin template1 | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/XXXXXXXXXXX + | | | | | XXXXXXXXXXX=CTc/XXXXXXXXXXX (4 rows) postgres=> 8.dbtestを再作成します。 下記ご覧のように、CollateとCtypeはja_JP.UTF-8に変更しました。 postgres=> CREATE DATABASE dbtest LC_COLLATE 'ja_JP.UTF-8' LC_CTYPE 'ja_JP.UTF-8' ENCODING 'UTF8' TEMPLATE template0; CREATE DATABASE postgres=> \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+-------------+----------+-------------+-------------+----------------------------- dbtest | XXXXXXXXXXX | UTF8 | ja_JP.UTF-8 | ja_JP.UTF-8 | postgres | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin template0 | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin + | | | | | rdsadmin=CTc/rdsadmin template1 | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/XXXXXXXXXXX + | | | | | XXXXXXXXXXX=CTc/XXXXXXXXXXX (5 rows) 9.バックアップしたDumpファイルを上記新規作成されたdbtestへインポートします [root@ip-XXX-XXX-XXX-XXX ~]# psql --host=<エンドポイント> --username=<ユーザネーム> --password --dbname=<DB名> < /tmp/db_bk.dump Password: SET SET SET SET SET set_config ------------ (1 row) SET SET SET SET SET SET CREATE TABLE ALTER TABLE COPY 3 REVOKE REVOKE GRANT GRANT 10.確認 dbtest=> \l List of databases Name | Owner | Encoding | Collate | Ctype | Access privileges -----------+-------------+----------+-------------+-------------+----------------------------- dbtest | XXXXXXXXXXX | UTF8 | ja_JP.UTF-8 | ja_JP.UTF-8 | postgres | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | rdsadmin=CTc/rdsadmin template0 | rdsadmin | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/rdsadmin + | | | | | rdsadmin=CTc/rdsadmin template1 | XXXXXXXXXXX | UTF8 | en_US.UTF-8 | en_US.UTF-8 | =c/XXXXXXXXXXX + | | | | | XXXXXXXXXXX=CTc/XXXXXXXXXXX (5 rows) dbtest=> \d List of relations Schema | Name | Type | Owner --------+----------+-------+------------- public | user_tbl | table | XXXXXXXXXXX (1 row) dbtest=> select * from user_tbl; name | signup_date ------+------------- 田中 | 2021-07-26 鈴木 | 2021-07-26 太郎 | 2021-07-26 (3 rows) 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS Lambda×LINE Messaging API】AWS SAMで翻訳アプリを作ろう

はじめに 皆さん、Lambdaをご存知でしょうか? Lambdaはサーバーレスアーキテクチャを実現する上で根幹となるサービスです。 サーバーレスアーキテクチャとは AWSにおけるサーバーレスとは、「インスタンスベースの仮想サーバー(EC2など)を使わずにアプリケーションを開発するアーキテクチャ」を指します。 一般にシステムの運用には、プログラムを動かすためのサーバーが必要です。 そしてそのサーバーは、常に稼働していなければなりません。 しかし開発者がやりたいことは、「サーバーの管理」なのでしょうか? エンドユーザーに価値を届けることこそが使命なわけです。 ということで、こういうめんどくさい作業から解放してくれるのがサーバーレスアーキテクチャなわけです。 サーバーレスアーキテクチャでよく使われるサービスは以下の通りです。 特に、丸で囲っている3つがよく使われます。 ということで、この3つ全てを使った翻訳アプリを作りたいと思います。 また、構成やデプロイはAWS SAMを使用します。 AWS SAMを使うことでコマンドのみで環境構築やデプロイを行えます。 アーキテクチャ 以下の2つの条件を満たしたら成功です。 ①LINEで「こんにちは」と入力したら、「Hello」と返ってくる ②タイムスタンプと「こんにちは」、「Hello」がDBに保存される GitHub 完成形のコードは以下となります。 ハンズオン 前提 初めてAWSを使う方に対しての注意です。 ルートユーザーで行うのはよろしくないので、全ての権限を与えたAdministratorユーザーを作っておいてください。 公式サイトはこちらです。 文章は辛いよって方は、初学者のハンズオン動画があるのでこちらからどうぞ。 sam initを実行する ゼロから書いていってもいいのですが、初めての方はまずはsam initを使いましょう。 以下のように選択していってください。 ターミナル $ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 What package type would you like to use? 1 - Zip (artifact is a zip uploaded to S3) 2 - Image (artifact is an image uploaded to an ECR image repository) Package type: 1 Which runtime would you like to use? 1 - nodejs14.x 2 - python3.8 3 - ruby2.7 4 - go1.x 5 - java11 6 - dotnetcore3.1 7 - nodejs12.x 8 - nodejs10.x 9 - python3.7 10 - python3.6 11 - python2.7 12 - ruby2.5 13 - java8.al2 14 - java8 15 - dotnetcore2.1 Runtime: 7 Project name [sam-app]: Translate AWS quick start application templates: 1 - Hello World Example 2 - Step Functions Sample App (Stock Trader) 3 - Quick Start: From Scratch 4 - Quick Start: Scheduled Events 5 - Quick Start: S3 6 - Quick Start: SNS 7 - Quick Start: SQS 8 - Quick Start: App Backend using TypeScript 9 - Quick Start: Web Backend Template selection: 1 ここまでできれば作成されます。 このような構成になっていればOKです。 .Translate ├── events/ │ ├── event.json ├── hello-world/ │ ├── tests │ │ └── integration │ │ │ └── test-api-gateway.js │ │ └── unit │ │ │ └── test-handler.js │ ├── .npmignore │ ├── app.js │ ├── package.json ├── .gitignore ├── README.md ├── template.yaml 必要ないファイルなどがあるのでそれを削除していきましょう。 .Translate ├── hello-world/ │ ├── app.js ├── .gitignore ├── README.md ├── template.yaml また、ディレクトリ名やファイル名を変えましょう。 .Translate ├── api/ │ ├── index.js ├── .gitignore ├── README.md ├── template.yaml 次は、template.yamlを修正して、SAMの実行をしてみたいところですが、一旦後回しにします。 先にTypeScriptなどのパッケージを入れ、ディレクトリ構造を明確にした後の方が理解しやすいので。。 ということでパッケージを入れていきましょう。 package.jsonの作成 以下のコマンドを入力してください。 これで、package.jsonの作成が完了します。 ターミナル $ npm init -y 必要なパッケージのインストール dependencies dependenciesはすべてのステージで使用するパッケージです。 今回使用するパッケージは以下の4つです。 ・@line/bot-sdk ・aws-sdk 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install @line/bot-sdk aws-sdk --save ちなみに、Lambdaでは元よりaws-sdkが使えるようなのでなくても問題ないです。 インストールしなければその分容量が軽くなるので、レスポンスは早くなります。 devDependencies devDependenciesはコーディングステージのみで使用するパッケージです。 今回使用するパッケージは以下の5つです。 ・typescript ・@types/node ・ts-node ・rimraf ・npm-run-all 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install -D typescript @types/node ts-node rimraf npm-run-all package.jsonにコマンドの設定を行う npm run buildでコンパイルを行います。 package.json { "scripts": { "clean": "rimraf dist", "tsc": "tsc", "build": "npm-run-all clean tsc" }, } tsconfig.jsonの作成 以下のコマンドを実行しTypeScriptの初期設定を行います。 ターミナル $ npx tsc --init それでは、作成されたtsconfig.jsonの上書きをしていきます。 tsconfig.json { "compilerOptions": { "target": "ES2018", "module": "commonjs", "sourceMap": true, "outDir": "./api/dist", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["api/src/*"] } 簡単にまとめると、 api/srcディレクトリ以下を対象として、それらをapi/distディレクトリにES2018の書き方でビルドされるという設定です。 tsconfig.jsonに関して詳しく知りたい方は以下のサイトをどうぞ。 また、この辺りで必要ないディレクトリはGithubにpushしたくないので、.gitignoreも作成しておきましょう。 .gitignore node_modules package-lock.json .aws-sam samconfig.toml dist 最終的にはこのようなディレクトリ構成にしましょう。 .WeatherFashion ├── api/ │ ├── dist(コンパイル後) │ │ └── node_modules(コピーする) │ │ └── package.json(コピーする) │ ├── src(コンパイル前) │ │ └── index.ts ├── node_modules(コピー元) ├── .gitignore ├── package.json(コピー元) ├── package-lock.json ├── README.md ├── template.yaml ├── tsconfig.json やるべきことは以下の2つです。 ①distディレクトリを作成する ②distディレクトリに、node_modules, package.jsonをコピーする 次に、template.yamlを書いていきましょう。 SAM Templateを記載する ファイル内にコメントを残しています。 これで大まかには理解できるかと思います。 詳しくは公式サイトを見てください。 template.yaml # AWS CloudFormationテンプレートのバージョン AWSTemplateFormatVersion: '2010-09-09' # CloudFormationではなくSAMを使うと明記する Transform: AWS::Serverless-2016-10-31 # CloudFormationのスタックの説明文(重要ではないので適当でOK) Description: > Translate Globals: # Lambda関数のタイムアウト値(3秒に設定) Function: Timeout: 3 Resources: # API Gateway TranslateAPI: # Typeを指定する(今回はAPI Gateway) Type: AWS::Serverless::Api Properties: # ステージ名(APIのURLの最後にこのステージ名が付与されます) StageName: v1 # DynamoDB TranslateDynamoDB: # Typeを指定する(今回はDynamoDB) Type: AWS::Serverless::SimpleTable Properties: # テーブルの名前 TableName: translations # プライマリキーの設定(名前とプライマリキーのタイプ) PrimaryKey: Name: TimeStamp Type: String # プロビジョニングされたキャパシティの設定(今回の要件では最小の1でOK) ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 # Lambda TranslateFunction: # Typeを指定する(今回はLambda) Type: AWS::Serverless::Function Properties: # 関数が格納されているディレクトリ(今回はコンパイル後なので、distディレクトリを使用する) CodeUri: api/dist # ファイル名と関数名(今回はファイル名がindex.js、関数名がexports.handlerなので、index.handlerとなります) Handler: index.handler # どの言語とどのバージョンを使用するか Runtime: nodejs12.x # ポリシーを付与する(今回はLambdaの権限とSSMの読み取り権限とDynamoDBのフルアクセス権限とAmazon translateのフルアクセス権限を付与) Policies: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess - arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess - arn:aws:iam::aws:policy/TranslateFullAccess # この関数をトリガーするイベントを指定します Events: # API Gateway TranslateAPI: Type: Api Properties: # どのAPIを使用するか(!Refは値の参照に使用します) RestApiId: !Ref TranslateAPI # URL Path: / # POSTメソッド Method: post Outputs: TranslateAPI: Description: 'API Gateway' # URLを作成(!Subは${}で値を指定することができます) Value: !Sub 'https://${TranslateAPI}.execute-api.${AWS::Region}.amazonaws.com/v1' TranslateFunction: Description: 'Lambda' # ロールの値を返す Value: !GetAtt TranslateFunction.Arn TranslateFunctionIamRole: Description: 'IAM Role' # ロールの値を返す Value: !GetAtt TranslateFunctionRole.Arn LINE Developersにアカウントを作成する LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。 その後諸々入力してもらったら以下のように作成できるかと思います。 注意事項としては、今回Messaging APIとなるので、チャネルの種類を間違えた方は修正してください。 チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。 これで必要な環境変数は取得できました。 それでは、これをSSMを使ってLambda内で使えるようにしていきましょう。 SSMパラメータストアで環境変数を設定 なぜSSMパラメータストアを使うのか? SAMのLambda設定にも、環境変数の項目はあります。 しかし、2点問題点があります。 ①Lambdaの環境変数の変更をしたいとき、Lambdaのバージョンも新規発行をしなければならない ②Lambdaのバージョンとエイリアスを紐付けて管理をするとき、もし環境変数にリリース先環境別の値をセットしていると、リリース時に手動で環境変数の変更をしなければならないケースが発生する 簡単にまとめると、「リアルタイムで反映できないし、人為的なミスのリスクもあるよ」ということです。 SSMパラメータストアで値を管理すると以下の3点のメリットがあります。 ①Lambdaの環境変数の管理が不要 ②Lambdaも含めた値関連情報を一元管理できる ③Lambda外部からリアルタイムに環境変数を変更制御できる ということで、SSMパラメータストアを使用しましょう。 みんな大好きクラスメソッドの記事にやり方が書いてあります。 こちらの記事が完璧なのでこちらを見てやってみてください。 私は以下のように命名して作成しました。 SSMパラメータが取得できているかconsole.logで検証 api/src/index.ts // import import aws from 'aws-sdk'; // SSM const ssm = new aws.SSM(); exports.handler = async (event: any, context: any) => { const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; console.log('channelAccessToken: ' + channelAccessToken); }; これをコンパイルしてデプロイしていきましょう。 ターミナル // コンパイル $ npm run build // ビルド $ sam build // デプロイ $ sam deploy --guided Configuring SAM deploy ====================== Looking for samconfig.toml : Not found Setting default arguments for 'sam deploy' ========================================= // CloudFormation スタック名の指定 Stack Name [sam-app]: Translate // リージョンの指定 AWS Region [us-east-1]: ap-northeast-1 // デプロイ前にCloudformationの変更セットを確認するか #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: y // SAM CLI に IAM ロールの作成を許可するか(CAPABILITY_IAM) #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y // API イベントタイプの関数に認証が含まれていない場合、警告される HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y // この設定を samconfig.toml として保存するか Save arguments to samconfig.toml [Y/n]: y これでデプロイが完了します。 では、API GatewayのURLを確認しましょう。 Webhook URLの登録 先ほどAPI Gatewayで作成したhttpsのURLをコピーしてください。 これをLINE DevelopersのWebhookに設定します。 それではSSMパラメータが正しく取得できているか確認しましょう。 CloudWatchで確認しましょう! 取得できていますね! ここからの流れはこのような感じです。 ①翻訳機能を作成 ②翻訳された言葉をDBに保存 今回は、翻訳する部分、DBにデータを登録する部分と様々な機能があるため動作ごとにファイルを切り分けてあげましょう。 以下のように作っていきます。 . ├── api/ │ ├── src/ │ │ ├── Common/ │ │ └── getTranslate.ts │ │ └── putDynamoDB.ts │ └── index.ts またここからはLINEBotのオリジナルの型が頻出します。 1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。 ①翻訳機能を作成 api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent, TextMessage } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールのインストール import { getTranslate } from './Common/getTranslate'; // SSM const ssm = new aws.SSM(); const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_TRANSLATE_CHANNEL_SECRET = { Name: 'LINE_TRANSLATE_CHANNEL_SECRET', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { try { // SSM (.env) const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_TRANSLATE_CHANNEL_SECRET).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; // client const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // JSONとして解析して値やオブジェクトを構築する const body: any = JSON.parse(event.body); // LINE Eventを取得 const response: WebhookEvent = body.events[0]; // 送られるメッセージがテキスト以外の場合 if (response.type !== 'message' || response.message.type !== 'text') { return; } // 翻訳を行うために必要な情報 const input_text: string = response.message.text; const sourceLang: string = 'ja'; const targetLang: string = 'en'; const res: any = await getTranslate(input_text, sourceLang, targetLang); const output_text: string = res.TranslatedText; // メッセージ送信のために必要な情報 const replyToken = response.replyToken; const post: TextMessage = { type: 'text', text: output_text, }; // メッセージの送信 await client.replyMessage(replyToken, post); } catch (err) { console.log(err); } }; では次に、getTranslate.tsを作っていきましょう。 コードだけ書いても訳がわからないと思うので、リファレンスを見ましょう。 入力されたテキストをソース言語からターゲット言語に変換する、translateTextを使います。 必須項目は以下の3つで、SourceLanguageCodeに元の言語コード、TargetLanguageCodeに変換先の言語コード、Textに変換するテキストを入れればいいことがわかります。 SourceLanguageCode: 'STRING_VALUE', /* required */ TargetLanguageCode: 'STRING_VALUE', /* required */ Text: 'STRING_VALUE', /* required */ そのあとはこのデータを実行するだけです。 translate.translateText(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response }); APIが理解できたところで進めていきましょう。 api/src/Common/getTranslate.ts // パッケージのインストール import aws from 'aws-sdk'; // 必要なAWSサービス const translate = new aws.Translate(); export const getTranslate = (input: string, inLang: string, outLang: string) => { return new Promise((resolve, reject) => { // 必要なデータ const params = { Text: input, SourceLanguageCode: inLang, TargetLanguageCode: outLang, }; // 翻訳を行う translate.translateText(params, (err, data) => { if (err) { console.log(err); reject(); } else { resolve(data); } }); }); }; ②翻訳された言葉をDBに保存 api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent, TextMessage } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールのインストール import { getTranslate } from './Common/getTranslate'; import { putDynamoDB } from './Common/putDynamoDB'; // SSM const ssm = new aws.SSM(); const LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_TRANSLATE_CHANNEL_SECRET = { Name: 'LINE_TRANSLATE_CHANNEL_SECRET', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { try { // SSM (.env) const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_TRANSLATE_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_TRANSLATE_CHANNEL_SECRET).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; // client const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // JSONとして解析して値やオブジェクトを構築する const body: any = JSON.parse(event.body); // LINE Eventを取得 const response: WebhookEvent = body.events[0]; // 送られるメッセージがテキスト以外の場合 if (response.type !== 'message' || response.message.type !== 'text') { return; } // 翻訳を行うために必要な情報 const input_text: string = response.message.text; const sourceLang: string = 'ja'; const targetLang: string = 'en'; const res: any = await getTranslate(input_text, sourceLang, targetLang); const output_text: string = res.TranslatedText; // メッセージ送信のために必要な情報 const replyToken = response.replyToken; const post: TextMessage = { type: 'text', text: output_text, }; // メッセージの送信 await client.replyMessage(replyToken, post); // DB-タイムスタンプ const date = new Date(); const Y = date.getFullYear(); const M = ('00' + (date.getMonth() + 1)).slice(-2); const D = ('00' + date.getDate()).slice(-2); const h = ('00' + (date.getHours() + 9)).slice(-2); const m = ('00' + date.getMinutes()).slice(-2); const s = ('00' + date.getSeconds()).slice(-2); const dayTime = Y + M + D + h + m + s; // DynamoDB保存 await putDynamoDB(dayTime, input_text, output_text); } catch (err) { console.log(err); } }; 次に、putDynamoDB.tsを作ります。 コードだけ書いても訳がわからないと思うので、リファレンスを見ましょう。 アイテム(レコード)を作成したいので、putItemを使います。 必須項目は以下の3つで、Itemにデータ、ReturnConsumedCapacityに集計、TableNameにテーブルの名前を入れればいいことがわかります。 var params = { Item: { "AlbumTitle": { S: "Somewhat Famous" }, "Artist": { S: "No One You Know" }, "SongTitle": { S: "Call Me Today" } }, ReturnConsumedCapacity: "TOTAL", TableName: "Music" }; そのあとはこのデータを実行するだけです。 dynamodb.putItem(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response }); APIが理解できたところで進めていきましょう。 api/src/Common/putDynamoDB.ts // パッケージのインストール import aws from 'aws-sdk'; // 必要なAWSサービス const dynamodb = new aws.DynamoDB(); export const putDynamoDB = (dayTime: string, input: string, output: string) => { return new Promise((resolve, reject) => { const params = { Item: { TimeStamp: { S: dayTime, }, InputText: { S: input, }, OutputText: { S: output, }, }, ReturnConsumedCapacity: 'TOTAL', TableName: 'translations', }; dynamodb.putItem(params, (err, data) => { if (err) { console.log(err); reject(err); } else { resolve(data); } }); }); }; これで完成です! では、デプロイしていきましょう。 デプロイ まずは、npm run buildでコンパイルしましょう。 ターミナル $ npm run build コンパイルされた後は、ビルドしてデプロイしていきましょう。 ターミナル // ビルド $ sam build // デプロイ $ sam deploy --guided 最後に動作検証をしましょう。 DynamoDBも確認しましょう。 しっかり保存されていますね! 最後に 以前すべて手作業で行いましたが、SAMを使うと効率的にデプロイが行えます。 SAMテンプレートの書き方を学ぶコストは発生しますが、1度作ればそれをそのまま使えるので汎用性も高いのでおすすめです。 サーバーレスアーキテクチャを勉強する方がいましたらぜひSAMも勉強してみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

物理サーバのAWS移行注意点(CloudEndureを使ったP2V)

P2V実施の経緯 老朽化している物理サーバが1台残っているのですが、復旧できない故障が発生した場合に備えてP2V(P2C)を検討しました。CloudEndureにて簡単にP2Vできるとの記事を見て実行したのですが、以外にハマりましたので、その内容を記載したいと思います。 P2Vの流れ ■CloudEndureのアカウント作成       ↓ ■CloudEndure用のIAM作成       ↓ ■CloudEndure管理コンソールでプロジェクトの作成       ↓ ■CloudEndure管理コンソールで移行マシンの設定 ← ここがつまづいた原因       ↓ ■オンプレサーバへのエージェントインストール       ↓ ■P2Vしたサーバにリモート接続して動作テスト  ※スケジューラにタスクを登録している場合はタスクを無効にしておくこと   (そのまま動く可能性があります) 全体的な流れを解説しているサイトがありましたので、リンクを掲載させていただきます。 つまづいた点 ■オンプレサーバへのエージェントインストール CloudEndure管理コンソールからインストールエージェントのダウンロードしました。 コンソール上に表示してくれているコマンドをサーバのコマンドプロンプトに貼り付けて実行。 ↓↓コマンドの内容↓↓ installer_win.exe -t xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx-xxxx ← 16ケタのコード  --no-prompt が、うまくいきません。 移行対象のサーバがWindowsServer2008R2であった為、.NET Frameworkが古く、アップデートが必要でした。 ■.NET Framework4.5以上のインストール オフライン用.NET Framework4.5インストーラーをダウンロード https://bit.ly/35HWLWd 再度インストール用のコマンドをサーバのコマンドプロンプトに貼り付けて実行したところ、エージェントインストールは完了しました。 ■CloudEndure管理コンソールでレプリケーション情報を確認 エージェントがインストールされるとレプリケーションは自動で進みます。 CloudEndure管理コンソールでレプリケーション先のマシン設定を編集します。 Machinesタブから編集するマシンをクリックします。 BLUEPRINTタブ BLUEPRINTタブでレプリケーション先マシンの設定を確認します。 SubnetとSecurity groupsの初期値がCreate newになっている為、私は既存のSubnetとSecurity groupsを使うように変更しました。 Private IPは初期値がSOURCE MachineのIPとなっています。 オンプレのマシンはIP体系が違う為、今回はCreate newを選択しました。 Diskですが、初期値はProvisionedSSD-io1になっています。 汎用ボリュームで良いので、SSD-gp3に変更しました。 REPLICATION SETTINGSタブ Choose the Replication Server instance typeはDefaultを推奨 ■ここがつまづいた原因 最初、コストを節約したかったのでレプリケーション先サーバインスタンスを固定する為にMachineTypeをm5.largeに変更しました。後からわかりましたが、ここが問題だったようで、Defaultのままにしておいた方が良いようです。 理由として、1度目にm5.largeにしたことで起動できなかったのですが、2度目はDefaultでうまく立ち上がりました。その差として、自動で生成されたスペックがc4.4xlargeでした。試しにc5.4xlargeにしても起動できなくなりましたので、恐らくCPUの世代が古くてc4しか対応していなかったのだろうと予想しています。 Defaultであればこの差を自動で対応してくれる為、Defaultが良いと考えています。 ドキュメントに記載があるのかも知れませんが十分な調査を行っていなかった為、起動しない原因がわからず結構ハマりました。 どなたかのお役に立てれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon Elastic Container Registry (ECR) 新規作成手順

Amazon Elastic Container Registry (ECR)とは Amazon ECRは、コンテナイメージをクラウド上に保管できるサービスです。 詳細については、公式ページのDescription部分で説明されています。 Amazon Elastic Container Registry (ECR) は、完全マネージド型の Docker コンテナレジストリです。このレジストリを使うと、デベロッパーは Docker コンテナイメージを簡単に保存、管理、デプロイできます。 https://aws.amazon.com/jp/ecr/ 本記事ではECRにリポジトリを作成するところから、コンテナイメージをプッシュするまでを紹介します。 ※AWSアカウントは作成済み。PCはMacを想定しています。 ECRリポジトリ作成手順 https://ap-northeast-1.console.aws.amazon.com/ecr/get-started?region=ap-northeast-1 Amazon ECRのページから[使用方法]ボタンをクリック。 リポジトリを作成 可視性設定:プライベート リポジトリ名:任意 タグのイミュータビリティ:無効 タグのイミュータブルを有効にした場合、同じタグでの上書きを禁止します。 同じタグを別のイメージにつけてpushできないため、latest運用ができなくなります。 無効にした場合は、タグによるイメージの追跡が出来なくなるためSHAを使用して探す必要が出てきます。 https://aws.amazon.com/jp/about-aws/whats-new/2019/07/amazon-ecr-now-supports-immutable-image-tags/ イメージスキャンの設定:無効 KMS暗号化:無効 プッシュ時にスキャンを有効にした場合は、プッシュした際に自動でリポジトリ内のイメージに対して脆弱性スキャンをかけてくれます。 https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/image-scanning.html また、KMS暗号化を有効にすると、デフォルトの暗号化の代わりにKMS(Key Management Service)のカスタマーマスターキーを用いて暗号化するようになります。 [リポジトリを作成] ボタンをクリックし、 正常に作成されましたというメッセージ が表示されるとリポジトリの作成は完了です。 ECRにイメージをプッシュ 準備 AWS CLI と Dockerが必要なので、あらかじめインストールを行います。 既にインストール済みの方はスキップしてください。 AWS CLIのインストール 以下のコマンドを実行し、最新版のAWS CLIをインストールします。 curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg" sudo installer -pkg AWSCLIV2.pkg -target / AWS CLIのインストールが完了したら、aws configコマンドで認証情報の設定を行います。 IAM ユーザーを設定済みの場合 以下の手順からアクセスキーとシークレットアクセスキーを生成した後、aws configを実行し情報を入力します。 https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-creds IAM ユーザーを未設定の場合 管理者用のIAMを作成します。 IAM画面に遷移し、[ユーザー]→[ユーザーの追加]をクリックします。 https://console.aws.amazon.com/iam/home#/users ユーザー名を入力し、アクセスの種類から[プログラムによるアクセス]を選択し、[次のステップ]をクリックします。 アクセス許可の設定で、既存のポリシーを直接アタッチをから、 AdministratorAccess を選択し次へ進みます。 タグの追加 (オプション)は特に行わず、次のステップに進みます。 確認画面で[ユーザーの作成]をクリックします。 ユーザーの作成が完了すると、アクセスIDとシークレットアクセスキーが表示されます。 後の手順で利用するのでひかえておきます。 aws configコマンドを実行し、以下の通り情報を入力します。 AWS Access Key ID [None]:先程作成したアクセスID AWS Secret Access Key [None]:先程作成したシークレットアクセスキー Default region name :ap-northeast-1 (東京リージョン) Default output format :json $ aws configure AWS Access Key ID [None]: *** AWS Secret Access Key [None]: *** Default region name [None]: ap-northeast-1 Default output format [None]: json Dockerのインストール dockerコマンドを実行する必要があるため、最新のDockerをインストールします。 公式から [get Docker] をクリックしダウンロードし、インストールしてください。 https://hub.docker.com/editions/community/docker-ce-desktop-mac インストールが完了したら起動し、メニューバーにDockerアイコンが現れることを確認します。 コンテナイメージの格納 準備が完了したら、リポジトリにイメージを格納してみます。 メッセージ横の[プッシュコマンドの表示]をクリックします。 メッセージを閉じてしまった場合は、リポジトリ画面からでも遷移出来ます。 先程作成したリポジトリにプッシュするための手順が表示されます。 コマンド実行 1.認証 $ aws ecr get-login-password --region us-east-2 | docker login --username AWS --password-stdin <ドメイン名> ダイアログ1番目のコマンドをコピペして実行します。 認証コマンドに成功すると、Login Succeededと表示されます。 Error response from daemon: dial unix docker.raw.sock: connect: connection refused 上記のメッセージが表示された場合は、Dockerを起動していない可能性が高いので、Dockerが起動しているか確認してください。 Unable to locate credentials. You can configure credentials by running "aws configure". Error: Cannot perform an interactive login from a non TTY device 上記のメッセージが表示された場合は、認証情報の設定が未完了ですのでaws configureを実行し認証設定を済ませてください。 2.ビルド dockerイメージのあるディレクトリに移動して、2番めのコマンドを実行します。 $ docker build -t test . もしも、dockerイメージの用意が無い場合は、試しにdockerのdoodle(いたずら書き)をcloneしてからビルドしてみましょう。 $ git clone https://github.com/docker/doodle.git $ cd doodle/cheers2019 $ docker build -t test . 3.イメージにタグづけ 3番目のコマンドをコピペして実行します。 docker tag test:latest <aws_account_id>.dkr.ecr.ap-northeast-1.amazonaws.com/test:latest 一定時間時間が経つと認証が切れてコマンドに失敗することがあります。 以下のメッセージが表示されたら、1.認証に戻ってコマンドを再実行してください。 no basic auth credentials 4.dockerイメージのpush 4番目のコマンドをコピペして実行します。 docker push <aws_account_id>.dkr.ecr.ap-northeast-1.amazonaws.com/test:latest 以上で、コンテナイメージのpushは完了です。 イメージの確認 コンテナイメージが正常に格納されたことを確認します。 ECRの画面からリポジトリを選択すると、プッシュしたイメージタグが表示されると思います。 プッシュされた日時が最新になっていればOKです。 https://ap-northeast-1.console.aws.amazon.com/ecr/repositories?region=ap-northeast-1#
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【4日目】AWS認定ソリューションアーキテクト合格までの道

ネットワークサービス Amazon Virtual Private Cloud(VPC) VPC(Amazon Virtual Private Cloud) AWS上に独立したプライベートネットワーク空間を作成できるサービス 特定のユーザーだけが利用できるネットワーク環境を構築することは、セキュリティの観点から重要である 作成時の注意 IPv4のVPCはプライベートIPアドレス内で、16ビット以上28ビット以内のCIDRを指定 IPアドレスの重複をしない IPv6のVPCは56ビットのCIDRに固定 CIDR(Classless Inter-Domain Routing) IPアドレスの分類を考慮せずに割り当てられたIPアドレス →使用するユーザー分のIPアドレスを用意し、過不足がないようにすること サブネット VPC内に構成するネットワークセグメント どのアベイラビリティーゾーン(AZ)でサブネットを構成するかを指定する ひとつのVPCに対して複数のサブネットを作成できるが、VPCのIPアドレス範囲内でCIDRを設定する パブリックサブネット インターネット通信可能で外部通信の必要があるサーバー プライベートサブネット インターネット接続の必要がないサーバー VPCとサブネット インターネットゲートウェイ(IGW:Internet Gateway) VPC内のリソースからインターネットへアクセスするためのゲートウェイ ルートテーブル サブネット内のEC2インスタンスに対して、静的にルーティングを定義したもの VPC(192.168.0.0/16)におけるルートテーブルの例 送信先 ターゲット 192.168.0.0/16 local 0.0.0.0/0 igw デフォルトでルートテーブルにlocalの経路が定義されていて、同じVPC内にあるサブネット間で相互通信が可能(異なるAZに作成されたサブネットも含まれる) パブリックサブネット ルートテーブル内のデフォルトゲートウェイ(0.0.0.0/0)へのルーティングにIGWを指定したサブネット プライベートサブネット IGWの指定をしていないサブネット NATゲートウェイ プライベートサブネットからインターネットに接続する機能 AWSではマネージドサービスとして提供されている AZ内で冗長化(多重化)されていて、予備が用意されている NAT(Network Address Translation) ネットワークアドレス変換機能 LANに接続された端末からインターネットに接続する際に「プライベートIPアドレス」を自動的に外部ネットワークで使用できる「グローバルIPアドレス」に変換する機能 NATゲートウェイを利用しない場合 NATインスタンス(NAT機能を持ったEC2インスタンス)を利用してアクセスする 「送信元/送信先チェック」を無効化しないといけない EC2インスタンスをNATサーバーとして利用するため、単一障害点とならないように冗長化の仕組みを検討する必要がある 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS DVAを1週間で合格してきた話

はじめに もう随分前になりますが、過去にAWS試験について以下のような記事を書きました。 AWS 認定クラウドプラクティショナーを受験(合格)してきた話 AWS 認定ソリューションアーキテクトアソシエイトを受験(合格)してきた話 そこから今までに別のAWS試験も受けたりしましたが、先日開発系のサービスについての学習の意味も込めてデベロッパーアソシエイトを受験してきましたので、勉強した内容とかを書いていきます。 注意 今回は時間がなかったので、最短で学習して試験に合格するような内容になっています。 ちゃんと腰を据えて学びたいという方には向いてませんので、その場合は他の方の合格記をご参照ください。 (正直初学者向けではないかもしれません) 前提 DVA受験時の前提は以下の通りです。 一般的なIT、クラウドの知識は持っている 運用設計の思想は理解している 2年前にCPとSAAは取得済み 今年の2月にData Analyticsを取得済み 主要なAWSサービスの概要は理解済み 開発系のサービスの使用経験は一部のみ 受験結果 まずは受験結果ですが、スコアとしては867点と余裕をもって合格ができました。 分野別に見ても全般的にコンピテンシーを満たしていました。 学習期間 本業が忙しいのもあったので申し込みから受験まで1週間で勉強しました。 時間はなかったですが、連休があったのでそこでブーストかけて実施しました。 学習内容 学習に使用したのは以下の2つです。 ポケットスタディ AWS認定デベロッパーアソシエイト WEB問題集で学習しよう 順番としては以下のような流れです。 ポケットスタディのテキストを1周する Web問題集を解く(1セクション7問で、7問全てが100%になるまで解いたら次のセクションへ...という風に解きました) Web問題集の本試験モードを解く(1回のみ) AWSの模擬試験を解く(1回のみ) 不足部分をポケットスタディで再学習・Web問題集で関連する問題を解く 本番 最初はWeb問題集だけでなんとかなるかなと思って数問解いてみたのですが、やはりサービスを使ったことがないとどういう風な設定が可能なのかもわからないので、思うように解けませんでした。 そこでポケットスタディを試しに買って読んでみたところ、実際のAWSコンソール画面やCloudformationのコードと説明がまとめて書いてあったので、サービスを触ったことがなくても大体のイメージができました。 もちろん実際にサービスを使ってみるのが最善なのですが、時間がなくサッと理解してあとは問題を解いて学習したいという方は上記のやり方がオススメです。 ※AWSサービスは変わりゆくものなのでずっとこのやり方が通用するとは思ってません 試験の感想 DVA試験のセクション別に問題を見たとき、開発と展開についてはCode系などのサービスへの理解があれば解けるものが多いですが、セキュリティはAWSサービス全般に関わってくるものなので学習範囲が他のものに比べて若干広かったです。 リファクタリングとモニタリング・トラブルシューティングは特定のAWSサービスというよりはAWSが求めるベストプラクティスがちゃんと理解できていれば解けるものが多いので、AWSサービス単体で見るよりはベストプラクティスなども組み合わせて学習してみるとより理解が深まりやすいと思います。 この内容が誰かの参考になれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[AWS IoT] 証明書の作成・登録方法 Part3 JITP

はじめに AWS IoTにはいくつかの証明書の作成方法、登録方法があります。前回の記事では AmazonのCAを使用するFleet Provisioningについて試してみました。今回はAmazon以外のCAで発行した証明書を利用して、デバイスが初回接続時に証明証を発行する方法の1つであるJust in time provisioning (JITP)について試してみたいと思います。 証明書発行方法の分類 こちらにAWS IoTでの証明書発行の方法が分類されています。 今回はこの中で、製造時にデバイスに証明書を埋め込む必要のない Just In Time Prvisioning(JITP)について試してみます。JITR( Just In Time Registration)については次回試してみますが、JITRとJITPの主な違いは、JITPは事前に作成したテンプレートをもとにしてプロビジョニングを行うのに対して、JITRはLambda関数を用意してプロビジョニングを行うので、JITRのほうがユーザーの方で自由にカスタマイズしやすいという違いがあります。 Just in time provisioning (JITP) CA証明書と検証証明書の作成 まず、CA証明書と検証証明書をを作成する必要があります。CA証明書はOpenSSLのコマンドで作成することができます。 CA証明書を作成 $ openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.pem # 適宜入力 ----- Country Name (2 letter code) []:JP State or Province Name (full name) []:Tokyo Locality Name (eg, city) []:Tokyo Organization Name (eg, company) []:MyOrg Organizational Unit Name (eg, section) []:MyUnit Common Name (eg, fully qualified host name) []:RootCA Email Address []:hogehoge AWS IoT registration code を取得 $ aws iot get-registration-code { "registrationCode": "017102e221eb356519.....(中略)" } 検証証明書のキーペアを作成 $ openssl genrsa -out verificationCert.key 2048 検証証明書のCSRを作成 (Common Nameに registration codeを入力) $ openssl req -new -key verificationCert.key -out verificationCert.csr Country Name (2 letter code) []:JP State or Province Name (full name) []:JP Locality Name (eg, city) []:Tokyo Organization Name (eg, company) []:MyOrg Organizational Unit Name (eg, section) []:MyOU Common Name (eg, fully qualified host name) []:017102e221eb356519 ......(中略) Email Address []: CSRから検証証明書を作成 $ openssl x509 -req -in verificationCert.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out verificationCert.crt -days 500 -sha256 AWS IoT CoreにCA証明書を登録 aws iot register-ca-certificate --ca-certificate rootCA.pem --verification-cert verificationCert.crt 私の環境ではここで、以下のようなエラーとなってしまいました。CA Certificateが正しくないと。 An error occurred (InvalidRequestException) when calling the RegisterCACertificate operation: Contents of the CA Certificate certificate are not correct こちらの記事を見たところ、CAの設定でbasicConstraintsが設定されていないとダメなようで、opensslのConfig fileを作成してCA certificationのbasicConstraintsをTrueにしてCAを再作成したところうまく登録できました。 IoT Core側の設定 まず、IAMのロールからロールの作成を行い、IoTのThingの登録をできる権限を付与したRoleを作成します。 次に、デバイスにアタッチするPolicyを用意します。適宜デバイスに必要な権限を付与するようにします。すでに作成済みのPolicyがあればそれを使用しても構いません。ここではこちらで以前に作成したPolicyを使用しました。 次にプロビジョニングテンプレートを作成します。プロビジョニングテンプレートはこちらに解説があるように、入力するParameterと作成するResourceを定義したテンプレートです。前回のFleet Provisioningでも使用しましたが、JITPでの注意点は、ここで使用できるParameterが証明書の中に含まれる変数に限定されるということです。opensslのコマンドで証明書を作成する際にCountryNameやCommonNameを入力すると思いますが、これらの変数を使って必要な情報を渡してあげる必要があります。 ここでは以下のように、Parametersに"AWS::IoT::Certificate::CommonName": { "Type": "String" }や"AWS::IoT::Certificate::Country": { "Type": "String" }を入れて、CommonNameにThing名を入れています。 { "Parameters" : { "AWS::IoT::Certificate::CommonName": { "Type": "String" }, "AWS::IoT::Certificate::Country": { "Type": "String" }, "AWS::IoT::Certificate::Id": { "Type": "String" } }, "Resources" : { "thing" : { "Type" : "AWS::IoT::Thing", "Properties" : { "ThingName" : {"Ref" : "AWS::IoT::Certificate::CommonName"}, "AttributePayload" : { "version" : "v1", "Country" : {"Ref" : "AWS::IoT::Certificate::Country"}} } }, "certificate" : { "Type" : "AWS::IoT::Certificate", "Properties" : { "CertificateId": {"Ref" : "AWS::IoT::Certificate::Id"} } }, "policy" : { "Type" : "AWS::IoT::Policy", "Properties" : { "PolicyName" : "My_IoT_Policy" } } } } 実際にAWSにこのテンプレートを設定する際には、こちらに解説があるようにJSONを文字列変換し、”をエスケープしたあと、Roleと合わせて一つのファイルにする必要があります。私はPythonからjson.dumpsで整形し、以下のような形にしました。 { "templateBody": "{\"Parameters\": {\"AWS::IoT::Certificate::CommonName\": {\"Type\": \"String\"}, \"AWS::IoT::Certificate::Country\": {\"Type\": \"String\"}, \"AWS::IoT::Certificate::Id\": {\"Type\": \"String\"}}, \"Resources\": {\"thing\": {\"Type\": \"AWS::IoT::Thing\", \"Properties\": {\"ThingName\": {\"Ref\": \"AWS::IoT::Certificate::CommonName\"}, \"AttributePayload\": {\"version\": \"v1\", \"Country\": {\"Ref\": \"AWS::IoT::Certificate::Country\"}}}}, \"certificate\": {\"Type\": \"AWS::IoT::Certificate\", \"Properties\": {\"CertificateId\": {\"Ref\": \"AWS::IoT::Certificate::Id\"}}}, \"policy\": {\"Type\": \"AWS::IoT::Policy\", \"Properties\": {\"PolicyName\": \"My_IoT_Policy\"}}}}", "roleArn": "arn:aws:iam::${account ID}:role/JITP_Role" } このテンプレートを先程登録したCA証明書に紐付けます。 $ aws iot list-ca-certificates として先程登録したCA証明書のCertificateIDを確認した後、以下で登録したCA証明書をACTIVEに設定します。 aws iot update-ca-certificate --certificate-id <CertificateID> --new-status ACTIVE テンプレートを指定してCA証明書に紐付け。 aws iot update-ca-certificate --certificate-id <CertificateID> --new-auto-registration-status ENABLE --registration-config file://provisioning_template.json デバイス側の設定 次にデバイス証明書を作成します。まず、キーを作成。 openssl genrsa -out deviceCert.key 2048 CSRを作成 openssl req -new -key deviceCert.key -out deviceCert.csr プロンプトで以下のようにCommonNameにThing名を指定します。 ----- Country Name (2 letter code) []:JP State or Province Name (full name) []:State Locality Name (eg, city) []:Tokyo Organization Name (eg, company) []:Org Organizational Unit Name (eg, section) []:OU Common Name (eg, fully qualified host name) []:TsugunaoThing1 Email Address []: CSRからCAを使って証明書を作成 openssl x509 -req -in deviceCert.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out deviceCert.crt -days 365 -sha256 JITPでは証明書とCAを連結して送信する必要があるので、連結します。 cat deviceCert.crt rootCA.pem > deviceCertAndCACert.crt 次に作成された証明書(deviceCertAndCACert.crt)、デバイスキー(deviceCert.key)を使ってアクセスしてみます。Amazonのサーバー認証用のCA証明書が必要となるのでこちらから事前にダウンロードしておきます。 今回はクライアントとしてmosquittoを使って接続してみました。 初回の接続。 $ ./mosquitto_pub --cafile jitp_cert/root.cert --cert jitp_cert/deviceCertAndCACert.crt --key jitp_cert/deviceCert.key -h "<host name>-ats.iot.ap-northeast-1.amazonaws.com" -p 8883 -q 1 -d -t topic/test -m "Hello World" -i id01 --tls-version tlsv1.2 接続エラーとなります。 Client id01 sending CONNECT Error: The connection was lost. 2回目の接続 ./mosquitto_pub --cafile jitp_cert/root.cert --cert jitp_cert/deviceCertAndCACert.crt --key jitp_cert/deviceCert.key -h "<host name>-ats.iot.ap-northeast-1.amazonaws.com" -p 8883 -q 1 -d -t topic/test -m "Hello World" -i id01 --tls-version tlsv1.2 1回目の失敗時にデバイス証明書が登録されているため正しく接続されます。 Client id01 sending CONNECT Client id01 received CONNACK (0) Client id01 sending PUBLISH (d0, q1, r0, m1, 'topic/test', ... (11 bytes)) Client id01 received PUBACK (Mid: 1, RC:0) Client id01 sending DISCONNECT マネジメントコンソール上で確認すると、モノが作成されていることが確認できます。 まとめ 今回はJITPにてデバイスの初回アクセス時に証明書を作成して登録する方法を試してみました。次回はより柔軟に設定を行えるJITRについて試してみたいと思います。 参考にしたサイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

filename-yyyymmddの形式で退避したS3をAthenaで日付条件クエリ

はじめに OSのログをS3へ退避する際、 とりあえず貯めとけ精神でOS上でログローテされたまま保存しますよね。 そうすると後々、 ある月のログに対してAthenaでクエリかけたい...ってときに苦しみますよね。 でもHive形式に作り替えるのも面倒ですよね。 ※Athenaでのパーティション分割:https://docs.aws.amazon.com/ja_jp/athena/latest/ug/partitions.html そんなときは、検索に時間かけても良いからゴリ押しでクエリをかけましょう。 0. S3の構造 バケットURL:s3://secure-log-athena-test/secure/ オブジェクト: secure secure-20210701 secure-20210702 secure-20210703 secure-20210704 secure-20210705 secure-20210706 secure-20210707 ・・・ 1. サンプルDDL テーブル名, 'バケット名URI' 部分は各S3バケットのURIに置き換えてください。 CREATE EXTERNAL TABLE `テーブル名`( `month` string COMMENT '', `day` string COMMENT '', `time` string COMMENT '', `host` string COMMENT '', `cmd` string COMMENT '', `user` string COMMENT '', `etc` string COMMENT '') ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe' WITH SERDEPROPERTIES ( 'input.regex'='^(\\S+) (\\S+) (\\S+) (\\S+) (\\S+) (\\S+) (.+)$') STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 'バケット名URI' TBLPROPERTIES ( 'has_encrypted_data'='false', 'transient_lastDdlTime'='1622793925') 2. クエリ "DB名", "テーブル名" 部分は各S3バケットのURIに置き換えてください。 SELECT * FROM "DB名"."テーブル名" WHERE "$path" LIKE '%secure-202107%' AND cmd LIKE '%sudo%' AND user NOT LIKE '%pam_unix%'; おわりに ・運用等で定期的に検索をかける必要がある ・クエリに時間をかけたくない(お金もかけたくない) そんなときは腹を括ってS3の構造と退避スクリプトに向き合いましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Athenaで基礎からしっかり入門 分析SQL(Python・Pandasコード付き) #2

今まで複雑なデータ操作・分析などはPythonでやっており、SQLは普通のアプリ開発程度のライトなものしか触って来なかったのですが、やはり分析用の長いSQLなども書けた方がやりとり等で便利・・・という印象なので、復習も兼ねて記事にしておきます。 また、SQLに加えて検算も兼ねてPythonやPandasなどを使ったコードもSQLと併記していきます(Pythonで書くとどういった記述が該当するのかの比較用として使います)。 ※長くなるのでいくつかの記事に分割します。本記事は2記事目となります。 他のシリーズ記事 前回(#1) 用語の説明・SELECT、WHERE、ORDER BY、LIMIT、AS、DISTINCT、基本的な集計関係(COUNTやAVGなど)、Athenaのパーティション、型、CAST、JOIN、UNION(INTERSECTなど含む)などについてはこちらの記事で既に説明済みなので本記事では触れません。 本記事で触れる点 GROUP BY HAVING サブクエリ CASE COALESCE NULLIF LEAST GREATEST 四則演算などの基本的な計算 日付と日時の各操作 ※ボリューム的には最後の日付と日時の操作関係で半分以上を占めています。 環境の準備 以下の#1の記事でS3へのAthena用のデータの配置やテーブルのCREATE文などのGitHubに公開しているものに関しての情報を記載していますのでそちらをご参照ください。 特記実行 お仕事がAWSなので合わせてDBはAWSのAthena(Presto)を利用していきます。BigQueryやRedshift、MySQLやPostgreSQLなどではある程度方言や使える関数の差などがあると思いますがご了承ください。 同様にお仕事がゲーム業界なので、用意するデータセットはモバイルゲームなどを意識した形(データ・テーブル)で進めます。 エンジニア以外の方(プランナーさんやマーケの方など)も少し読者として想定しています。ある程度技術的なところで煩雑な記述もありますがご容赦ください。 長くなるのでいくつかの記事に分割して執筆を進めています。 同じ値をグループ化する: GROUP BY GROUP BYを使うと各カラムで同じ値のものをグループ化することができます。これによって例えば「ユーザーごとの売り上げを計算したい」とか「日付ごとの売り上げを出したい」といったように、一度のSQLでいくつものグループ(ユーザー単位であったり日付ごとであったりのグループ)を作って計算なとを行うことができます。 使い方はGROUP BY <グループ化で使いたい対象のカラム名>といった具合に書きます。例えばユーザーごとにグループ化したければユーザーIDのカラムを指定する形でGROUP BY user_idといった記述になります。 例として、2021-01-01と2021-01-02の2日間で各ユーザーが何回ログインしているのかを集計してみましょう。 各ユーザーのログインを扱うloginというテーブルを使っていきます。loginテーブルは以下のように各ユーザーのID(user_id)や日時などのカラムを持っています。 SELECT * FROM athena_workshop.login LIMIT 10; ユーザーごとにグループ化してそれぞれのユーザーでのログイン回数を出したいのでGROUP BY user_idという記述を使います。 また、テーブルの性質上各ユーザーごとの行数がそのまま各ユーザーのログイン回数となるので、COUNT(*)の記述を使って行数をカウントしています。そのままだとCOUNTで指定したカラムが_col1といった名前で表示されてしまうので、AS login_countと指定してなんのカラムなのかが分かりやすくしています。 SELECT user_id, COUNT(*) AS login_count FROM athena_workshop.login WHERE dt IN ('2021-01-01', '2021-01-02') GROUP BY user_id これで特定の期間におけるログイン回数を出すことができました。 Pythonでの書き方 Pandasにgroupbyメソッドがあるのでそちらを使います。by引数でグループ化で使いたいカラム名を指定します。このメソッドではDataFrameGroupByやSeriesGroupByといったインスタンスの型が返却され、そちらに対してcountやsumなどの集計関数(メソッド)を実行することで通常のデータフレームなどが返ってきます。by引数で指定したカラムはインデックスに設定された状態で返ってきます。もしSQLのようにインデックスではなくカラムに設定したい場合には別途reset_indexなどのメソッドを利用する必要があります。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df = df[['user_id', 'time']] df = df.groupby(by='user_id').count() df.rename(columns={'time': 'login_count'}, inplace=True) print(df.head(5)) login_count user_id 1 1 3 2 5 1 6 3 9 2 他の句との順番に注意 WHERE句やLIMIT句などをGROUP BYと一緒に使う場合にはそれぞれの句の順番を意識する必要があります。順番が正しくないとエラーになってしまいます。 まずはWHERE句ですが、これはGROUP BYよりも前に書かないといけません。 以下のようにGROUP BYの後に書くとエラーになります。 SELECT user_id, COUNT(*) AS login_count FROM athena_workshop.login GROUP BY user_id WHERE dt IN ('2021-01-01', '2021-01-02') ORDER BYに関してはGROUP BYの直後に書きます。 SELECT user_id, COUNT(*) AS login_count FROM athena_workshop.login WHERE dt IN ('2021-01-01', '2021-01-02') GROUP BY user_id ORDER BY user_id GROUP BYの前に書くとエラーになります。 LIMIT句に関しては最後に書きます。 SELECT user_id, COUNT(*) AS login_count FROM athena_workshop.login WHERE dt IN ('2021-01-01', '2021-01-02') GROUP BY user_id ORDER BY user_id LIMIT 3 複数のカラムを指定する GROUP BYには複数のカラムを指定することができます。この場合グループ化の条件が増えるので、1つ辺りのグループの行数は基本的に減少します。例えばAカラムとBカラムという2つのカラムを指定した場合、「A且つB」という2つの条件を満たした行同士が同じグループとなります。 例えば「日別」且つ「ユーザーごと」といったように日付やユーザーIDといった複数の条件を付けて集計をやりたいケースなどはよく発生します。 書き方はORDER BYなどで複数のカラムを指定するときと同様に、半角のコンマ区切りで複数のカラムを指定します。 以下のSQLではユーザーID(user_id)と日付(dt)のパーティションをGROUP BYに指定しています。分かりやすいようにユーザーはIDが1と3のユーザーのみに絞ってあります。 SELECT user_id, dt, COUNT(*) AS login_count FROM athena_workshop.login WHERE user_id IN(1, 3) GROUP BY user_id, dt ORDER BY dt, user_id Pythonでの書き方 groupbyメソッドのby引数にはリストなども指定できるので、リスト等で複数のカラム名を指定すれば対応ができます。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df = df[['user_id', 'device_type', 'time']] df = df.groupby(by=['user_id', 'device_type']).count() df.rename(columns={'time': 'login_count'}, inplace=True) print(df.head(5)) login_count user_id device_type 1 2 1 3 2 2 5 1 1 6 2 3 9 2 2 基本的には利用できるカラムはGROUP BYで指定したカラムもしくは集計関数が利用できる GROUP BYを使った場合、SELECT ... FROMの...のカラムの指定部分は以下の2つの条件を満たすカラムのみ指定ができます。 GROUP BYで指定したカラム GROUP BYで指定していないカラムで、且つ集計関数(SUMやCOUNTなど)を使う場合 COUNTなどの集計関数であればアスタリスクの全カラム指定も使える それぞれ試していきましょう。まずはGROUP BYで指定されたカラムのケースです。以下のように通常通りSQLを投げることができます。 SELECT user_id FROM athena_workshop.login WHERE user_id IN(1, 3) GROUP BY user_id 続いて集計関数を挟む形のSQLです。サンプルとしてCOUNTの集計関数を使います。カラムはGROUP BYで指定していないdtのパーティションを指定しています。こちらも通常通りSQLが流れます。 SELECT user_id, COUNT(dt) AS login_count FROM athena_workshop.login WHERE user_id IN(1, 3) GROUP BY user_id 続いて指定できない条件を試してみます。GROUP BYに指定されておらず、且つ集計関数も挟まない形のdtのパーティションを指定してみます。 SELECT user_id, dt FROM athena_workshop.login WHERE user_id IN(1, 3) GROUP BY user_id 以下のようにエラーになります。 エラーメッセージに以下のように出ています。 SYNTAX_ERROR: line 1:17: 'dt' must be an aggregate expression or appear in GROUP BY clause dtのカラム(or パーティション)は集計(aggregate)表現(集計関数)もしくはGROUP BY句(clause)でしか使えませんよ、といったことが表示されています。 このようにGROUP BYを使った場合には使えるカラムの挙動が特殊になります。 GROUP BYした集計関数の値に対して条件を設定する: HAVING GROUP BYでグループ化し、集計関数を指定したカラムに対して条件を指定したくなることも結構出てきます。例えば以下のユーザーごとのログイン回数を集計するSQLで、ログイン回数が3回以上のユーザーの行のみを抽出したいとします。 SELECT user_id, COUNT(*) AS login_count FROM athena_workshop.login WHERE dt IN ('2021-01-01', '2021-01-02') GROUP BY user_id しかしながらGROUP BYを使った場合にはWHERE句でそのカラムを指定することはできません。以下のようにWHERE login_count >= 3としてみてもエラーになります。 SELECT user_id, COUNT(*) AS login_count FROM athena_workshop.login WHERE dt IN ('2021-01-01', '2021-01-02') AND login_count >= 3 GROUP BY user_id そういった場合はHAVINGを使うとグループ化し集計関数を挟んだ値に対して条件を設定することができます。HAVINGはGROUP BYの後に書きます。また、左辺などに集計関数と対象のカラムの指定を行います。残りと等値や以下・超過などの条件の書き方はWHERE句と同じような書き方となります。 以下のSQLでは先ほどWHERE句関係でエラーになったものを対応でき、ログイン回数が3回以上の行のみが抽出できています。 SELECT user_id, COUNT(*) AS login_count FROM athena_workshop.login WHERE dt IN ('2021-01-01', '2021-01-02') GROUP BY user_id HAVING COUNT(*) >= 3 クエリ結果を参照する: サブクエリ この節以降ではメインのSQLとは別のSQLを同時に使い、結果をメインのSQLで使う形となるサブクエリについて触れていきます。SQL内で複数のSELECT文などが出現する形となります。 サブクエリ使用時の前提 サブクエリ使用時の前提と留意点として以下のようなものがあります。 基本的にはWHERE句やJOINなどに直接サブクエリを書かずにWITH句などを使う方がSQLがシンプルで読みやすくなることが多めです。どうしてもサブクエリで複数のクエリを使うと複雑になりがちで、且つWHERE句などにぎっしり詰まると他の人がそのSQLを読んだりが難しくなったりして好ましくないケースがあり注意が必要です。 WHERE句などでのサブクエリはJOINなどで代替できればそちらの方がシンプルになることが多いように感じます。参考 : WHERE句のサブクエリは大抵の場合テーブルJOINで代替できる サブクエリが増えれば増えるほどSQLの内容が遅くなったりスキャンサイズが増えたりしてきます。繰り返し参照するものや過程の検算などをしたい場合には後々触れる一時テーブル(中間テーブル)を作成する対応(CTAS)の方が好ましいケースも結構あるかもしれません。 クエリ結果を条件指定(WHERE)で利用する まずはサブクエリをWHERE句で使う方法について見ていきます。サブクエリ側は一つのカラムしか選択はできません。 且つ一番基本的な使い方としては、サブクエリ側の行は1行のみとする形となります。 説明のため、以前の記事でJOINの説明時に使ったサンプルテーブルを使っていこうと思います。join_sample_left_tableとjoin_sample_right_tableという2つのテーブルを使います。 join_sample_left_tableテーブルは以下のように武器のIDと名前を格納したテーブルになります。 SELECT * FROM athena_workshop.join_sample_left_table join_sample_right_tableテーブルは以下のように武器のIDと攻撃力を持ったテーブルになります。 SELECT * FROM athena_workshop.join_sample_right_table join_sample_right_tableテーブル側(攻撃力のデータを持つテーブル)で、武器の名前がグングニルになっている行の攻撃力を確認したいとします。もちろんIDを確認してIDの方をWHERE句に使ったりもできますが、今回はIDを見ずにSQLを投げたいとします。 その場合以下のようなサブクエリを使ったSQLを書くことでWHERE句に武器名を指定しつつSQLを実行することができます。 SELECT * FROM athena_workshop.join_sample_right_table WHERE id = (SELECT id FROM athena_workshop.join_sample_left_table WHERE name = 'グングニル') ポイントとしてはWHERE id = (<サブクエリ>)となっている点です。WHERE句部分のカラム名の指定とサブクエリで指定しているカラム名は別の名前でも動きますが、型が同一で且つサブクエリ側は1つのカラムのみ、且つ今回の基本的なサブクエリではサブクエリの結果の行も1行のみにする必要があります。 サブクエリ側で別のテーブルを参照して名前に対してWHERE句で条件を指定し、結果のIDがメインのSQLのWHERE句の条件として使われる・・・という形になります。 他にも色々機能はあるのですが、前述の通りWITH句を使った方が読みやすかったりJOINで代替できるケースも多かったりで仕事でもあまりこのサブクエリは使う機会が無いので本記事ではあまり触れずにいこうと思います(読んだ分析のSQL本でも基本的にWITH句や中間テーブルなどがメインに使われており、WHERE句のサブクエリは軽く触れる程度だったため)。 クエリ結果の列を連結(JOIN)する こちらもWHERE句と同様、WITH句で対応したり普通にJOINしたりすることが仕事では多いのですが一応軽く触れておきます。 以下のようにJOINするテーブルの指定部分にサブクエリで別のSELECT文を流しています。サブクエリ側の結果はWHERE句の指定で武器名がグングニルとなっている行のみとなるのと、内部結合(INNER JOIN)させているので結果的にグングニルの1行のみ残ります。 これだけだとサブクエリ使わずに連結してからWHERE句とかで制御すれば同じ結果が得られるのですが、もっと複雑な加工をサブクエリ側でやる場合などに役立ったりするケースがあります(ただし、それらの場合でもWITH句などを使う方が読みやすくなるかもしれません)。 SELECT * FROM athena_workshop.join_sample_right_table INNER JOIN ( SELECT * FROM athena_workshop.join_sample_left_table WHERE name = 'グングニル' ) USING(id) WITHを使うと読みやすいSQLになる WITH句というものを使ってサブクエリを書くこともできます。クエリ結果を一時的なテーブルとして任意の名前を付けることができ、そのテーブルをメインのSQL内で利用することができます。 WITH <設定する一時テーブル名> AS (サブクエリのSELECT文) メインのSELECT文といったように書きます。サブクエリの結果の一時テーブル名に名前を付けるフォーマットになっていることから分かる通り、サブクエリの結果に変数や定数のように名前を付けられます。つまりサブクエリの結果を一時的なテーブルとして、その名前を使ってメインのSQL内の各所で利用することができます。 プログラミングでいうとWITH句を使わないサブクエリがハードコーディング、WITH句を使ったものが変数などを利用したようなものに近くなります。WITH句を使うことで以下のようなメリットがあります。 複数個所でサブクエリの結果が必要になった場合にも毎回サブクエリを書かずに済みます(サブクエリの記述の重複を避けれます)。 結果の一時テーブルに名前を付けられるので、なんの内容のサブクエリなのかが分かりやすくなります。 サブクエリとメインのSQLの各SELECT文を分離できるので、メインのSQLがごちゃっとしにくくなります(WHERE句やJOIN部分に詰め込まれず、分かれる形になります)。 WITH句を使わずに書いていたサブクエリの内容をWITH句を使って同様の内容を記述すると以下のようになります。 WITH target_weapon AS ( SELECT * FROM athena_workshop.join_sample_left_table WHERE name = 'グングニル' ) SELECT * FROM athena_workshop.join_sample_right_table INNER JOIN target_weapon USING(id) 条件に応じた制御を行う この節以降では条件に応じた制御について触れていきます。 条件に応じた値の置換を行う: CASE CASEを使うと条件に応じた特定の列の値を別のカラムの値として結果に設定することができます。プログラミングでいうところのif文みたいなことができます。 基本的な書き方としてはSELECT ... FROMのカラム指定部分にCASE WHEN <対象のカラム> = <対象値> THEN <条件を満たした場合に設定される値> ENDといったように書きます。 Pythonのif文で書くと if <対象のカラムの値> == <対象値>: <条件を満たした場合に値を設定する処理> みたいなものに近くなります。 また、ENDの後にはASを使って結果の値のカラム名を設定することができます。 説明のため引き続きJOINなどの節で使ったjoin_sample_right_tableテーブルを利用していきます。以下のようなテーブルとなっています。武器の攻撃力を想定したテーブルですね。 SELECT * FROM athena_workshop.join_sample_right_table 例として、このテーブルでIDはあるものの武器名などが設定されているマスタのテーブルはAthenaの環境に無くJOINができない・・・しかし武器名などを設定したいといったケースを想定してみましょう(マスタではなく種別値などのラベルなどはテーブルが存在しないケースなどは結構あると思います)。 こういったケースでは数が少なければCASEで対応することができます。例えば以下のSQLではCASEを使ってIDが2の武器の行に対してグングニルという武器名を設定しています。そちらの武器名のカラム名にはweapon_nameという名前をASを使って設定しています。 SELECT *, CASE WHEN id = 2 THEN 'グングニル' END AS weapon_name FROM athena_workshop.join_sample_right_table Pythonでの書き方 データフレームに対する条件分岐的な制御は色々書き方はあります。maskを使ったりwhereを使ったりapplyを使ったり、もしくはリストに直してループで回してしまうなど様々です。 今回はmaskメソッドを使います。特定のシリーズ(カラム)でmaskメソッドを使い、第一引数にスライスなどで使う時と同じように条件(今回はdf['id'] == 2)を指定し、第二引数に設定する値(今回は'グングニル')を指定します。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/join_sample_right_table/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['weapon_name'] = df['id'].mask(df['id'] == 2, 'グングニル') print(df) id attack weapon_name 0 2 1000 グングニル 1 3 800 3 複数の条件の値の置換を行う 複数の条件を設定したい場合にはENDの前にWHEN ... THEN ...の記述を追加していきます。Pythonで言うところのelifのような処理になります。 SELECT *, CASE WHEN id = 2 THEN 'グングニル' WHEN id = 3 THEN 'トライデント' END AS weapon_name FROM athena_workshop.join_sample_right_table どの条件にも該当しない場合の設定 いくつかWHEN ... THEN ...で条件を設定したとして、「その他」のケースで値を設定したいケースにはELSEを使います。どの条件にも該当しない場合にはその行がELSEで指定された値にて置換されます。ELSE <設定する値>といったように書きます。WHEN ... THEN ...の条件の後、且つENDの前に書きます。 SELECT *, CASE WHEN id = 1 THEN 'エクスカリバー' WHEN id = 2 THEN 'グングニル' ELSE '不明な武器' END AS weapon_name FROM athena_workshop.join_sample_right_table 欠損値(NULL)の場合に別の値に置き換える: COALESCE 欠損値(NULL)の行の値を別の値に置き換えたい場合にはCALESCE関数を使います。COALESCEは「 癒合(ゆごう)する、合体する、合同する」といった意味を持つ単語のようです。発音的にはコウアレスとかに近いようです。 coalesceの読み方はカタカナで書くと「コウアレス」に近いようです。 SQL関数coalesceの使い方と読み方 以下のように前記事で扱った、欠損値が一部で存在するdevice_unique_idを使っていきます。 SELECT * FROM athena_workshop.device_unique_id WHERE unique_id IS NULL LIMIT 10 COALESCE関数を使うと欠損している値を別の値に置換できます。第一引数には対象のカラム名、第二引数には欠損値に対して設定する値を指定します。以下では分かりやすいように欠損している箇所に対して不明な値という値を設定しています。 SELECT *, COALESCE(unique_id, '不明な値') AS label FROM athena_workshop.device_unique_id WHERE unique_id IS NULL LIMIT 10 ログが欠損値を含む場合に別のラベルに置換しておきたい場合であったり、他の不正なケースの値と統一したい(例 : 空文字に統一したり、何らかの欠損値を示す文字列と統一したり)といった場合に役立ちます。 Pythonでの書き方 fillnaメソッドで欠損値補完が効くのでそちらを使います。第一引数に設定する値を指定します。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/device_unique_id/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['unique_id'].fillna('不明な値', inplace=True) print(df[df['unique_id'] == '不明な値'].head()) user_id date device_type unique_id 1065 7367 2021-01-01 2 不明な値 1201 6101 2021-01-01 1 不明な値 1759 17509 2021-01-01 1 不明な値 1830 20110 2021-01-01 2 不明な値 1858 20459 2021-01-01 1 不明な値 条件を満たした値を欠損値(NULL)に置き換える: NULLIF NULLIF関数はCOALESCEと逆のような挙動をします。特定の条件を満たす行を欠損値に置換します。 第一引数に対象のカラム名、第二引数に該当する条件の値を指定します。 以下のSQLではdevice_typeが2の行の値を欠損値にしています。 SELECT user_id, device_type, NULLIF(device_type, 2) AS nullif_column FROM athena_workshop.device_unique_id LIMIT 10 Pythonでの書き方 replaceメソッドで置換が利くので置換の値でNumPyの欠損値(np.nan)を指定すれば同じようなことができます。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/device_unique_id/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['device_type'].replace(to_replace=2, value=np.nan, inplace=True) print(df.head(10)) user_id date device_type unique_id 0 9958 2021-01-01 1.0 80dd469bc6fc1ac5c51bfca801c605d6cb296868a64471... 1 26025 2021-01-01 1.0 d302032f6b82abaf18f9df6bbde6ba999b901b7a8def00... 2 28390 2021-01-01 NaN 4ebfcc61825f46ca981eb56041ed837b1ff5bd08c495e4... 3 11977 2021-01-01 1.0 de24cf852f0270fe864de27b633e888d57ab55278b0030... 4 10732 2021-01-01 1.0 3420f6554809274949fab197b685f549e2b7d94ccb58f0... 5 16670 2021-01-01 NaN 0a2e7ebfa13f583361d4dc51a7ddab03acaeb1700c90d5... 6 17426 2021-01-01 NaN 60c4893828c76fb134247d773c02b8bf0c4801c95d392d... 7 26623 2021-01-01 1.0 eeb5d2235bb2db0c87f016d0bbe1050e9f08d7767dd5fc... 8 2083 2021-01-01 NaN 4e7a0668e1de434a88c26ae248ab80e71f3dc5ec603421... 9 9079 2021-01-01 NaN fae26219b0f6340c86bb7440576dbef94afa7ca5809d0b... 値が小さい方を設定する: LEAST LEAST関数を使うと、指定されたカラムや数値などの中から最小の値が選択されます。以下のSQLではtotal_salesというカラムの値と100という値の2つの中で小さい方が結果に残っていることを確認できます。特定のカラムに対して上限値などを設定する時などに便利かもしれません。 SELECT user_id, total_sales, LEAST(total_sales, 100) AS least_result FROM athena_workshop.user_total_sales_and_power LIMIT 50; なお、この関数は3つ以上の引数も受け付けてくれます。複数のカラムなどを指定して、その中から一番小さい値を残す・・・といった制御ができます。 SELECT user_id, total_sales, power, LEAST(total_sales, power, 50000) AS least_result FROM athena_workshop.user_total_sales_and_power LIMIT 50; Pythonでの書き方 minメソッドでaxis=1と指定すれば列方向での最小値が取れます。既存のカラム以外にも固定値も必要な場合にはそのカラムを追加しておくことで対応ができます(以下のコードではdf['fixed_value'] = 50000として固定値のカラムを追加しています)。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_total_sales_and_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['fixed_value'] = 50000 df['least_result'] = df[['total_sales', 'power', 'fixed_value']].min(axis=1) print(df.head(10)) user_id date device_type total_sales power fixed_value least_result 0 19914 2021-01-01 1 0 39203 50000 0 1 19005 2021-01-01 2 0 42748 50000 0 2 5741 2021-01-01 2 0 69826 50000 0 3 14028 2021-01-01 2 0 102833 50000 0 4 18434 2021-01-01 2 0 72106 50000 0 5 6231 2021-01-01 1 0 68877 50000 0 6 7403 2021-01-01 1 82900 223433 50000 50000 7 23378 2021-01-01 2 0 110646 50000 0 8 9597 2021-01-01 1 30300 70400 50000 30300 9 1553 2021-01-01 2 0 62792 50000 0 値が大きい方を設定する: GREATEST GREATEST関数はLEAST関数の逆の挙動をします。つまり指定されたカラムや値の中で一番大きな値が選択されます。 LEASTとは逆に最小値を固定値で設定したい場合などに便利です。 SELECT user_id, total_sales, power, GREATEST(total_sales, power, 50000) AS least_result FROM athena_workshop.user_total_sales_and_power LIMIT 50 Pythonでの書き方 LEASTの時にminメソッドを使っていた箇所をmaxメソッドにするだけです。axis=1などの引数設定は変わりません。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_total_sales_and_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['fixed_value'] = 50000 df['least_result'] = df[['total_sales', 'power', 'fixed_value']].max(axis=1) print(df.head(10)) user_id date device_type total_sales power fixed_value least_result 0 19914 2021-01-01 1 0 39203 50000 50000 1 19005 2021-01-01 2 0 42748 50000 50000 2 5741 2021-01-01 2 0 69826 50000 69826 3 14028 2021-01-01 2 0 102833 50000 102833 4 18434 2021-01-01 2 0 72106 50000 72106 5 6231 2021-01-01 1 0 68877 50000 68877 6 7403 2021-01-01 1 82900 223433 50000 223433 7 23378 2021-01-01 2 0 110646 50000 110646 8 9597 2021-01-01 1 30300 70400 50000 70400 9 1553 2021-01-01 2 0 62792 50000 62792 基本的な演算 この節以降では四則演算などについて触れていきます。 加算 特定のカラムに対して+の記号を使って加算(足し算)を行うことができます。例えば<特定のカラム> + 10000とすると指定したカラムに1万足された状態で各行が返ってきます。 以下のSQLではadded_valueというカラム名を付ける形で元のカラムよりも1万足された値を表示しています。 SELECT user_id, total_sales, total_sales + 10000 AS added_value FROM athena_workshop.user_total_sales_and_power LIMIT 50 また、固定値だけでなくカラム同士の足し算なども可能です。以下のSQLではtotal_salesカラムとpowerカラム同士を足し算した結果を表示しています。 SELECT total_sales, power, total_sales + power AS added_value FROM athena_workshop.user_total_sales_and_power LIMIT 50 3つ以上の複数の加算を繋げることも可能です。以下のSQLでは2つのカラムに加えて固定値の1万をさらに加えています。 SELECT total_sales, power, total_sales + power + 10000 AS added_value FROM athena_workshop.user_total_sales_and_power LIMIT 50 このようにカラム同士を計算したり複数の演算子を繋げたりすることは他の計算(減算や乗算など)でも可能です。 Pythonでの書き方 そのまま+や+=などの演算子で計算ができます。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_total_sales_and_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['added_value'] = df['total_sales'] + 10000 print(df.head(10)) user_id date device_type total_sales power added_value 0 19914 2021-01-01 1 0 39203 10000 1 19005 2021-01-01 2 0 42748 10000 2 5741 2021-01-01 2 0 69826 10000 3 14028 2021-01-01 2 0 102833 10000 4 18434 2021-01-01 2 0 72106 10000 5 6231 2021-01-01 1 0 68877 10000 6 7403 2021-01-01 1 82900 223433 92900 7 23378 2021-01-01 2 0 110646 10000 8 9597 2021-01-01 1 30300 70400 40300 9 1553 2021-01-01 2 0 62792 10000 減算 -の記号を使うと減算(引き算)できます。 SELECT total_sales, total_sales - 5000 AS subtracted_value FROM athena_workshop.user_total_sales_and_power LIMIT 50 Pythonでの書き方 こちらも-や-=などの演算子を使って対応ができます。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_total_sales_and_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['subtracted_value'] = df['total_sales'] - 5000 print(df.head(10)) user_id date device_type total_sales power subtracted_value 0 19914 2021-01-01 1 0 39203 -5000 1 19005 2021-01-01 2 0 42748 -5000 2 5741 2021-01-01 2 0 69826 -5000 3 14028 2021-01-01 2 0 102833 -5000 4 18434 2021-01-01 2 0 72106 -5000 5 6231 2021-01-01 1 0 68877 -5000 6 7403 2021-01-01 1 82900 223433 77900 7 23378 2021-01-01 2 0 110646 -5000 8 9597 2021-01-01 1 30300 70400 25300 9 1553 2021-01-01 2 0 62792 -5000 乗算 *の演算子で乗算(掛け算)できます。 SELECT total_sales, total_sales * 3 AS multiplied_value FROM athena_workshop.user_total_sales_and_power LIMIT 50 Pythonでの書き方 *や*=の演算子で対応ができます。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_total_sales_and_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['multiplied_value'] = df['total_sales'] * 3 print(df.head(10)) user_id date device_type total_sales power multiplied_value 0 19914 2021-01-01 1 0 39203 0 1 19005 2021-01-01 2 0 42748 0 2 5741 2021-01-01 2 0 69826 0 3 14028 2021-01-01 2 0 102833 0 4 18434 2021-01-01 2 0 72106 0 5 6231 2021-01-01 1 0 68877 0 6 7403 2021-01-01 1 82900 223433 248700 7 23378 2021-01-01 2 0 110646 0 8 9597 2021-01-01 1 30300 70400 90900 9 1553 2021-01-01 2 0 62792 0 除算 /の記号を使うと除算(割り算)することができます。 SELECT power, power / 2 AS divided_value FROM athena_workshop.user_total_sales_and_power LIMIT 50 気を付けないといけない点として、整数の型のカラム(intやbigintなど)に対して除算を行うと結果も整数になります。つまり小数点以下が切り捨てとなります。 前述したSQLの結果で102833という値の2での除算結果が51416となっている点に注目してください。51416.5といったように浮動小数点数にはなりません。Python2系のような挙動ですね。これはAthena(Presto)特有の挙動というものでもなく、他の例えばPostgreSQLなどでも同様に切り捨ての挙動となります。 結果を浮動小数点数(floatやdoubleなど)で取得したい場合には前回の記事で触れたように対象のカラムを浮動小数点数に型変換(キャスト)してから除算する必要があります。 以下のSQLでは事前に浮動小数点数の型であるdoubleにキャストしています。 SELECT power, CAST(power AS double) / 2 AS divided_value FROM athena_workshop.user_total_sales_and_power LIMIT 50 Pythonでの書き方 /や/=の記号などで対応ができます。Python(Pandas)側は除算しても切り捨てにはならずに浮動小数点数の値となります。Pandasなどを使わずに且つPython2系などを使って除算をすると切り捨てにはなります。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_total_sales_and_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['divided_value'] = df['power'] / 2 print(df.head(10)) user_id date device_type total_sales power divided_value 0 19914 2021-01-01 1 0 39203 19601.5 1 19005 2021-01-01 2 0 42748 21374.0 2 5741 2021-01-01 2 0 69826 34913.0 3 14028 2021-01-01 2 0 102833 51416.5 4 18434 2021-01-01 2 0 72106 36053.0 5 6231 2021-01-01 1 0 68877 34438.5 6 7403 2021-01-01 1 82900 223433 111716.5 7 23378 2021-01-01 2 0 110646 55323.0 8 9597 2021-01-01 1 30300 70400 35200.0 9 1553 2021-01-01 2 0 62792 31396.0 SQL側と合わせて除算結果を切り捨てにしたい場合にはastypeメソッドを使ってintにキャストすれば切り捨てが実行されます。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_total_sales_and_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['divided_value'] = df['power'] / 2 df['divided_value'] = df['divided_value'].astype(int, copy=False) print(df.head(10)) 0 19914 2021-01-01 1 0 39203 19601 1 19005 2021-01-01 2 0 42748 21374 2 5741 2021-01-01 2 0 69826 34913 3 14028 2021-01-01 2 0 102833 51416 4 18434 2021-01-01 2 0 72106 36053 5 6231 2021-01-01 1 0 68877 34438 6 7403 2021-01-01 1 82900 223433 111716 7 23378 2021-01-01 2 0 110646 55323 8 9597 2021-01-01 1 30300 70400 35200 9 1553 2021-01-01 2 0 62792 31396 剰余 剰余(余り)を計算したい場合には%の記号を使います。 SELECT power, power % 100 as modulo_value FROM athena_workshop.user_total_sales_and_power LIMIT 50 Pythonでの書き方 %の記号を使ったりなどで対応ができます。 import pandas as pd import numpy as np df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/user_total_sales_and_power/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['divided_value'] = df['power'] % 100 print(df.head(10)) user_id date device_type total_sales power divided_value 0 19914 2021-01-01 1 0 39203 3 1 19005 2021-01-01 2 0 42748 48 2 5741 2021-01-01 2 0 69826 26 3 14028 2021-01-01 2 0 102833 33 4 18434 2021-01-01 2 0 72106 6 5 6231 2021-01-01 1 0 68877 77 6 7403 2021-01-01 1 82900 223433 33 7 23378 2021-01-01 2 0 110646 46 8 9597 2021-01-01 1 30300 70400 0 9 1553 2021-01-01 2 0 62792 92 日付と日時の操作 この節以降では日付(date)と日時(timestamp)関係の操作について触れていきます。 必要なキャスト処理 ここまでの節で触れてきた各テーブルは日付も日時も文字列のカラムとして扱ってきました。そのため日付や日時に対する操作を行う場合には以下のようにCAST関数を使って型をDATEやTIMESTAMPなどに変換してから扱う必要があります。 日付に対するキャスト例 : SELECT date, sales, CAST(date AS DATE) FROM athena_workshop.total_sales_daily LIMIT 10; 日時に対するキャスト例 : SELECT user_id, time, CAST(time AS TIMESTAMP) FROM athena_workshop.login LIMIT 10; Pythonでの書き方 pd.to_datetime関数に特定の日付や日時になっている文字列のカラムのシリーズなどを引数に渡すとPandasのTimestamp型の値が取れます。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) print(df) print(type(df.at[0, 'date'])) date sales 0 2021-01-01 768500 <class 'pandas._libs.tslibs.timestamps.Timestamp'> 日数を加算する <日付もしくは日時> + INTERVAL '<日数>' DAYとすると対象のカラムの日付もしくは日時から指定された日数分あとの日付や日時が取得できます。例えばdateという日付のカラムに対してCAST(date AS DATE) + INTERVAL '2' DAYとすると2日後の日付が取れます。日付と日時問わずに利用できます。 2日後の日付を取得する例 SELECT date, sales, CAST(date AS date) + INTERVAL '2' DAY AS two_days_after FROM athena_workshop.total_sales_daily LIMIT 10; 2日後の日時を取得する例 SELECT user_id, time, CAST(time AS TIMESTAMP) + INTERVAL '2' DAY AS two_days_after FROM athena_workshop.login LIMIT 10; Pythonでの書き方 PandasのDayクラスを使うと日付の加算ができます。引数を省略すると+1日されます。 import pandas as pd from pandas.tseries.offsets import Day df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] += Day() print(df) date sales 0 2021-01-02 768500 引数に日数を指定した場合はその日数分加算を行うことができます。 import pandas as pd from pandas.tseries.offsets import Day df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] += Day(3) print(df) date sales 0 2021-01-04 768500 時間を加算する <日時> + INTERVAL '時間' HOURとすると対象の日時から指定された時間分加算した日時が取得できます。例えばtimeという日時のカラムに対してCAST(time AS TIMESTAMP) + INTERVAL '2' HOURとすると2時間後の時間が取れます。 SELECT user_id, time, CAST(time AS TIMESTAMP) + INTERVAL '2' HOUR AS two_hours_after FROM athena_workshop.login LIMIT 10; Pythonでの書き方 PandasでHourクラスがあるのでそちらを加算することで対応ができます。 import pandas as pd from pandas.tseries.offsets import Hour df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['time'] = pd.to_datetime(df['time']) df['time'] += Hour(2) print(df.head()) user_id time device_type 0 8590 2021-01-01 02:00:01 1 1 568 2021-01-01 02:00:02 1 2 17543 2021-01-01 02:00:04 2 3 15924 2021-01-01 02:00:07 1 4 5243 2021-01-01 02:00:09 2 年の加算 <対象の日付もしくは日時> + INTERVAL '<年数> YEAR'とすると年の加算ができます。 SELECT date, sales, CAST(date AS date) + INTERVAL '1' YEAR AS next_year FROM athena_workshop.total_sales_daily LIMIT 10; Pythonでの書き方 そういえば年での加算をPythonで扱ったことがない・・・気がしたのですが、調べたらYearクラスは存在しない?ようです。しかしDateOffsetクラスにyears引数を指定することで同じようなことができるようです(DateOffsetクラスを初めて使いました・・・)。 import pandas as pd from pandas.tseries.offsets import DateOffset df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] += DateOffset(years=1) print(df) date sales 0 2022-01-01 768500 月の加算 <日付もしくは日時> + INTERVAL '<月数>' MONTHとすると月の加算処理を扱えます。 SELECT date, sales, CAST(date AS date) + INTERVAL '1' MONTH AS next_month FROM athena_workshop.total_sales_daily LIMIT 10; Pythonでの書き方 こちらも年と同様、MonthというクラスはPandasにはありませんがDateOffsetクラスのmonths引数を指定することで同じような対応を行うことができます。 import pandas as pd from pandas.tseries.offsets import DateOffset df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] += DateOffset(months=1) print(df) date sales 0 2021-02-01 768500 複数の加算を同時に行う INTERVAL ...の記述は1つのカラムに対して複数の設定を同時に行えます。例えば1か月と2日後・・・といった設定が可能です。 SELECT date, sales, CAST(date AS date) + INTERVAL '1' MONTH + INTERVAL '2' DAY AS future_date FROM athena_workshop.total_sales_daily LIMIT 10 特定の部分(年や月など)の取得処理 ※この節で触れる方法以外にも、後々の節で触れる各種関数でも整数の形で取得することができます。 文字列の状態のカラムであればSUBSTRという関数で文字列の特定の位置の値が抽出できます。第一引数には対象のカラム、第二引数に文字列の開始位置、第三引数に抽出する文字列を指定します。文字列の開始位置の整数は1からスタートするので注意してください。また、文字列用の関数なので日付や日時にキャストしていない点にも注意してください。 例えばYYYY-MM-DD HH:MM:SSの形式の日時の文字列であれば年部分は1文字目以降・4文字分なのでSUBSTR(time, 1, 4)となります。月であれば6文字目・2文字なのでSUBSTR(time, 6, 2)となります。 SELECT user_id, time, SUBSTR(time, 1, 4) AS year, SUBSTR(time, 6, 2) AS month, SUBSTR(time, 9, 2) AS day, SUBSTR(time, 12, 2) AS hour, SUBSTR(time, 15, 2) AS minute, SUBSTR(time, 18, 2) AS second FROM athena_workshop.login LIMIT 10; Pythonでの書き方 日付もしくは日時に変換する前の文字列のカラムに対してPandasのシリーズのstr属性でスライスの記法([0:4]など)をすることで対応ができます。SQLのSUBSTR関数と異なりインデックスのスタートは1からではなく0からとなります。 import pandas as pd from pandas.tseries.offsets import DateOffset df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['year'] = df['time'].str[0:4] df['month'] = df['time'].str[5:7] df['date'] = df['time'].str[8:10] df['hour'] = df['time'].str[11:13] df['minute'] = df['time'].str[14:16] df['second'] = df['time'].str[17:19] print(df.head()) user_id time device_type year month date hour minute second 0 8590 2021-01-01 00:00:01 1 2021 01 01 00 00 01 1 568 2021-01-01 00:00:02 1 2021 01 01 00 00 02 2 17543 2021-01-01 00:00:04 2 2021 01 01 00 00 04 3 15924 2021-01-01 00:00:07 1 2021 01 01 00 00 07 4 5243 2021-01-01 00:00:09 2 2021 01 01 00 00 09 各種減算処理 減算も-の記号を使うことで加算と同様に行えます。加算と同様にYEAR, MONTH, DAY, HOURの指定に対応しており、使い方は加算と同じです。 SELECT date, sales, CAST(date AS date) - INTERVAL '1' MONTH - INTERVAL '2' DAY AS future_date FROM athena_workshop.total_sales_daily LIMIT 10 Pythonでの書き方 各種クラス(Dayなど)はそのまま減算のオペレーター(-や-=など)が使えます。 import pandas as pd from pandas.tseries.offsets import Day df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] -= Day(3) print(df) date sales 0 2020-12-29 768500 もしくはクラスの引数の値に負の値を指定しても同じ挙動になります。 import pandas as pd from pandas.tseries.offsets import Day df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] += Day(-3) print(df) date sales 0 2020-12-29 768500 特定の部分の連結処理 例えばYYYY-MMの形式での年月部分だったり、MM/DDの月日だったりを抽出したいとします。YYYY-MMの年月の形式であれば前節で触れたSUBSTR関数だけでも対応ができます。ただしフォーマットが変わった場合(例えばYYYY年MM月といったような日本語表記が必要な場合など)には別の対応が必要になります。 そういった場合にはSUBSTR関数での抽出処理とCONCAT関数での連結処理を組み合わせることで対応ができます(英語でconcatenateは「連結する」という意味になります)。 CONCAT関数ではコンマ区切りで複数の文字列のカラムもしくは固定の文字列を指定することで、それぞれの値を連結した文字列を返してくれます。このCONCAT関数の中にSUBSTR関数のものなどを入れることで任意の部分・任意のフォーマットで文字列を出力することができます。 以下のSQLではYYYY年MM月というフォーマットで結果を出力しています。 SELECT date, sales, CONCAT(SUBSTR(date, 1, 4), '年', SUBSTR(date, 6, 2), '月') as year_and_month FROM athena_workshop.total_sales_daily LIMIT 10 もしくは||の記号を使っても文字列を連結することができます。 SELECT date, sales, SUBSTR(date, 1, 4) || '年' || SUBSTR(date, 6, 2) || '月' as year_and_month FROM athena_workshop.total_sales_daily LIMIT 10 Pythonでの書き方 シンプルに+の演算子だけで各カラムや特定の文字列を連結できます。 import pandas as pd from pandas.tseries.offsets import Day df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = df['date'].astype(str, copy=False) df['year'] = df['date'].str[0:4] df['month'] = df['date'].str[5:7] df['year_and_month'] = df['year'] + '年' + df['month'] + '月' print(df) date sales year month year_and_month 0 2021-01-01 768500 2021 01 2021年01月 日時の特定単位の切り落とし : DATE_TRUNC DATE_TRUNC 関数を使うと特定の単位での日付もしくは日時の値の切り捨て処理が行えます。TRUNCATEで「切り捨てる」といったような意味になります。 例えば月の単位で指定すれば日付以降が切り捨てられ、月初の日付などが得られます。日付の単位で指定すれば1日の始まりの日時が得られて時間や分などは切り捨てられます。 DATE_TRUNC('<対象の単位>', <日付もしくは日時>)という形で、第一引数に単位、第二引数に対象のカラム名などを指定します。 単位には以下のいずれかの文字列が指定できます。 'SECOND': 秒までが残ってミリ秒以下が切り捨てとなります。 'MINUTE': 分までが残って秒以下が切り捨てとなります。 'HOUR': 時までが残って分以下が切り捨てとなります。 'DAY': 日までが残って時以下が切り捨てとなります。 'WEEK': 月曜の開始時点の日付・日時になるように切り捨てとなります。 'MONTH': 月までが残って日以下が切り捨てとなります。 'QUARTER': 四半期ごとの開始の日付・日時になるように切り捨てとなります。 'YEAR': 年だけが残り、他は切り捨てられます。 以下のSQLではYEARを指定しているため年のみが残り月と日は01に、時分秒は00に切り捨てられています。 SELECT user_id, time, DATE_TRUNC('YEAR', CAST(time AS TIMESTAMP)) AS month_begin FROM athena_workshop.login LIMIT 10 月初の日付の算出 Pandasで言うところのMonthBeginクラスで扱うような、月初を得たりは結構お仕事で必要になったりします。そういった場合は前述のDATE_TRUNC関数で'MONTH'の単位を指定することで月初が得られます。 SELECT date, sales, DATE_TRUNC('MONTH', CAST(date AS DATE)) AS month_begin FROM athena_workshop.total_sales_daily LIMIT 10 Pythonでの書き方 PandasにMonthBeginクラスがあるのでそちらを使えます。使い方はDayなどの他のクラスと同様です。引数に整数を指定した場合はその月数分で計算がされます。 ただし注意しない点として、対象が既に月初の日付になっている場合前月の月初になってしまいます。例えば2021-01-01の日付の行に対して計算がされると2020-12-01といったように前月の日付になってしまいます。一方で月初以外の2021-01-05などの日付は2021-01-01の日付になります。 2021-01-01の日付指定時 import pandas as pd from pandas.tseries.offsets import MonthBegin df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] -= MonthBegin() print(df) date sales 0 2020-12-01 768500 2021-01-05の日付指定時 import pandas as pd from pandas.tseries.offsets import MonthBegin df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-05/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] -= MonthBegin() print(df) date sales 0 2021-01-01 687100 1つの解決策として、一度MonthBeginで加算して翌月にしてからMonthBeginで1か月分減算するという方法があります。これなら元の日付が月初かどうかに関係なくその月の月初になります。 import pandas as pd from pandas.tseries.offsets import MonthBegin df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] += MonthBegin() df['date'] -= MonthBegin() print(df) date sales 0 2021-01-01 768500 月末の日付の算出 月初と同様に月末の日付の算出なども必要になることが結構あります。HIVEとかだとそれ専用の関数なども用意されていますしPandasだとMonthEndクラスなどがありますが、Athena(Presto)ではこの記事を書いている時点では(UDFなどを除いて)対応するものが無い?感じがしますので少しステップを踏む必要があります。 色々やり方はあると思いますが、今回は以下のようにして月末を計算しています。 月初を計算します(DATE_TRUNC('MONTH', ...))。 翌月の月初に変換します(+ INTERVAL '1' MONTH)。 翌月月初の前日を取得します(- INTERVAL '1' DAY as month_end)。 SELECT date, sales, DATE_TRUNC('MONTH', CAST(date AS DATE)) + INTERVAL '1' MONTH - INTERVAL '1' DAY as month_end FROM athena_workshop.total_sales_daily LIMIT 10 Pythonでの書き方 PandasにMonthEndクラスなどがありそちらで計算すると月末の日付が取れます。ただし月初のMonthBeginと同様に、対象が既に月末の日付の場合翌月の月末になってしまいます。 2021-01-31の月末の日付に対してMonthEndを加算している例 import pandas as pd from pandas.tseries.offsets import MonthEnd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-31/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] += MonthEnd() print(df) date sales 0 2021-02-28 747700 その月の月末などに統一したい場合の解決策の一つとしては、一度MonthBeginを加算して翌月月初にしてからDayもしくはMonthEndを減算するという方法が取れます。 import pandas as pd from pandas.tseries.offsets import MonthBegin, MonthEnd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/total_sales_daily/dt%3D2021-01-31/data.json.gz?raw=true', lines=True, compression='gzip') df['date'] = pd.to_datetime(df['date']) df['date'] += MonthBegin() df['date'] -= MonthEnd() print(df) date sales 0 2021-01-31 747700 WEEKの単位について試す DATE_TRUNCのWEEKについて少し実際に動かして詳細を確認してみます。ドキュメントを読んでいた感じ、月曜に変換されるようです。 SELECT date, sales, DATE_TRUNC('WEEK', CAST(date AS DATE)) AS week_start FROM athena_workshop.total_sales_daily LIMIT 50 以下のように変換がされていることが分かります。 2021-01-01(金) -> 2020-12-28(月) 2021-03-02(火) -> 2021-03-01(月) ... 2021-02-22(月) -> 2021-02-22(月) 確かに月曜に変換されています。 QUARTERの単位について試す QUARTER(四半期)に関しても同様に少し実行してみます。 SELECT date, sales, DATE_TRUNC('QUARTER', CAST(date AS DATE)) AS quarter_start FROM athena_workshop.total_sales_daily LIMIT 10 各日付が2021-01-01の日付になっていることが分かります。ドキュメントではサンプルが2000-07-01という結果になっているので、以下のように変換される感じだと思われます。 1月月初から3月月末 -> 1月の月初 4月月初から6月月末 -> 4月の月初 7月月初から9月月末 -> 7月の月初 10月月初から12月月末 -> 10月の月初 DATE_ADDでもINTERVALと同じようなことができる INTERVALの方を使えばこちらはあまり使う機会が無さそう・・・な気もしますが、DATE_ADD関数でも似たような制御を行えます。 SELECT date, sales, DATE_ADD('DAY', 1, CAST(date AS date)) AS future_date FROM athena_workshop.total_sales_daily LIMIT 10 第一引数に単位、第二引数に値(DAYを指定した場合は日数)、第三引数に日付のカラムなどを指定します。単位の部分にはINTERVALと同様の値が指定できます。たとえばDAYの代わりにYEARを指定すれば年単位で加算などがされます。 SELECT date, sales, DATE_ADD('YEAR', 1, CAST(date AS date)) AS future_date FROM athena_workshop.total_sales_daily LIMIT 10 第二引数には負の値も指定できます。負の値を指定すると減算の挙動になります。 SELECT date, sales, DATE_ADD('YEAR', -1, CAST(date AS date)) AS future_date FROM athena_workshop.total_sales_daily LIMIT 10 現在の時間を取得する: CURRENT_TIME カラムの指定部分などにCURRENT_TIMEと指定すると現在の時間が取れます。後述するCURRENT_TIMESTAMPやCURRENT_DATEなども同じなのですが、関数と違って()の括弧が不要な点にはご注意ください。 SELECT date, sales, CURRENT_TIME FROM athena_workshop.total_sales_daily LIMIT 10 他の関数などと同様にASなども指定できます(以下のサンプルではcurrent_timeは予約語となりASで使えないのでcurrent_time_とサフィックスにアンダースコアを付けています)。 SELECT date, sales, CURRENT_TIME AS current_time_ FROM athena_workshop.total_sales_daily LIMIT 10 現在の日時を取得する: CURRENT_TIMESTAMPとNOW CURRENT_TIMEの代わりにCURRENT_TIMESTAMPを指定すると日付付きで現在の日時が取得できます。 SELECT date, sales, CURRENT_TIMESTAMP FROM athena_workshop.total_sales_daily LIMIT 10 ちなみにこのCURRENT_TIMESTAMPの代わりにNOW関数も使うことができます。挙動は同じですが記述が短くなります。 SELECT date, sales, NOW() AS now FROM athena_workshop.total_sales_daily LIMIT 10 Pythonでの書き方 ビルトインのdatetimeのnow関数で現在日時を取れます。 from datetime import datetime import pandas as pd df: pd.DataFrame = pd.DataFrame(data={'a': [1, 2]}) df['now'] = datetime.now() print(df) a now 0 1 2021-07-13 08:49:58.254533 1 2 2021-07-13 08:49:58.254533 データフレームに設定すると値はPandasのTimestampとなります。そちらはdatetime型と同じようにstrftimeメソッドが使えるので%H:%M:%Sなどの任意のフォーマットを指定すれば時間部分などのみ取得できます。 from datetime import datetime import pandas as pd df: pd.DataFrame = pd.DataFrame(data={'a': [1, 2]}) df['now'] = datetime.now() timestamp: pd.Timestamp = df.at[0, 'now'] print(timestamp.strftime('%H:%M:%S')) 08:53:11 現在の日付を取得する: CURRENT_DATE CURRENT_DATEを指定すると現在の日付(時間は含みません)を取得できます。 SELECT date, sales, CURRENT_DATE FROM athena_workshop.total_sales_daily LIMIT 10 ただし、この値後述するタイムゾーン関係がエラーとなり設定できません(?)。日本環境で扱うと場合によっては日付がずれうるので注意が必要かもしれません(後でもう少し試してみようと思いますが、タイムゾーン付きでCURRENT_TIMESTAMPなどで算出してから日付部分を抽出した方が無難な気もちょっとしています)。 タイムゾーンの設定 前節のCURRENT_TIMEではUTCのタイムゾーンが設定されていました。ただし日本でお仕事する上では日本のタイムゾーンの値が取りたくなることが多くなると思います。 そういった場合にはAT TIME ZONE 'Asia/Tokyo'などと設定すると日本の時間となります。 SELECT date, sales, CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Tokyo' AS current_time_ FROM athena_workshop.total_sales_daily LIMIT 10 日付へのキャストのエイリアス(短縮系の記述) 前節で触れてきたように、文字列などを日付にキャスト(型変換)したい場合にはCAST(<対象のカラム> AS DATE)といった記述が必要になりました。 Athena(Presto)ではこのキャストの記述を短くするためのDATE(<対象のカラム>)という関数が別途用意されています。こちらを使うとCASTの記述を省略できます(挙動はDATEへCASTした時と同じものになります)。 SELECT date AS date_str, DATE(date) AS date, sales FROM athena_workshop.total_sales_daily LIMIT 10 UNIXTIMEを日時に変換する ※この節以降、login_unixtimeというログの日時がUNIXTIMEになっているテーブルを利用します。loginテーブルと同様に、ユーザーのログイン時間を扱う想定のデータとしてあります。 unixtimeカラムは整数の型で以下のようなデータになっています。 SELECT * FROM athena_workshop.login_unixtime LIMIT 10; たまにログでUNIXTIMを扱う必要が出で来るときがあります。そういった場合にはFROM_UNIXTIME関数でUNIXTIMEから通常の日時に変換することができます。第一引数に変換したいカラム名(今回はunixtimeカラム)などを指定します。 SELECT user_id, unixtime, FROM_UNIXTIME(unixtime) AS time FROM athena_workshop.login_unixtime LIMIT 10; ただしこの値はタイムゾーンが加味されていません。日本時間で扱いたい場合には他と同様にAT TIME ZONE 'Asia/Tokyo'とタイムゾーンの指定を追加する必要があります。 SELECT user_id, unixtime, FROM_UNIXTIME(unixtime) AT TIME ZONE 'Asia/Tokyo' AS time FROM athena_workshop.login_unixtime LIMIT 10; Pythonでの書き方 Pandasだとまずはdt.tz_localize('UTC')でUTCに変換してからさらに特定のタイムゾーン(今回は日本時間が欲しいのでAsia/Tokyoをdt.tz_convert)を指定する必要があるようです。少し煩雑には感じます。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login_unixtime/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['time'] = pd.to_datetime(df['unixtime'], unit='s') df['time'] = df['time'].dt.tz_localize('UTC').dt.tz_convert('Asia/Tokyo') print(df.head()) user_id unixtime device_type time 0 17447 1609426803 2 2021-01-01 00:00:03+09:00 1 10800 1609426805 1 2021-01-01 00:00:05+09:00 2 15188 1609426807 1 2021-01-01 00:00:07+09:00 3 18304 1609426807 1 2021-01-01 00:00:07+09:00 4 22496 1609426810 2 2021-01-01 00:00:10+09:00 日本時間に変換した後にタイムゾーン部分の表記を消したい場合にはdt.tz_localize(None)とさらに繋げると対応できるようです。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login_unixtime/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['time'] = pd.to_datetime(df['unixtime'], unit='s') df['time'] = df['time'].dt.tz_localize('UTC').dt.tz_convert('Asia/Tokyo') df['time'] = df['time'].dt.tz_localize(None) print(df.head()) user_id unixtime device_type time 0 17447 1609426803 2 2021-01-01 00:00:03 1 10800 1609426805 1 2021-01-01 00:00:05 2 15188 1609426807 1 2021-01-01 00:00:07 3 18304 1609426807 1 2021-01-01 00:00:07 4 22496 1609426810 2 2021-01-01 00:00:10 日時からUNIXTIMEに変換する 逆に日時からUNIXTIMEにしたい場合にはTO_UNIXTIME関数を使います。第一引数に対象の時間のカラムなどを指定します。 ただし元の日時などが日本時間の場合にはこの関数でも時差を加味する必要があります。 関数自体には時差関係の引数などは無いようで、且つAT TIME ZONEなどで調整していてもちょっとうまくいかなかったので以下のSQLでは普通に日本時間を想定して9時間減算しています。 SELECT user_id, time, TO_UNIXTIME(CAST(time AS TIMESTAMP) - INTERVAL '9' HOUR) AS unixtime FROM athena_workshop.login LIMIT 10 結果は浮動小数点数で返ってくるので指数表記になっています(例 : 1.613919601E9)。整数の形式のものが必要なケースも多いでしょうから、そういった場合にはCAST(... AS BIGINT)と指定して整数変換(キャスト)すると必要な値が取れます。 SELECT user_id, time, CAST(TO_UNIXTIME(CAST(time AS TIMESTAMP) - INTERVAL '9' HOUR) AS BIGINT) AS unixtime FROM athena_workshop.login LIMIT 10 Pythonでの書き方 以下の手順で対応します。 pd.to_datetimeでまずは文字列から日時の型(Timestamp)に変換。 .dt.tz_localize('Asia/Tokyo')でタイムゾーンを日本時間のものに変換。 タイムゾーン変換結果は2021-01-01 00:00:01+09:00といったような値になるので、これをastype(int)でキャストするとナノ秒まで含めた1609426801000000000といった値になるので、それを// 10 ** 9と除算することで結果の整数を取得。 としています(//は切り捨て除算なので整数になります)。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['timestamp'] = pd.to_datetime(df['time']) df['timestamp'] = df['timestamp'].dt.tz_localize('Asia/Tokyo') df['timestamp'] = df['timestamp'].astype(int) // 10 ** 9 print(df.head()) user_id time device_type timestamp 0 8590 2021-01-01 00:00:01 1 1609426801 1 568 2021-01-01 00:00:02 1 1609426802 2 17543 2021-01-01 00:00:04 2 1609426804 3 15924 2021-01-01 00:00:07 1 1609426807 4 5243 2021-01-01 00:00:09 2 1609426809 特定単位の時間(間隔値)への変換処理 Athena(Presto)にはPARSE_DURATIONという関数があります。第一引数に特定の単位付きの文字列を指定すると特殊な型(interval day to second)の時間の値が返ってきます。例えばminuteを表すmと一緒に10mという文字列を引数に指定すると10分のinterval day to secondの時間が返ってきます。0 00:10:00.000といったような特殊な表記になります。 SELECT user_id, time, PARSE_DURATION('10m') AS ten_minutes FROM athena_workshop.login LIMIT 10 Prestoのドキュメントにはあるものの、他の記事とかはほとんどこの関数関係がヒットせずに「何に使うのだろう?」と少し考えていたのですが、1つの使い道として浮動小数点数もサポートされているという点がメリットかなと思いました。 例えば10.5mといった用に書くと10分と30秒といった値が返ってきます。 SELECT user_id, time, PARSE_DURATION('10.5m') AS minute_val FROM athena_workshop.login LIMIT 10 この値はTO_MILLISECONDS関数でミリ秒変換できます。/ 1000で除算すれば秒単位の値にできます。 SELECT user_id, time, TO_MILLISECONDS(PARSE_DURATION('10.5m')) / 1000 AS second_val FROM athena_workshop.login LIMIT 10 この値をDATE_ADD関数に指定すれば10.5分後みたいな制御ができるようになります(INTERVALにはこの値は直接指定するとエラーになるようで、DATE_ADD関数の方を使いました。また、INTERVALの記述の場合は浮動小数点数を指定するとエラーになります)。 SELECT user_id, time, DATE_ADD('SECOND', TO_MILLISECONDS(PARSE_DURATION('10.5m')) / 1000, CAST(time AS TIMESTAMP)) AS future_time FROM athena_workshop.login LIMIT 10 10.5分後みたいなシンプルなケースであればINTERVALの記述を2回するなり、630秒後とかで計算してもシンプルに対応できますが、計算が面倒な場合やもっと直観的ではない値(例えば10.3日など)の場合の計算はこういった浮動小数点数付きの表記の方が可読性が高くなる(意図が伝わりやすくなる)ケースがあるかもしれません。 分のm以外にも以下のような色々な単位がサポートされています。 単位 内容 ns ナノ秒(1 / 1000 / 1000 / 1000秒) us マイクロ秒(1 / 1000 / 1000秒) ms ミリ秒(1 / 1000秒) s 秒 m 分 h 時 d 日 MySQL互換の日時のフォーマット処理 普段この辺りは仕事だとPython上で扱うケースが大半なのでMySQL上でほぼ使ったことはないのですが、任意のフォーマットの文字列 → 時間の変換やその逆の時間 → 任意のフォーマットの文字列に変換する関数が存在します。Pythonだとstrftimeとかstrptimeと同じようなものに該当します。 strftime的に日時 → 任意のフォーマットの文字列に変換したい場合にはDATE_FORMAT関数を使います。strptime的に任意のフォーマットの文字列 → 日時に変換したい場合にはDATE_PARSE関数を使います。それぞれの関数やサポートされているフォーマットの指定などは以降の節で順番に触れていきます。 日時から任意のフォーマットの文字列を生成する: DATE_FORMAT DATE_FORMAT関数を使うと日時のカラムなどを特定のフォーマットの文字列に変換できます。第一引数に日時などのカラム、第二引数にはフォーマットの文字列を指定します。 第二引数のフォーマットに関しては後々の節で詳しく触れます。以下のSQLでは'%m/%d'というフォーマットを指定しています。%mが2文字の月の文字列、%dが2文字の日の文字列なので'%m/%d'で01/05みたいな表記になります。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%m/%d') FROM athena_workshop.login LIMIT 10 Pythonでの書き方 Pandasのシリーズのdt属性でビルトインのdatetimeみたいなインターフェイスにアクセスできるようになっているのでそちらのstrftimeメソッドで対応ができます。今回の例ではフォーマットの文字列も一致していますが、他のフォーマットは一致していないものも多いのでご注意ください。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['time'] = pd.to_datetime(df['time']) df['formatted_time'] = df['time'].dt.strftime('%m/%d') print(df.head()) user_id time device_type formatted_time 0 8590 2021-01-01 00:00:01 1 01/01 1 568 2021-01-01 00:00:02 1 01/01 2 17543 2021-01-01 00:00:04 2 01/01 3 15924 2021-01-01 00:00:07 1 01/01 4 5243 2021-01-01 00:00:09 2 01/01 任意の日時のフォーマットの文字列から日時を生成する: DATE_PARSE ※この節から説明のためにno_symbol_loginという名前のテーブルを扱っていきます。日時のtimeカラムがハイフンやコロンなどの記号を含まない形で保存されているログインデータのテーブルとなります。 SQLで確認してみると以下のようになっています。 SELECT * FROM athena_workshop.no_symbol_login LIMIT 10; 今度は逆に特定のフォーマットの文字列から日時(TIMESTAMP)を取得する処理について触れていきます。たとえば20210101103050みたいにハイフンなどの記号が省かれているデータがあるかもしれませんし、もしくは2021年01月01日10時30分50秒みたいな日本語表記になっているデータもあるかもしれません。そういった特殊なフォーマットの場合に便利です。 DATE_PARSE関数を使います。第一引数に該当のフォーマットの文字列のカラムなどを指定し、第二引数に想定されるフォーマットを指定します。 今回のSQL例では20210101103050といったように記号やスペースなどを含まない年月日時分秒のフォーマットの文字列を日時に変更します。フォーマットは%Y%m%d%H%i%Sとなります。 SELECT user_id, time, DATE_PARSE(time, '%Y%m%d%H%i%S') AS parsed_time FROM athena_workshop.no_symbol_login LIMIT 10; Pythonでの書き方 Pandas側ではpd.to_datetime関数を使う際にformat引数でフォーマットを指定する必要があります。 import pandas as pd df: pd.DataFrame = pd.read_json( 'https://github.com/simon-ritchie/athena_workshop/blob/main/workshop/no_symbol_login/dt%3D2021-01-01/data.json.gz?raw=true', lines=True, compression='gzip') df['parsed_time'] = pd.to_datetime(df['time'], format='%Y%m%d%H%M%S') print(df.head()) user_id time device_type parsed_time 0 9093 20210101000000 1 2021-01-01 00:00:00 1 23210 20210101000007 1 2021-01-01 00:00:07 2 9560 20210101000027 2 2021-01-01 00:00:27 3 23197 20210101000028 2 2021-01-01 00:00:28 4 2569 20210101000032 1 2021-01-01 00:00:32 日時の各フォーマット種別 以降の節ではフォーマットの各文字列の使い方や主だったもののフォーマットの定義について触れていきます。 ※全てのフォーマットは触れません。また、Prestoのドキュメントには載っているフォーマットでもAthena上で利用しようとするとエラーになるものもあるようです(Athena側で裏側で使われているPrestoのバージョンが最新のドキュメントと比べて少し古い・・・とかでしょうか?)。 フォーマットの指定の基本的な使い方 各フォーマット(年や月など)を表す部分には半角の%の記号が付けられます。 フォーマットの部分はシングルクォーテーションで囲む必要があります(例 : '%d')。 複数のフォーマットの文字列を組み合わせて使うことができます(例 : %m%d)。 任意の文字や記号をフォーマットの文字列の間に挟むことができます(例 : %m月%d日, %m-%d)。 4桁の西暦のフォーマット: %Y %Yのフォーマットで4桁の西暦の年が取れます(例 : 2021)。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%Y') AS year FROM athena_workshop.login LIMIT 10 2桁の西暦のフォーマット: %y %yのフォーマットで下2桁の西暦の年が取れます(例 : 21)。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%y') AS year FROM athena_workshop.login LIMIT 10 ゼロ埋めされた形の月のフォーマット: %m %mのフォーマットで左にゼロ埋めがされた状態の月の値が取れます。結果はゼロ埋めされて必ず2桁となります(例 : 03)。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%m') AS month FROM athena_workshop.login LIMIT 10 ゼロ埋めしない形の月のフォーマット: %c %cのフォーマットでゼロ埋めがされていない状態の月の値が取れます(例 : 3)。結果は月に応じて1桁もしくは2桁になります。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%c') AS month FROM athena_workshop.login LIMIT 10 月の文字列のフォーマット: %M 英語表記となりますが、%Mの表記で月のラベルを取得することができます(例 : February)。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%M') AS month FROM athena_workshop.login LIMIT 10 ゼロ埋めされた形の日のフォーマット: %d %dのフォーマットでゼロ埋めされた状態の日が取れます(例 : 05)。結果は必ず2桁となります。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%d') AS day FROM athena_workshop.login LIMIT 10 ゼロ埋めしない形の日のフォーマット: %e %eのフォーマットでゼロ埋めされていない形の日の値が取れます(例 : 6)。結果は日付に応じて1桁もしくは2桁となります。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%e') AS day FROM athena_workshop.login LIMIT 10 曜日の英語の文字列のフォーマット: %aと%W %aと%Wのフォーマットで曜日の英語のラベルを取ることができます。%aの方は短い表記となります(例 : Mon)。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%a') AS weekday FROM athena_workshop.login LIMIT 10 %Wの方は長い表記となります(例 : Wednesday)。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%W') AS weekday FROM athena_workshop.login LIMIT 10 ゼロ埋めされた形の時のフォーマット: %H %Hのフォーマットでゼロ埋めされた時の値が取れます(例 : 05)。結果は必ず2桁となります。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%H') AS hour FROM athena_workshop.login WHERE time >= '2021-03-05 05:30:00' AND dt = '2021-03-05' LIMIT 10 ゼロ埋めされていない形の時のフォーマット: %k %kのフォーマットを使うとゼロ埋めされていない時のフォーマットの値が取れます(例 : 5)。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%k') AS hour FROM athena_workshop.login WHERE time >= '2021-03-05 05:30:00' AND dt = '2021-03-05' LIMIT 10 午前か午後かの英語表記のフォーマット: %p %pというフォーマットを使うと午前か午後かに応じてAMもしくはPMという文字列が取得できます。 午前に結果がなるケース例 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%p') AS am_or_pm FROM athena_workshop.login WHERE time >= '2021-03-05 05:30:00' AND dt = '2021-03-05' LIMIT 10 午後に結果がなるケース例 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%p') AS am_or_pm FROM athena_workshop.login WHERE time >= '2021-03-05 15:30:00' AND dt = '2021-03-05' LIMIT 10 午前か午後かの値が続く時分秒のフォーマット: %r %rのフォーマットを使うとAMもしくはPMが付く形で時分秒の値が取れます。24時間表記ではなくAMとPMごとの12時間表記となります。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%r') AS time FROM athena_workshop.login WHERE time >= '2021-03-05 15:30:00' AND dt = '2021-03-05' LIMIT 10 24時間表記での時分秒のフォーマット: %T %Tのフォーマットでは、AMやPMなどの表記はなく24時間表記での時間が取れます。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%T') AS time FROM athena_workshop.login WHERE time >= '2021-03-05 15:30:00' AND dt = '2021-03-05' LIMIT 10 ゼロ埋めされた形の分のフォーマット: %i %iのフォーマットでゼロ埋めされた分の値が取れます(00~59)。結果は必ず2桁となります。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%i') AS minute FROM athena_workshop.login WHERE time >= '2021-03-05 15:05:00' AND dt = '2021-03-05' LIMIT 10 秒とかもそうなのですが、ゼロ埋めされない形の分や秒のフォーマットはドキュメントに見当たらない?ようです。 ゼロ埋めされた形の秒のフォーマット: %sと%S %sと%Sのフォーマットでゼロ埋めされた秒の値が取れます。大文字小文字両方同じ結果になるようです。 小文字のsを使った場合 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%s') AS second FROM athena_workshop.login WHERE time >= '2021-03-05 15:05:00' AND dt = '2021-03-05' LIMIT 10 大文字のSを使った場合 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%S') AS second FROM athena_workshop.login WHERE time >= '2021-03-05 15:05:00' AND dt = '2021-03-05' LIMIT 10 1年のうちで何日目なのかのフォーマット: %j %jのフォーマットで1年で何日目に該当するのかの日数が取れます。三桁分にゼロ埋めされる形で返ってくるようです。 SELECT user_id, time, DATE_FORMAT(CAST(time AS TIMESTAMP), '%j') AS second FROM athena_workshop.login WHERE time >= '2021-01-05 15:05:00' AND dt = '2021-01-05' LIMIT 10 日時の部分的な抽出処理を楽にする関数集 Athena(Presto)には特定部分の日時を抽出するのに便利な関数が色々用意されています。これらで完結するならこちらを使うとSQLの記述がシンプルになるケースがあります。 以降の節ではそれらの関数についてそれぞれ触れていきます。各関数の引数には日時(TIMESTAMP)に型変換(キャスト)したカラムなどが必要になります。また、各関数の結果はゼロ埋めなどはされず整数などで返ってきます。 YEAR関数 YEAR関数では年部分を抽出することができます。 SELECT user_id, time, YEAR(CAST(time AS TIMESTAMP)) AS year FROM athena_workshop.login WHERE time >= '2021-01-05 15:05:00' AND dt = '2021-01-05' LIMIT 10 QUARTER関数 QUARTERでは各日時が第何四半期に該当するのかの値が取れます。値は1からスタートします(1~4の値となります)。 SELECT user_id, time, QUARTER(CAST(time AS TIMESTAMP)) AS quarter FROM athena_workshop.login WHERE time >= '2021-01-05 15:05:00' AND dt = '2021-01-05' LIMIT 10 MONTH関数 MONTH関数では月部分の整数が取れます。 SELECT user_id, time, MONTH(CAST(time AS TIMESTAMP)) AS month FROM athena_workshop.login WHERE time >= '2021-01-05 15:05:00' AND dt = '2021-01-05' LIMIT 10 WEEK関数 WEEK関数では該当の日時が1年の中で第何週に該当するのかの値が取れます。 SELECT user_id, time, WEEK(CAST(time AS TIMESTAMP)) AS week FROM athena_workshop.login WHERE time >= '2021-02-10 15:05:00' AND dt = '2021-02-10' LIMIT 10 DAY関数 DAY関数では該当の日時の日部分が整数で返ってきます。 SELECT user_id, time, DAY(CAST(time AS TIMESTAMP)) AS day FROM athena_workshop.login WHERE time >= '2021-02-05 15:05:00' AND dt = '2021-02-05' LIMIT 10 DAY_OF_WEEKとDOW関数 DAY_OF_WEEK関数では該当の日時の曜日の数値が返ってきます。月曜が1で日曜が7となります。 SELECT user_id, time, DAY_OF_WEEK(CAST(time AS TIMESTAMP)) AS day_of_week FROM athena_workshop.login WHERE time >= '2021-02-05 15:05:00' AND dt = '2021-02-05' LIMIT 10 DOW関数はDAY_OF_WEEKの短縮系となります。こちらも同じ挙動をします。 SELECT user_id, time, DOW(CAST(time AS TIMESTAMP)) AS day_of_week FROM athena_workshop.login WHERE time >= '2021-02-05 15:05:00' AND dt = '2021-02-05' LIMIT 10 DAY_OF_YEARとDOY関数 DAY_OF_YEAR関数では1年の中で何日目に該当するのかの値を取得できます。 SELECT user_id, time, DAY_OF_YEAR(CAST(time AS TIMESTAMP)) AS day_of_year FROM athena_workshop.login WHERE time >= '2021-02-05 15:05:00' AND dt = '2021-02-05' LIMIT 10 DOY関数はDAY_OF_YEAR関数の短縮系です。こちらも同じ挙動をします。 SELECT user_id, time, DOY(CAST(time AS TIMESTAMP)) AS day_of_year FROM athena_workshop.login WHERE time >= '2021-02-05 15:05:00' AND dt = '2021-02-05' LIMIT 10 HOUR関数 HOUR関数では時間が取れます(0~23)。 SELECT user_id, time, HOUR(CAST(time AS TIMESTAMP)) AS hour FROM athena_workshop.login WHERE time >= '2021-02-05 05:05:00' AND dt = '2021-02-05' LIMIT 10 MINUTE関数 MINUTE関数では分の値が取れます(0~59)。 SELECT user_id, time, MINUTE(CAST(time AS TIMESTAMP)) AS minute FROM athena_workshop.login WHERE time >= '2021-02-05 05:08:00' AND dt = '2021-02-05' LIMIT 10 SECOND関数 SECOND関数では秒の値が取れます(0~59)。 SELECT user_id, time, SECOND(CAST(time AS TIMESTAMP)) AS second FROM athena_workshop.login WHERE time >= '2021-02-05 05:00:00' AND dt = '2021-02-05' LIMIT 10 参考文献・参考サイトまとめ The Applied SQL Data Analytics Workshop: Develop your practical skills and prepare to become a professional data analyst, 2nd Edition HAVINGの使い方 【SQL入門】 サブクエリを使った検索条件の設定 WHERE句のサブクエリは大抵の場合テーブルJOINで代替できる SQL関数coalesceの使い方と読み方 coalesceとは pandas get the row-wise minimum value of two or more columns Comparison Functions and Operators Mathematical Functions and Operators Add months to a date in Pandas pandasで文字列にスライスを適用して任意の位置・長さの部分を抽出 String Functions and Operators pandas.DataFrameの複数の列の文字列を結合して新たな列を生成 Treasure Data(Presto/Hive)で月の最終日を取得する方法 Date and Time Functions and Operators athena(presto)で、dateとtimeからJSTを得る using time zone in pandas to_datetime DataFrame からタイムゾーンを削除したいがうまくいかない pandasでunixtimeへの変換 prestoの日付関数 pandasデータフレームの日付型⇄文字型変換とdfply使用時のメモ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS】Amazon Route53について

はじめに AWS認定試験取得に向けてAWSの知識を整理するためのまとめです。 今回はRoute53についてまとめます。 Amazon Route53とは 可用性と拡張性に優れたクラウドのDNS(ドメインネームシステム)サービスです。 DNSというのはドメイン名(例:www.example.com)を、IPアドレス(例:192.0.2.2)へ変換し、 ルーティングする仕組みです。 Amazon Route53の特徴 ホストゾーン ホストゾーンとはレコードのコンテナであり、レコードには example.com や そのサブドメインのトラフィックをどのようにルーティングするかに関する情報を保持します。 ホストゾーンの名前と対応するドメインの名前は同じです。 ホストゾーンは2種類あります。 ホストゾーン 詳細 パブリックホストゾーン トラフィックをインターネット上でどのようにルーティングするか指定するレコードが含まれています。 プライベートホストゾーン トラフィックをVPCでどのようにルーティングするかを指定するレコードが含まれています。 ルーティングポリシー ルーティングポリシーとはクエリに対してどう応答するかを決めるポリシーです。 レコードを作成するときにルーティングポリシーを選択します。 ルーティングポリシー 詳細 シンプルルーティング 一つのリソース(1つのウェブサーバなど)に対して、ルーティングするとき(スタンダード) フェイルオーバールーティング アクティブ/パッシブフェイルオーバー構成の時に使用しますプライマリリソースとセカンダリリソースを用意し、プライマリリソースで異常が発生した場合、セカンダリリソースへ切り替えます。 位置情報ルーティング ユーザの位置情報に基づいてルーティングする場合に使用します。具体的にはクライアントのIPアドレスをもとに、国、地域にローカライズされたコンテンツを表示する場合に使用します。 地理的接近ルーティング ユーザに物理的に近い場所にあるリソースを提供する場合に使用します。 レイテンシールーティング 複数のAWSリージョンにリソースがあり、より少ない往復時間で最良のレイテンシーを実現するリージョンにルーティングする場合に使用します。 複数値回答ルーティング(マルチバリュー) 複数のリソースにDNSレスポンスを分散する場合に使用します。ヘルスチェックも同時にされて、正常なところへルーティングされる。 加重ルーティング 指定した比率で複数のリソースにルーティングする場合に使用します。 DNSレコードタイプ レコードタイプとは Route53でサポートされるレコードタイプは下記の通りです。 レコードタイプ 詳細 A レコードタイプ ホスト名とIPアドレス(IPv4)を関連付けます。 AAAA レコードタイプ ホスト名とIPアドレス(IPv6)を関連付けます。 CAA レコードタイプ ドメインの証明書を発行する認証機関を指定します。第三者が勝手にサーバ証明書を発行することを防止できます。 CNAME レコードタイプ ホスト名とドメインを関連付けます。例えばtest.example.comに対して、 別のドメインsample.example.comなどを関連づけることができます。 DS レコードタイプ DNSSECを利用するためのレコードです。 DNSSECとはDNSキャッシュポイズニングのような攻撃を防ぐ仕組みで、それを利用する場合はこのレコードを設定します。 MX レコードタイプ メールサーバーの名前を指定します。 NAPTR レコードタイプ 名前付け権限ポインタ (NAPTR) は、動的委任発見システム (DDDS) アプリケーションで、1 つの値を別の値に変換または置き換えるために使用されるレコードのタイプです。例えば、1 つの一般的な用途は、電話番号を SIP URI に変換する場合です。 NS レコードタイプ ホストゾーンのネームサーバーを設定します。 PTR レコードタイプ IPアドレスに対応するドメイン名を設定します。IPアドレスからホスト名を割り出す「逆引き」時に利用されます。 SOA レコードタイプ ゾーンを管理するための情報などを設定します。 SPF レコードタイプ メールの送信者の身元を確認するために使用されていましたが、運用性の問題から現在はTXTレコードで作成することが推奨されています。 SRV レコードタイプ 負荷分散サービスの提供/冗長性の確保/サービスポート番号の通知を可能にするレコードです。 TXT レコードタイプ ホスト名に関連付けるテキスト情報(文字列)を定義するレコードです。送信ドメイン認証の認証情報などを記述します。(SPFレコードの設定も可能です) 他のサービスとの連携 CloudTrail Route53に対して発行されたリクエストの種類、リクエストの発行元IPアドレスなどを記録できます。 CloudWatch CloudWatchを使用する事でRoute53のヘルスチェックの状態を監視できます。 なんらか異常が発生したときは、異常を検知してメールなどで通知することも可能です。 他にも様々なサービスと連携可能です。 料金 基本的には従量課金制となります。 ホストゾーンの管理 Route53で管理しているホストゾーンごとに月額料金が発生します。 DNSクエリへの応答 クエリの内容ごとに日割り計算で料金が発生します。 他にも様々な要素で料金が発生します。 参考サイト https://aws.amazon.com/jp/route53/ https://dev.classmethod.jp/articles/route53-resolver/ https://aws-community.com/aws-services-route-53/ https://www.nic.ad.jp/ja/basics/terms/dnssec.html https://help.onamae.com/answer/7883
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【AWS認定資格】SOA-C02にアップデートされたぞぉぉぉ!!(途中なのでちょくちょく更新)

はじめに AWS SysOps Administraitor - Associateが2021/07/26をもって、SOA-C01から完全にSOA-C02へと移行されます。 ちょっとずつまとめていきます。 とりあえず、試験ガイドの比較だけ見ていただければ(笑) ※この記事に網羅性はありません。すべて個人的に出そうだな~というところをまとめたものです。 試験ガイド 比較 ・SOA-C01 試験ガイドはこちら ・SOA-C02 試験ガイドはこちら ・試験ガイドの比較 項目 SOA-C01 SOA-C02 評価項目 ・AWS での拡張性、可用性、フォルトトレランス性の高いシステムのデプロイ、管理、運用・AWS との間で送受信されるデータフローの実現と制御・コンピューティング、データ、セキュリティの要件に基づく、適切な AWS サービスの選択・AWS の運用に関するベストプラクティスの適切な適用方法の特定・AWS の利用料金の見積りと、運用コストの管理方法の特定・オンプレミスのワークロードの AWS への移行 ・AWS 上のワークロードを展開、管理、および運用する。・AWS Well-Architected フレームワークに従って AWS ワークロードをサポートおよび保守する。・AWS Management Console および AWS CLI を使用して運用する。・セキュリティコントロールを実装することにより、コンプライアンス要件を満たす。・システムを監視、ロギング、およびトラブルシューティングする。・ネットワーキングに関する概念 (例: DNS、TCP/IP、ファイアウォール) を適用する。・アーキテクチャ要件 (例: 高可用性、パフォーマンス、キャパシティ) を実装する。・事業継続およびディザスタリカバリに関する手順を実行する。・インシデントを特定、分類、および解決する。 回答タイプ 2種類・択一選択問題: 選択肢には 1 つの正解と 3 つの不正解 (誤答) があります。・複数選択問題: 5 つ以上の選択肢の中に 2 つ以上の正解があります。 3種類(増えた…)・択一選択問題:選択肢には 1 つの正解と 3 つの不正解 (誤答) があります。・複数選択問題:5 つの選択肢のうち、2 つが正解です。(選択肢5個以上じゃなくなった?)・試験ラボ:AWS Management Console または AWS CLI で実行するタスクで構成されたシナリオです。(後述)(!?) 合格スコア 720 720 試験内容の概要 分野1:モニタリングとレポート 22%分野2:高可用性 8%分野3:展開とプロビジョニング 14%分野4:ストレージ及びデータの管理 12%分野5:セキュリティとコンプライアンス 18%分野6:ネットワーク 14%分野7:自動化と最適化 12% 分野1:監視、ロギング、及び解決 20%分野2:信頼性および事業継続性 16%分野3:展開、プロビジョニング、および自動化 18%分野4:セキュリティおよびコンプライアンス 16%分野5:ネットワークおよびコンテンツ配信 18%分野6:コストおよびパフォーマンスの最適化 12% 全体的にすごく豪華になった印象です(笑) 回答タイプは増えていますし、概要の分野は減ってこそいますが、すべての分野で「および」が入っているので、実質増えています(笑) 新回答タイプ!試験ラボ!! こちらを見ていただけるとイメージが沸くかなと思いますが、右側の指示に対して実際にオペレーションをしていくというハンズオンみたいな形式ですね。 サンプル見る限りではラッキー問題に見えますが、どうなんでしょうか。 例えば、「災対環境への切替を行え」とか来たら、なかなか普段行わない業務なので、戸惑いそうですね。 なにより、これから上位資格でもこの形式が追加されるかもしれないと思うと… 概要 試験ガイドが非常に丁寧になったこともあり、まとめやすくなったので、時間があるときに少しずつまとめていきます。とりあえず書けるところまで、メモ書き程度に… 分野 1: 監視、ロギング、および解決 1.1 AWS の監視サービスおよびロギングサービスを使用して、メトリクス、アラーム、およびフィルタを実装する。 ログを識別、収集、分析、およびエクスポートする (例: Amazon CloudWatch Logs、CloudWatch Logs Insights、AWS CloudTrail ログ)。 ■CloudWatch Logs 公式ドキュメントはこちら EC2インスタンス、CloudTrail、Route53などのAWS内の多くのサービスのログを保存することができます。 ↓ロググループとログストリームのイメージ 【機能】 ・ CloudWatch Logs Insights Amazon CloudWatch Logsのログデータを検索し分析することができます。 CloudWatch Logs Insightsで使用されるクエリについてはこちらを参照してください。 ・ ログの保持期間 ロググループごとに保持期間を設定できます。(デフォルトは無制限) 選択可能な範囲は1日間~10年間 ずっとロググループに保存しているとコストが高くつくので、特定の期間ロググループで保存した後は、S3に移動するのが推奨ですね。 ■AWS CloudTrail ログ APIコールを取得します。 証跡を追うために用いられるサービスですね。 イベント履歴の保持期間は90日間のため、証跡を作成して、S3バケットに保存します。 CloudWatch エージェントを使用して、メトリクスおよびログを収集する。 CloudWatch Agentを使用すると、OS内部から情報を取得できます。 例えば、メモリ使用率やディスクI/Oなどを取得する要件がある場合はCloudWatch Agentのインストールが必要になります。 また、CloudWatch AgentからCloudWatchサービスエンドポイントへのアクセスを許可する必要があります。 ・インスタンスにIAMロールのアタッチ ・アクセス経路の確保  VPCエンドポイント経由 or IGW経由   CloudWatch アラームを作成する。 特定のイベントを検知して発報するサービスです。(「CPU使用率が80%を超えた」など) 【アラームの状態(ステータス)】 状態 説明 OK メトリクスや式は、定義されているしきい値の範囲内です。 ALARM メトリクスまたは式が、定義されているしきい値を超えています。 INSUFFICIENT_DATA アラームが開始直後であるか、メトリクスが利用できないか、メトリクス用のデータが不足しているため、アラームの状態を判定できません。 【設定値】 設定項目 説明 例 期間 メトリクスや式を評価する期間  1分と指定した場合、1分に1回評価を行います 評価期間 アラームの状態を決定するまでに要する最新の期間またはデータポイントの数 5と指定すると、最新の5個のデータポイントを評価します アラームを実行するデータポイント アラームが ALARM 状態に移るためにしきい値を超過する必要がある評価期間内のデータポイントの数 3と指定すると評価期間内の3個のデータポイントで閾値を超過した場合に、ALARM状態に移行します 例) 期間:1分 評価期間:5 アラームを実行するデータポイント:3 評価期間(5)のうち、アラームを実行するデータポイント(3)が閾値を超えたときアラームを発報します。 【高解像度アラーム】 通常のアラームの場合には、60秒の倍数でしか期間を設定できません。 しかし、高解像度アラームを使用することで、10秒または30秒の期間を指定できます。 (高解像度アラームは有料です。料金はこちらから) 【数式に基づくアラーム】 1 つ以上の CloudWatch メトリクスに含む数式の結果に基づいてアラームを設定できます。 アラーム用の数式には 10 個までのメトリクスを含めることができます。各メトリクスは同じ期間を使用している必要があります。 数式に基づくアラームでは Amazon EC2 アクションを実行できません。 メトリクスフィルタを作成する。 ログから特定の文字列(ERRORなど)を検知して発報します。 CloudWatch ダッシュボードを作成する。 メトリクスを可視化することができるサービスですね。 ↓こんな感じ(私の環境はまともなメトリクスを取ってなかったので、こちらから拝借) CloudWatchダッシュボードはマルチアカウントにも対応していて、管理下のアカウントで作成されたダッシュボードを一つのアカウントから閲覧することもできます。 マルチアカウントで、運用管理を一元化する要件が出た際には設定する項目かと思います。 該当記事:Amazon CloudWatch でクロスアカウント、クロスリージョンダッシュボードの利用が可能に 通知を構成する (例: Amazon Simple Notification Service (Amazon SNS)、ServiceQuotas、CloudWatch アラーム、AWS Health イベント)。 通知のよくある構成は先ほどの「メトリクスフィルタを作成する。」を確認してください。 ■Amazon SNS 言わずと知れた、AWSで通知を行う際に最も使われているサービスですね。 【イベント送信先】 大まかに分けると以下の二種類があります。 ・A2Aメッセージング(アプリケーション to アプリケーション) 例)Kinesis Data Firehose、SQS ・A2P通知(アプリケーション to Person) 例)Eメール、SMS 詳しくはこちら 【FIFOトピック】 重複排除や順序などの文言がある場合はFIFOトピックを検討する必要があります。 SQS FIFOキューと一緒に使用することが前提みたいですね。 詳しくはこちら 【サブスクリプションフィルター】 メッセージの属性でフィルターをかけて、送信先を制御することができます。 該当記事:SNSサブスクリプションのフィルタポリシーで良い知らせだけをメール受信する ■AWS Service Quotas 各リソースの上限数を管理します。 上限緩和申請もここから行えます。 Service Quotasと使用状況メトリクスを統合しているサービスはこちらを参照してください。 2021/07/20時点では、以下のサービスが統合されています。 ・AWS CloudHSM ・Amazon CloudWatch ・Amazon DynamoDB ・Amazon EC2 ・Amazon Elastic Container Registry ・AWS Fargate ・AWS Fault Injection Simulator ・AWS Interactive Video Service ・AWS Key Management Service ・Amazon Kinesis Data Firehose ・AWS Robomaker 通知設定を行うための手順は下記の記事が参考になります。 該当記事:CloudWatch で Service Quotas をグラフ化・アラート通知できるようになりました! ■AWS Health イベント 公式ドキュメント AWS Healthというよりは我々が意識しなければいけないのはAWS Personal Health Dashboard(PHD)を意識する必要があります。 AWS Personal Health Dashboard を使用して、AWS サービスまたはアカウントに影響を与える可能性がある AWS Health イベントについて確認できます。 アカウント固有の障害が発生した場合にはPHDにイベントが送られます。 リージョン規模の障害が発生した場合にはService Health Dashboardを使用する必要があります。 該当記事:リージョン規模の障害が発生した時に Service Health Dashboard から障害情報の通知を受け取る方法 通知設定については、こちらをご参照ください。 1.2 監視メトリクスおよび可用性メトリクスに基づいて問題を解決する。 通知およびアラームに基づいて、トラブルシューティングを行うかまたは是正措置を講じる。 エラーメッセージから対応策を考えるみたいな問題が出題されるのかなと思います。 以下、例です。(ざっと調べたやつまとめ) 個人的に出題されそうな対応策は太字にしてます。(ニュアンス) エラーメッセージ 対応策 InsufficientInstanceCapacity ・数分間待ってからリクエストを再度送信します。容量は頻繁に変化します。・インスタンス数を減らして新しいリクエストを送信します。たとえば、15 インスタンスを起動する 1 つのリクエストを行っている場合、代わりに 5 つのインスタンスに対する 3 つのリクエストを作成するか、1 つのインスタンスに対する 15 のリクエストを作成してみてください。・インスタンスを起動する場合は、アベイラビリティーゾーンを指定しないで新しいリクエストを送信します。・インスタンスを起動する場合は、別のインスタンスタイプを使用して新しいリクエストを送信します (これは後でサイズを変更できます)。詳細については、「インスタンスタイプを変更する」を参照してください。・クラスタープレイスメントグループに複数のインスタンスタイプでインスタンスを起動すると、容量不足エラーが発生する場合があります。同じインスタンスタイプで起動するのが推奨です。詳細については、「プレイスメントグループのルールと制限」を参照してください。 InstanceLimitExceeded 上限緩和申請(<サービス名>LimitExceededは対応していればすべて上限緩和申請で対応) UnauthorizedOperation decode-authorization-messageコマンドを実行しデコードします。不足している権限を該当のIAMリソースにアタッチします Client.InternalError ・EBSの名前を変更し、再アタッチする。・ボリュームからスナップショットを作成し、新しいボリュームを作成、アタッチする。・ボリュームで使用されているキーへのアクセス権をアタッチする。 Rate exceeded ・ GetResourceConfigHistoryまたは ListDiscoveredResources API 呼び出しの場合は、手順に従って、AWS Config コンソールのエラーメッセージをトラブルシューティングします。・PutMetricData API 呼び出しについては、「CloudWatch API で PutMetricData を呼び出すときに、スロットルを回避する方法を教えてください。」をご参照ください。・AWS Auto Scaling に関連する API 呼び出しについては、「Auto Scaling API 呼び出しがスロットリングされています。これを防ぐにはどうすればよいですか ?」をご参照ください。・AWS Lambda 関数に関連する API 呼び出しについては、「 「Rate exceeded」エラーと 429「TooManyRequestsException」エラーの Lambda 関数のスロットリングをトラブルシューティングする方法を教えてください。」をご参照ください。・AWS Elastic Beanstalk に関連する API 呼び出しについては、「Elastic Beanstalk で API スロットリングまたは「Rate Exceeded」エラーを解決する方法を教えてください。」をご参照ください。・また、待機ステートメントを追加することで、スロットリングが発生した後で AWS API 呼び出しを再試行することを許可することもできます。詳細については、「AWS でのエラーの再試行とエクスポネンシャルバックオフ」をご参照ください。 Access Denied 場合によりすぎるので、省略Amazon S3 から 403: Access Denied エラーをトラブルシューティングする方法 特に覚えておかないといけないのは、「上限緩和申請」「エクスポネンシャルバックオフ」「同時実行制限(Lambda)」あたりでしょうか。 キリがないので、一旦ここまでにしておきます… 参考: EC2 インスタンスを開始または起動できないのはなぜですか? EC2 インスタンスを開始または起動する際に発生する、InsufficientInstanceCapacity エラーのトラブルシューティング方法を教えてください。 EC2 インスタンスを起動しようとすると、「この操作を実行する権限がありません」というエラーメッセージが表示されるのはなぜですか? インスタンスを開始できず、describe-instances コマンドの実行時に Client.InternalError が表示されます。この問題を解決する方法を教えてください。 どの API 呼び出しが「Rate exceeded」エラーを引き起こしているかを確認するにはどうすればよいですか? アクションをトリガするよう、Amazon EventBridge ルールを構成する。 Amazon EventBridge は、直接関連していない独自のアプリケーションや AWS 外で提供される SaaS (Software-as-a-Service) アプリケーション、および AWS のサービスから送信されるデータを配信先としてサポートしている AWS サービスにルーティングするイベントバスを Amazon EventBridge コンソールで登録設定するだけでご利用いただけるよう機能およびリソースを提供しているサービスです。 下記の記事が非常にわかりやすいです。 サーバーレスのイベントバスって何 ? Amazon EventBridge をグラレコで解説 「Amazon EventBridge ルール」と「Amazon CloudWatch Events ルール」って何が違うの?ってたまに聞かれますが… ほぼ同じものです!Amazon CloudWatch Events ルールをベースにしたものがAmazon EventBridge ルールです、2021/07/21時点では両方使えますし、 片方にルールを追加するとちゃんと追う一方に反映されますね。 好きな方を使いましょう(笑) (イベントバスとかの見易さ的に個人的にはAmazon EventBridge ルールの方が好きですが、CloudWatchの他のサービスと一緒に使うことも多いと思うので、ほんとに好みの問題ですね) 【ルールの種類】 ・イベントに反応するルール   特定のイベントが実行された際にトリガーします。 ・スケジュールで実行するルール   時間を指定して、トリガーします。   使用できる形式は「cron式」「rate式」です。 AWS Systems Manager 自動化ドキュメントを使用して、AWS Config ルールに基づくアクションを実行する。 AWS Config では、AWS Config Rules で評価されている非準拠のリソースを修復できます。AWS Config では、AWS Systems Manager 自動化ドキュメントを使用して修復が適用されます。これらのドキュメントでは、非準拠で実行されるアクションを定義します。AWSによって評価されたリソースAWS Config Rules。SSM ドキュメントは、AWSManagement Console または API を使用して関連付けることができます。 例) required-tagsルールで特定のタグをつけていないリソースを検知した際、ルールの修復アクションに「AWS-SetRequiredTags」ドキュメントを使用したオートメーションを設定しておくことで、自動的に指定のタグをつけるといったことが可能になります。 分野 2: 信頼性および事業継続性 2.1 拡張性および伸縮性を実装する。 AWS Auto Scaling プランを作成および保守する AWS Auto Scaling は、アプリケーションを監視し、可能な限り低コストで安定した予測可能なパフォーマンスを維持するように自動的に容量を調整します。 スケーリングプランの対象とするリソースは以下のように三つの方法で検索することができます。 【スケーリング戦略】 スケーリング戦略(プラン)を指定します。 一番右側のカスタムを選択するとスケーリングメトリクスを編集することが可能になります。 【ベストプラクティス】 ・1 分間隔で Amazon EC2 インスタンスメトリクスをスケール ・Auto Scaling グループメトリクスを有効にする ・Auto Scalingグループが使用しているインスタンスタイプを確認 【ActiveWithProblemsエラー】 スケーリングプランがアクティブであっても、1 つまたは複数のリソースのスケーリング設定を適用できなかった場合に、このエラーが発生します。 このエラーを回避するためには以下の点について考慮する必要があります。 ・予測スケーリングでは、新しい Auto Scaling グループを作成してから 24 時間待ってスケーリングを設定する ・複数のプランで同じリソースが選択されている場合、既存のスケーリングポリシーを削除し、AWS Auto Scaling コンソールから作成されたターゲット追跡スケーリングポリシーに置き換えることができます。スケーリングポリシーを上書きする各リソースの [Replace external scaling policies (外部スケーリングポリシーを置き換え)] 設定を有効にします。 参考:AWS Auto Scalingスケーリングプランのベストプラクティス キャッシングを実装する。 急に抽象的になりましたが、きっとこの記事とか見ておくと良いのかな…知らんけど…… ぱっと思いつくキャッシングサービスは以下の通りです。 ・ElastiCache ・Amazon DynamoDB Accelerator (DAX) ・ローカルキャッシュ ・エッジキャッシュ(CloudFront) 詳しくはこちらの「ユースケースと業種」を見てみるとよいと思います。 Amazon RDS レプリカおよび Amazon Aurora レプリカを実装する。 SAAもそうですけど、ホント好きですよねリードレプリカ スタンバイインスタンスと一緒に出題されることが多い印象ですが、スタンバイインスタンスは読み込み処理とかできないですね。 リードレプリカとマルチAZ配置についてもよく出題される箇所かなと思います。 こちらの「リードレプリカ、マルチ AZ 配置、およびマルチリージョン配置」の比較表がわかりやすくて好きです。 読み取りワークロードが高いレプリケーションラグを許容できず、5つを超える読み取りレプリカが必要な場合は、AuroraPostgreSQLの方が適しています。レプリケーションの遅延が数秒から数分で許容可能であり、読み取りワークロードに最大5つのレプリカで十分な場合は、Amazon RDS forPostgreSQLの方が適しています。 とあるように、Auroraの方がより高速です。 RDSとAuroraの比較については、以下の記事が非常にわかりやすいです。(画像リンク) 疎結合アーキテクチャを実装する。 疎結合 = SQSと覚えてしまっている自分はダメ人間でしょうか… 【そもそも疎結合って?】 マイクロサービスのようにそれぞれのサービスが独立している状態で繋がっているものを疎結合という 参考:マイクロサービスの概要 サービスの形式には大きく分けて二種類あります。 「モノリシック」と「マイクロサービス」です。 ・モノリシック  すべてのプロセスが密に結合し、単一のサービスとして実行される ・マイクロサービス  サービス内の複数のコンポーネントが1つのサービスとして独立していて、それぞれをつなげる  各サービスの障害の影響が小さく済む それぞれにメリデメはありますが、最近の流行は明らかにマイクロサービスですね。 その疎結合を実現するサービスとして最もよく出しゃばってくるのがSQSです!(※個人の意見です) SQSについてはAWSのグラレコ記事が出ていてわかりやすいのでこちらを参照してください。 水平スケーリングと垂直スケーリングを使い分ける。 EC2インスタンスを例にすると、それぞれ以下のようになります。 ・水平スケーリング(スケールアウト/イン):インスタンスの数を増減する ・垂直スケーリング(スケールアップ/ダウン):インスタンスタイプを変更する(スペックを変更する) 以下、イメージ 【使い分け】 垂直スケーリング 水平スケーリング リソースの限界が存在 論理的には限界が存在しない インスタンスの停⽌が伴う インスタンスの停⽌が伴わない 参考; Scaling Your Amazon RDS Instance Vertically and Horizontally クラウドのためのアーキテクチャ設計 2.2 高可用性および回復性を備えた環境を実装する。 Elastic Load Balancer および Amazon Route 53 のヘルスチェックを構成する。 ■Auto Scaling インスタンスのHealth チェック Auto Scalingのヘルスチェックは以下の3種類のいずれかから通知を受け取ります。 ・Amazon EC2(デフォルト) インスタンスが以下の状態であるということを判断した場合に通知を出します。 〇stopping 〇stopped 〇shutting-down 〇terminated インスタンス自体に問題がないと検知しないため、中のアプリケーションでエラーが起きていても通知は出しません。 ・ELB ターゲットへのリクエストが正常に終了しない場合に通知を出します。 ステータスコードまで確認するため、アプリケーションでエラーが発生した場合も通知を出します。 ・カスタムヘルスチェック 独自のヘルスチェックシステムがある場合はこちらを使用します。 ■Elastic Load Balancerのヘルスチェック 詳しくは公式ドキュメントを参照してください。 【ヘルスチェックの設定】 よく見る設定値のみ、下記に記載します。 設定 説明 HealthyThresholdCount 非正常なインスタンスが正常であると見なすまでに必要なヘルスチェックの連続成功回数。範囲は 2~10 です。デフォルトは 3 です。 UnhealthyThresholdCount 非正常なインスタンスが非正常であると見なすまでに必要なヘルスチェックの連続失敗回数。この値は正常なしきい値カウントと同じでなければなりません。 matcher [HTTP/HTTPS ヘルスチェック] ターゲットからの正常なレスポンスを確認するために使用する HTTP コード。この値は、200~399 である必要があります。 【ターゲットヘルスステータス】 【ヘルスチェックの理由コード】 ターゲットのステータスが Healthy 以外の値の場合、API は問題の理由コードと説明を返し、コンソールのツールヒントで同じ説明が表示されます。Elb で始まる理由コードはロードバランサー側で発生し、Target で始まる理由コードはターゲット側で発生します。 ■Route53のヘルスチェック 【Amazon Route 53 ヘルスチェックの種類】 ・エンドポイントをモニタリングするヘルスチェック Route 53 は、世界各地にヘルスチェッカーを持っています。エンドポイントをモニタリングするヘルスチェックを作成すると、ヘルスチェッカーは、エンドポイントが正常であるかどうかを判断するためにユーザーが指定するエンドポイントにリクエストの送信を開始します <ヘルスチェッカーが正常性を評価するために使用する二つの値> 〇応答時間 〇失敗の閾値 <ヘルスチェックのタイプ> 〇HTTP/HTTPS ヘルスチェック 〇TCP ヘルスチェック 〇HTTP/HTTPS ヘルスチェックと文字列一致  指定の文字列が、レスポンス本文に含まれているかを検索し、なかった場合ヘルスチェックは不合格となります。  検索文字列はレスポンス本文の最初の5,120バイト内に存在する必要があります。 Route 53 は、実際のステータス (正常または非正常) を決定するための十分なデータを得るまでは、新しいヘルスチェックを正常と見なします。ヘルスチェックのステータスを反転するオプションを選択した場合、Route 53 は、十分なデータを得るまでは、新しいヘルスチェックを非正常と見なします。 ・他のヘルスチェック (算出したヘルスチェック) を監視するヘルスチェック ヘルスチェックの入れ子のような状態です。 1つの親ヘルスチェックは255個の子ヘルスチェックを監視できます。 以下、イメージです。 ・CloudWatch アラームをモニタリングするヘルスチェック CloudWatchアラームの状態によって、正常稼働どうかを判断します。 CloudWatch Alarm Route53ヘルスチェック OK 正常 Alarm 異常 データ不足 InsufficientDataHealthStatusの値 詳しくはこちらを参照してください。 単一アベイラビリティーゾーン配置とマルチ AZ 配置を使い分ける (例: Amazon EC2 AutoScaling グループ、Elastic Load Balancing、Amazon FSx、Amazon RDS)。 ■AutoScaling Group Multi AZ フォールトトレラントなワークロードを実装する (例: Amazon Elastic File System(Amazon EFS)、Elastic IP アドレス)。 Route 53 ルーティングポリシーを実装する (例: フェールオーバー、加重、レイテンシーベース)。 2.3 バックアップ戦略および復元戦略を実装する。 用途に基づいてスナップショット作成処理およびバックアップ処理を自動化する (例: RDSスナップショット、AWS Backup、RTO/RPO、Amazon Data Lifecycle Manager、保持ポリシー)。 データベースを復元する (例: 特定時点復元、リードレプリカの昇格)。 バージョニングルールおよびライフサイクルルールを実装する。 Amazon S3 のリージョン間レプリケーションを構成する。 ディザスタリカバリ手順を実行する。 分野 3: 展開、プロビジョニング、および自動化 3.1 クラウドリソースをプロビジョニングおよび保守する。 AMI を作成および管理する (例: EC2 Image Builder)。 AWS CloudFormation の構成要素を作成、管理、およびトラブルシューティングする。 複数の AWS リージョンおよび AWS アカウントにまたがってリソースをプロビジョニングする (例: AWS Resource Access Manager、CloudFormation StackSets、IAM クロスアカウントロール)。 展開シナリオおよび展開サービスを選択する (例: ブルー/グリーン、ローリング、カナリア)。 展開に関する問題を特定および解決する (例: サービスクオータ、サブネットのサイズ変更、CloudFormation エラー、AWS OpsWorks エラー、権限)。 3.2 手動プロセスまたは繰り返し実行されるプロセスを自動化する。 AWS サービスを使用して、展開プロセスを自動化する (例: OpsWorks、Systems Manager、CloudFormation)。 修正プログラムの自動管理を実装する。 AWS サービスを使用して、自動化されたタスクをスケジューリングする (例:EventBridge、AWS Config)。 分野 4: セキュリティおよびコンプライアンス 4.1 セキュリティポリシーおよびコンプライアンスポリシーを実装および管理する。 IAM 機能を実装する (例: パスワードポリシー、MFA、ロール、SAML、フェデレーションID、リソースポリシー、ポリシー条件)。 AWS サービスを使用して、アクセスに関する問題をトラブルシューティングおよび監査する(例: CloudTrail、IAM Access Analyzer、IAM Policy Simulator)。 サービスコントロールポリシーおよび権限境界の妥当性を検査する。 AWS Trusted Advisor セキュリティ検査の内容を確認する。 コンプライアンス要件に基づいて、選択した AWS リージョンおよびサービスの妥当性を検査する。 セキュアなマルチアカウント戦略を実装する (例: AWS Control Tower、AWSOrganizations)。 4.2 データ保護戦略およびインフラストラクチャ保護戦略を実装する。 データ分類スキームを適用する。 暗号化キーを作成、管理、および保護する。 格納データの暗号化を実装する (例: AWS Key Management Service (AWS KMS))。 送信中データの暗号化を実装する (例: AWS Certificate Manager、VPN)。 AWS サービスを使用して、シークレットをセキュアな方法で格納する (例: AWS SecretsManager、Systems Manager、Parameter Store)。 レポートまたは調査結果の内容を確認する (例: AWS Security Hub、Amazon GuardDuty、AWS Config、Amazon Inspector)。 分野 5: ネットワーキングおよびコンテンツ配信 5.1 ネットワーキング機能および接続を実装する。 VPC を構成する (例: サブネット、ルーティングテーブル、ネットワーク ACL、セキュリティグループ、NAT ゲートウェイ、インターネットゲートウェイ)。 プライベート接続を構成する (例: Systems Manager Session Manager、VPC エンドポイント、VPC ピアリング、VPN)。 AWS のネットワーク保護サービスを構成する (例: AWS WAF、AWS Shield) 5.2 ドメイン、DNS サービス、およびコンテンツ配信を構成する。 Route 53 のホストゾーンおよびレコードを構成する。 Route 53 ルーティングポリシーを実装する (例: 位置情報、地理的近接性)。 DNS を構成する (例: Route 53 Resolver)。 Amazon CloudFront および S3 オリジンアクセスアイデンティティ (OAI) を構成する。 S3 静的 Web サイトホスティングを構成する。 5.3 ネットワーク接続問題をトラブルシューティングする。 VPC 構成を理解する (例: サブネット、ルーティングテーブル、ネットワーク ACL、セキュリティグループ)。 ログを収集および解釈する (例: VPC フローログ、Elastic Load Balancer アクセスログ、AWS WAF Web ACL ログ、CloudFront ログ)。 CloudFront のキャッシング問題を特定および解決する。 ハイブリッド接続問題およびプライベート接続問題をトラブルシューティングする。 分野 6: コストおよびパフォーマンスの最適化 6.1 コスト最適化戦略を実装する。 コスト配分タグを実装する。 AWS のサービスおよびツールを使用して、使用率の低いリソースや使用していないリソースを特定し、修正する (例: Trusted Advisor、AWS Compute Optimizer、Cost Explorer)。 AWS Budgets および請求アラームを構成する。 リソース使用パターンを評価し、EC2 スポットインスタンスに適したワークロードを特定する。 マネージドサービスを使用する機会を特定する (例: Amazon RDS、AWS Fargate、EFS)。 6.2 パフォーマンス最適化戦略を実装する。 パフォーマンスメトリクスに基づいてコンピュートリソースを提案する。 Amazon EBS メトリクスを監視し、パフォーマンス効率を高めるよう構成を修正する。 S3 のパフォーマンス機能を実装する (例: S3 Transfer Acceleration、マルチパートアップロード)。 RDS メトリクスを監視し、パフォーマンス効率を高めるよう構成を修正する (例:Performance Insights、RDS Proxy)。 EC2 の拡張機能を有効化する (例: 拡張ネットワークアダプタ、インスタンスストア、プレイスメントグループ)。 終わりに 少しずつ概要に解説足していきます。 試験ラボが、天国に導いてくれるのか、地獄へ引きずり込んでくるのか… 試験ガイドを見る限りですが、試験範囲はそこまで変わってないのかな~という印象です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js, TypeScript】LINE MessagingAPIで作った天気予報アプリをAWS SAMを使ってデプロイしてみる!

先日、Node.jsとTypeScriptで天気予報アプリを作成しました。 完成形としては以下の通りです。 この記事内では、Glitchでデプロイをしています。 まぁ無料なわけで色々問題があります。 ・プロジェクトは、利用されていないときは5分でスリープ状態になる ・4000件/1hのリクエスト制限がある(Error: 429 too many requests) ということで、AWSのLambdaを使ってデプロイしました。 動作を確認するために所々デバッグして進めていきたいですが、 その度S3にアップロードしてそれをLambdaにもアップロードする必要があります。 こんなのめんどくさいですよね・・ ということで、ローカルでデバッグやテストを可能にしてくれる、かつコマンドのみでデプロイしてくれるSAMを使ってみましょう。 作成後記 LINE MessagingAPIではHTTPSサーバーが必要になります。しかし、SAMではHTTPサーバーしか作れないのでデバッグは不可能でした。それでもコマンドのみでデプロイできるのは便利でした。 アーキテクチャ アーキテクチャの説明の前にまずは、サーバーレスアーキテクチャに関して説明します。 サーバーレスアーキテクチャとは AWSにおけるサーバーレスとは、「インスタンスベースの仮想サーバー(EC2など)を使わずにアプリケーションを開発するアーキテクチャ」を指します。 一般にシステムの運用には、プログラムを動かすためのサーバーが必要です。 そしてそのサーバーは、常に稼働していなければなりません。 しかし開発者がやりたいことは、「サーバーの管理」なのでしょうか? エンドユーザーに価値を届けることこそが使命なわけです。 ということで、こういうめんどくさい作業から解放してくれるのがサーバーレスアーキテクチャなわけです。 サーバーレスアーキテクチャでよく使われるサービスは以下の通りです。 特に、丸で囲っている3つがよく使われます。 それではアーキテクチャに関してみていきましょう。 今回は、Lambda, API Gateway, S3の3つをSAMでデプロイを行い、環境変数をSSM(AWS Systems Manager)で管理していきます。 追記 AWSのEC2を使ってデプロイした記事もあります。 サーバーレスよりもEC2に興味があるぞという方はこちらの記事もどうぞ。 どのようなアプリか 皆さんは、今日の気温を聞いて、「快適に過ごすために今日のファッションをこうしよう」ってパッと思いつきますか? 私は、最高気温、最低気温を聞いてもそれがどのくらい暑いのか、寒いのかがピンと来ず、洋服のチョイスを外したことがしばしばあります。 こんな思いを2度としないために今回このアプリを作りました。 GitHub 完成形のコードは以下となります。 では実際に作成していきましょう! sam initを実行する ゼロから書いていってもいいのですが、初めての方はまずはsam initを使いましょう。 以下のように選択していってください。 ターミナル $ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 What package type would you like to use? 1 - Zip (artifact is a zip uploaded to S3) 2 - Image (artifact is an image uploaded to an ECR image repository) Package type: 1 Which runtime would you like to use? 1 - nodejs14.x 2 - python3.8 3 - ruby2.7 4 - go1.x 5 - java11 6 - dotnetcore3.1 7 - nodejs12.x 8 - nodejs10.x 9 - python3.7 10 - python3.6 11 - python2.7 12 - ruby2.5 13 - java8.al2 14 - java8 15 - dotnetcore2.1 Runtime: 7 Project name [sam-app]: WeatherFashion AWS quick start application templates: 1 - Hello World Example 2 - Step Functions Sample App (Stock Trader) 3 - Quick Start: From Scratch 4 - Quick Start: Scheduled Events 5 - Quick Start: S3 6 - Quick Start: SNS 7 - Quick Start: SQS 8 - Quick Start: App Backend using TypeScript 9 - Quick Start: Web Backend Template selection: 1 ここまでできれば作成されます。 このような構成になっていればOKです。 .WeatherFashion ├── events/ │ ├── event.json ├── hello-world/ │ ├── tests │ │ └── integration │ │ │ └── test-api-gateway.js │ │ └── unit │ │ │ └── test-handler.js │ ├── .npmignore │ ├── app.js │ ├── package.json ├── .gitignore ├── README.md ├── template.yaml 必要ないファイルなどがあるのでそれを削除していきましょう。 .WeatherFashion ├── hello-world/ │ ├── app.js ├── .gitignore ├── README.md ├── template.yaml また、ディレクトリ名やファイル名を変えましょう。 .WeatherFashion ├── api/ │ ├── index.js ├── .gitignore ├── README.md ├── template.yaml 次は、template.yamlを修正して、SAMの実行をしてみたいところですが、一旦後回しにします。 先にTypeScriptなどのパッケージを入れ、ディレクトリ構造を明確にした後の方が理解しやすいので。。 ということでパッケージを入れていきましょう。 package.jsonの作成 以下のコマンドを入力してください。 これで、package.jsonの作成が完了します。 ターミナル $ npm init -y 必要なパッケージのインストール dependencies dependenciesはすべてのステージで使用するパッケージです。 今回使用するパッケージは以下の4つです。 ・@line/bot-sdk ・aws-sdk ・axios 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install @line/bot-sdk aws-sdk axios --save ちなみに、Lambdaでは元よりaws-sdkが使えるようなのでなくても問題ないです。 インストールしなければその分容量が軽くなるので、レスポンスは早くなります。 devDependencies devDependenciesはコーディングステージのみで使用するパッケージです。 今回使用するパッケージは以下の5つです。 ・typescript ・@types/node ・ts-node ・rimraf ・npm-run-all 以下のコマンドを入力してください。 これで全てのパッケージがインストールされます。 ターミナル $ npm install -D typescript @types/node ts-node rimraf npm-run-all package.jsonにコマンドの設定を行う npm run buildでコンパイルを行います。 package.json { "scripts": { "clean": "rimraf dist", "tsc": "tsc", "build": "npm-run-all clean tsc" }, } tsconfig.jsonの作成 以下のコマンドを実行しTypeScriptの初期設定を行います。 ターミナル $ npx tsc --init それでは、作成されたtsconfig.jsonの上書きをしていきます。 tsconfig.json { "compilerOptions": { "target": "ES2018", "module": "commonjs", "sourceMap": true, "outDir": "./api/dist", "strict": true, "moduleResolution": "node", "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["api/src/*"] } 簡単にまとめると、 api/srcディレクトリ以下を対象として、それらをapi/distディレクトリにES2018の書き方でビルドされるという設定です。 tsconfig.jsonに関して詳しく知りたい方は以下のサイトをどうぞ。 また、この辺りで必要ないディレクトリはGithubにpushしたくないので、.gitignoreも作成しておきましょう。 .gitignore node_modules package-lock.json .aws-sam samconfig.toml dist 最終的にはこのようなディレクトリ構成にしましょう。 .WeatherFashion ├── api/ │ ├── dist(コンパイル後) │ │ └── node_modules(コピーする) │ │ └── package.json(コピーする) │ ├── src(コンパイル前) │ │ └── index.ts ├── node_modules(コピー元) ├── .gitignore ├── package.json(コピー元) ├── package-lock.json ├── README.md ├── template.yaml ├── tsconfig.json やるべきことは以下の2つです。 ①distディレクトリを作成する ②distディレクトリに、node_modules, package.jsonをコピーする 次に、template.yamlを書いていきましょう。 SAM Templateを記載する ファイル内にコメントを残しています。 これで大まかには理解できるかと思います。 詳しくは公式サイトを見てください。 template.yaml # AWS CloudFormationテンプレートのバージョン AWSTemplateFormatVersion: '2010-09-09' # CloudFormationではなくSAMを使うと明記する Transform: AWS::Serverless-2016-10-31 # CloudFormationのスタックの説明文(重要ではないので適当でOK) Description: > WeatherFashion Globals: # Lambda関数のタイムアウト値(3秒に設定) Function: Timeout: 3 Resources: # API Gateway WeatherFashionAPI: # Typeを指定する(今回はAPI Gateway) Type: AWS::Serverless::Api Properties: # ステージ名(APIのURLの最後にこのステージ名が付与されます) StageName: v1 # CORSの設定 Cors: AllowMethods: "'POST, GET, OPTIONS'" AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" AllowOrigin: "'*'" MaxAge: "'600'" # Lambda WeatherFashionFunction: # Typeを指定する(今回はLambda) Type: AWS::Serverless::Function Properties: # 関数が格納されているディレクトリ(今回はコンパイル後なので、distディレクトリを使用する) CodeUri: api/dist # ファイル名と関数名(今回はファイル名がindex.js、関数名がexports.handlerなので、index.handlerとなります) Handler: index.handler # どの言語とどのバージョンを使用するか Runtime: nodejs12.x # ポリシーを付与する(今回はLambdaの権限とSSMの読み取り権限を付与) Policies: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess # この関数をトリガーするイベントを指定します Events: # API Gateway WeatherFashionAPI: Type: Api Properties: # どのAPIを使用するか(!Refは値の参照に使用します) RestApiId: !Ref WeatherFashionAPI # URL Path: / # POSTメソッド Method: post Outputs: WeatherFashionAPI: Description: 'API Gateway' # URLを作成(!Subは${}で値を指定することができます) Value: !Sub 'https://${WeatherFashionAPI}.execute-api.${AWS::Region}.amazonaws.com/v1' WeatherFashionFunction: Description: 'Lambda' # ロールの値を返す Value: !GetAtt WeatherFashionFunction.Arn WeatherFashionFunctionIamRole: Description: 'IAM Role' # ロールの値を返す Value: !GetAtt WeatherFashionFunctionRole.Arn LINE Developersにアカウントを作成する LINE Developersにアクセスして、「ログイン」ボタンをクリックしてください。 その後諸々入力してもらったら以下のように作成できるかと思います。 注意事項としては、今回Messaging APIとなるので、チャネルの種類を間違えた方は修正してください。 チャネルシークレットとチャネルアクセストークンが必要になるのでこの2つを発行します。 OpenWeatherのAPIを取得する 以下にアクセスしてください。 アカウントを作成し、APIキーを発行してください。 これで必要な環境変数は取得できました。 それでは、これをSSMを使ってLambda内で使えるようにしていきましょう。 SSMパラメータストアで環境変数を設定 なぜSSMパラメータストアを使うのか? SAMのLambda設定にも、環境変数の項目はあります。 しかし、2点問題点があります。 ①Lambdaの環境変数の変更をしたいとき、Lambdaのバージョンも新規発行をしなければならない ②Lambdaのバージョンとエイリアスを紐付けて管理をするとき、もし環境変数にリリース先環境別の値をセットしていると、リリース時に手動で環境変数の変更をしなければならないケースが発生する 簡単にまとめると、「リアルタイムで反映できないし、人為的なミスのリスクもあるよ」ということです。 SSMパラメータストアで値を管理すると以下の3点のメリットがあります。 ①Lambdaの環境変数の管理が不要 ②Lambdaも含めた値関連情報を一元管理できる ③Lambda外部からリアルタイムに環境変数を変更制御できる ということで、SSMパラメータストアを使用しましょう。 みんな大好きクラスメソッドの記事にやり方が書いてあります。 こちらの記事が完璧なのでこちらを見てやってみてください。 私は以下のように命名して作成しました。 SSMパラメータが取得できているかconsole.logで検証 api/src/index.ts // import import aws from 'aws-sdk'; // SSM const ssm = new aws.SSM(); exports.handler = async (event: any, context: any) => { const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; console.log('channelAccessToken: ' + channelAccessToken); }; これをコンパイルしてデプロイしていきましょう。 ターミナル // コンパイル $ npm run build // ビルド $ sam build // デプロイ $ sam deploy --guided Configuring SAM deploy ====================== Looking for samconfig.toml : Not found Setting default arguments for 'sam deploy' ========================================= // CloudFormation スタック名の指定 Stack Name [sam-app]: WeatherFashion // リージョンの指定 AWS Region [us-east-1]: ap-northeast-1 // デプロイ前にCloudformationの変更セットを確認するか #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: y // SAM CLI に IAM ロールの作成を許可するか(CAPABILITY_IAM) #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: y // API イベントタイプの関数に認証が含まれていない場合、警告される HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y // この設定を samconfig.toml として保存するか Save arguments to samconfig.toml [Y/n]: y これでデプロイが完了します。 では、API GatewayのURLを確認しましょう。 Webhook URLの登録 先ほどAPI Gatewayで作成したhttpsのURLをコピーしてください。 これをLINE DevelopersのWebhookに設定します。 それではSSMパラメータが正しく取得できているか確認しましょう。 CloudWatchで確認しましょう! 取得できていますね! ここからの流れはこのような感じです。 ①「今日の洋服は?」というメッセージを受け取る ②「今日の洋服は?」を受け取ったら、位置情報メッセージを送る ③「今日の洋服は?」以外を受け取ったら、「そのメッセージには対応していません」と送る ④「位置情報メッセージ」を受け取る ⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する ⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る では作っていきましょう! またこれら全てのコードをapi/src/index.tsに書くとコードが肥大化し可読性が落ちます。 なのでCommonディレクトリに関数に切り分けて作成していきます。 またここからはLINEBotのオリジナルの型が頻出します。 1つずつ説明するのはあまりに時間がかかるので、知らない型が出てきたらその度に以下のサイトで検索するようにしてください。 ①「今日の洋服は?」というメッセージを受け取る api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { // ボタンメッセージを送る } else { // エラーメッセージを送る } } catch (err) { console.log(err); } }; ②「今日の洋服は?」を受け取ったら、位置情報メッセージを送る api/src/Common/ButtonMessage/ButtonMessageTemplate.ts // パッケージを読み込む import { TemplateMessage } from '@line/bot-sdk'; export const buttonMessageTemplate = (): Promise<TemplateMessage> => { return new Promise((resolve, reject) => { const params: TemplateMessage = { type: 'template', altText: 'This is a buttons template', template: { type: 'buttons', text: '今日はどんな洋服にしようかな', actions: [ { type: 'uri', label: '現在地を送る', uri: 'https://line.me/R/nv/location/', }, ], }, }; resolve(params); }); }; api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールを読み込む import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { const buttonMessage = await buttonMessageTemplate(); await client.replyMessage(replyToken, buttonMessage); } else { // エラーメッセージを送る } } catch (err) { console.log(err); } }; ボタンメッセージのJSON作成に関しては公式サイトを参考にしましょう。 ③「今日の洋服は?」以外を受け取ったら、「そのメッセージには対応していません」と送る api/src/Common/ButtonMessage/ErrorMessageTemplate.ts // パッケージを読み込む import { TextMessage } from '@line/bot-sdk'; export const errorMessageTemplate = (): Promise<TextMessage> => { return new Promise((resolve, reject) => { const params: TextMessage = { type: 'text', text: 'ごめんなさい、このメッセージは対応していません。', }; resolve(params); }); }; api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールを読み込む import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate'; import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { const buttonMessage = await buttonMessageTemplate(); await client.replyMessage(replyToken, buttonMessage); } else { const errorMessage = await errorMessageTemplate(); await client.replyMessage(replyToken, errorMessage); } } catch (err) { console.log(err); } }; テキストメッセージのJSON作成に関しては公式サイトを参考にしましょう。 ④「位置情報メッセージ」を受け取る api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールを読み込む import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate'; import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); await actionFlexMessage(response, client, weatherApi); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { const buttonMessage = await buttonMessageTemplate(); await client.replyMessage(replyToken, buttonMessage); } else { const errorMessage = await errorMessageTemplate(); await client.replyMessage(replyToken, errorMessage); } } catch (err) { console.log(err); } }; // 天気予報とファッションレコメンドメッセージを送る関数 const actionFlexMessage = async (event: WebhookEvent, client: Client, weatherApi: string) => { try { if (event.type !== 'message' || event.message.type !== 'location') { return; } // ファッションレコメンドメッセージを送る } catch (err) { console.log(err); } }; ⑤「位置情報メッセージ」を受け取ったら、緯度と経度を使って天気予報を取得する Flex Messageの作成方法に関してファイル名も出しながら説明します。 【ファイル名】GetWeatherForecast.ts 天気予報を取得します。 まずはOpenWeatherで天気予報を取得するために必要な情報が3つあります。 ①API ②経度 ③緯度 それではこの3つを取得していきましょう。 ①API APIはSSMパラメータストアで取得しています。 ②経度、③緯度 これら2つは、eventから取得できます。 ということで作っていきましょう。 api/src/Common/WeatherForecastMessage/GetWeatherForecast.ts // Load the package import { WebhookEvent } from '@line/bot-sdk'; import axios, { AxiosResponse } from 'axios'; export const getWeatherForecastData = async ( event: WebhookEvent, weatherApi: string ): Promise<any> => { return new Promise(async (resolve, reject) => { try { if (event.type !== 'message' || event.message.type !== 'location') { return; } // Get latitude and longitude const latitude: number = event.message.latitude; const longitude: number = event.message.longitude; // OpenWeatherURL const openWeatherURL: string = `https://api.openweathermap.org/data/2.5/onecall?lat=${latitude}&lon=${longitude}&units=metric&lang=ja&appid=${weatherApi}`; const weatherData: AxiosResponse<any> = await axios.get(openWeatherURL); resolve(weatherData); } catch (err) { reject(err); } }); }; 【ファイル名】FormatWeatherForecast.ts 取得した天気予報のデータの整形を行う。 こちらでは、const weatherとconst weatherArrayの2つで型定義ファイルを作成する必要があります。 ということで作成しましょう。 api/src/Common/WeatherForecastMessage/types/FormatWeatherForecast.type.ts export type WeatherType = { dt: number; sunrise: number; sunset: number; moonrise: number; moonset: number; moon_phase: number; temp: { day: number; min: number; max: number; night: number; eve: number; morn: number; }; feels_like: { day: number; night: number; eve: number; morn: number; }; pressure: number; humidity: number; dew_point: number; wind_speed: number; wind_deg: number; wind_gust: number; weather: [ { id: number; main: string; description: string; icon: string; } ]; clouds: number; pop: number; rain: number; uvi: number; }; export type WeatherArrayType = { today: string; imageURL: string; weatherForecast: string; mornTemperature: number; dayTemperature: number; eveTemperature: number; nightTemperature: number; fashionAdvice: string; }; 作成した型定義を使ってファイルを完成させます。 api/src/Common/WeatherForecastMessage/FormatWeatherForecast.ts // Load the package import { WebhookEvent } from '@line/bot-sdk'; import { AxiosResponse } from 'axios'; // Load the module import { getWeatherForecastData } from './GetWeatherForecast'; // types import { WeatherType, WeatherArrayType } from './types/FormatWeatherForecast.type'; export const formatWeatherForecastData = async ( event: WebhookEvent, weatherApi: string ): Promise<WeatherArrayType> => { return new Promise(async (resolve, reject) => { // Get the getWeatherForecastData const weathers: AxiosResponse<any> = await getWeatherForecastData(event, weatherApi); // Util const weather: WeatherType = weathers.data.daily[0]; // Five required data // 1) Today's date const UNIXToday: number = weather.dt; const convertUNIXToday: Date = new Date(UNIXToday * 1000); const today: string = convertUNIXToday.toLocaleDateString('ja-JP'); // 2) Weather forecast const weatherForecast: string = weather.weather[0].description; // 3) Temperature (morning, daytime, evening, night) const mornTemperature: number = weather.feels_like.morn; const dayTemperature: number = weather.feels_like.day; const eveTemperature: number = weather.feels_like.eve; const nightTemperature: number = weather.feels_like.night; // Bifurcate your clothing by maximum temperature const maximumTemperature: number = Math.max( mornTemperature, dayTemperature, eveTemperature, nightTemperature ); // 4) Fashion Advice let fashionAdvice: string = ''; // 5) Fashion Image let imageURL: string = ''; if (maximumTemperature >= 26) { fashionAdvice = '暑い!半袖が活躍する時期です。少し歩くだけで汗ばむ気温なので半袖1枚で大丈夫です。ハットや日焼け止めなどの対策もしましょう'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/60aa3c44153071e6df530eb7_71.png'; } else if (maximumTemperature >= 21) { fashionAdvice = '半袖と長袖の分かれ目の気温です。日差しのある日は半袖を、曇りや雨で日差しがない日は長袖がおすすめです。この気温では、半袖の上にライトアウターなどを着ていつでも脱げるようにしておくといいですね!'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e58a5923ad81f73ac747_10.png'; } else if (maximumTemperature >= 16) { fashionAdvice = 'レイヤードスタイルが楽しめる気温です。ちょっと肌寒いかな?というくらいの過ごしやすい時期なので目一杯ファッションを楽しみましょう!日中と朝晩で気温差が激しいので羽織ものを持つことを前提としたコーディネートがおすすめです。'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6087da411a3ce013f3ddcd42_66.png'; } else if (maximumTemperature >= 12) { fashionAdvice = 'じわじわと寒さを感じる気温です。ライトアウターやニットやパーカーなどが活躍します。この時期は急に暑さをぶり返すことも多いのでこのLINEで毎日天気を確認してくださいね!'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e498e7d26507413fd853_4.png'; } else if (maximumTemperature >= 7) { fashionAdvice = 'そろそろ冬本番です。冬服の上にアウターを羽織ってちょうどいいくらいです。ただし室内は暖房が効いていることが多いので脱ぎ着しやすいコーディネートがおすすめです!'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056e4de7156326ff560b1a1_6.png'; } else { fashionAdvice = '凍えるほどの寒さです。しっかり厚着して、マフラーや手袋、ニット帽などの冬小物もうまく使って防寒対策をしましょう!'; imageURL = 'https://uploads-ssl.webflow.com/603c87adb15be3cb0b3ed9b5/6056ebd3ea0ff76dfc900633_48.png'; } // Make an array of the above required items. const weatherArray: WeatherArrayType = { today, imageURL, weatherForecast, mornTemperature, dayTemperature, eveTemperature, nightTemperature, fashionAdvice, }; resolve(weatherArray); }); }; 【ファイル名】FlexMessageTemplate 整形したデータを取得して Flex Messageのテンプレートを作成する。 api/src/Common/WeatherForecastMessage/FlexMessageTemplate.ts // Load the package import { WebhookEvent, FlexMessage } from '@line/bot-sdk'; // Load the module import { formatWeatherForecastData } from './FormatWeatherForecast'; export const flexMessageTemplate = async ( event: WebhookEvent, weatherApi: string ): Promise<FlexMessage> => { return new Promise(async (resolve, reject) => { const data = await formatWeatherForecastData(event, weatherApi); resolve({ type: 'flex', altText: '天気予報です', contents: { type: 'bubble', header: { type: 'box', layout: 'vertical', contents: [ { type: 'text', text: data.today, color: '#FFFFFF', align: 'center', weight: 'bold', }, ], }, hero: { type: 'image', url: data.imageURL, size: 'full', }, body: { type: 'box', layout: 'vertical', contents: [ { type: 'text', text: `天気は、「${data.weatherForecast}」です`, weight: 'bold', align: 'center', }, { type: 'text', text: '■体感気温', margin: 'lg', }, { type: 'text', text: `朝:${data.mornTemperature}℃`, margin: 'sm', size: 'sm', color: '#C8BD16', }, { type: 'text', text: `日中:${data.dayTemperature}℃`, margin: 'sm', size: 'sm', color: '#789BC0', }, { type: 'text', text: `夕方:${data.eveTemperature}℃`, margin: 'sm', size: 'sm', color: '#091C43', }, { type: 'text', text: `夜:${data.nightTemperature}℃`, margin: 'sm', size: 'sm', color: '#004032', }, { type: 'separator', margin: 'xl', }, { type: 'text', text: '■洋服アドバイス', margin: 'xl', }, { type: 'text', text: data.fashionAdvice, margin: 'sm', wrap: true, size: 'xs', }, ], }, styles: { header: { backgroundColor: '#00B900', }, hero: { separator: false, }, }, }, }); }); }; ⑥「位置情報メッセージ」を受け取ったら、天気予報メッセージを送る api/src/index.ts // パッケージのインストール import { ClientConfig, Client, WebhookEvent } from '@line/bot-sdk'; import aws from 'aws-sdk'; // モジュールを読み込む import { buttonMessageTemplate } from './Common/ButtonMessage/ButtonMessageTemplate'; import { errorMessageTemplate } from './Common/ButtonMessage/ErrorMessageTemplate'; import { flexMessageTemplate } from './Common/WeatherForecastMessage/FlexMessageTemplate'; // SSMパラメータストア const ssm = new aws.SSM(); const LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN = { Name: 'LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN', WithDecryption: false, }; const LINE_WEATHER_FASHION_CHANNEL_SECRET = { Name: 'LINE_WEATHER_FASHION_CHANNEL_SECRET', WithDecryption: false, }; const LINE_WEATHER_FASHION_WEATHER_API = { Name: 'LINE_WEATHER_FASHION_WEATHER_API', WithDecryption: false, }; exports.handler = async (event: any, context: any) => { // SSMパラメータストアで値を取得する const CHANNEL_ACCESS_TOKEN: any = await ssm .getParameter(LINE_WEATHER_FASHION_CHANNEL_ACCESS_TOKEN) .promise(); const CHANNEL_SECRET: any = await ssm.getParameter(LINE_WEATHER_FASHION_CHANNEL_SECRET).promise(); const WEATHER_API: any = await ssm.getParameter(LINE_WEATHER_FASHION_WEATHER_API).promise(); const channelAccessToken: string = CHANNEL_ACCESS_TOKEN.Parameter.Value; const channelSecret: string = CHANNEL_SECRET.Parameter.Value; const weatherApi: string = WEATHER_API.Parameter.Value; // SSMパラメータストアを使ってclientを作成 const clientConfig: ClientConfig = { channelAccessToken: channelAccessToken, channelSecret: channelSecret, }; const client: Client = new Client(clientConfig); // post const body: any = JSON.parse(event.body); const response: WebhookEvent = body.events[0]; // action try { await actionButtonOrErrorMessage(response, client); await actionFlexMessage(response, client, weatherApi); } catch (err) { console.log(err); } }; // ボタンメッセージもしくはエラーメッセージを送る関数 const actionButtonOrErrorMessage = async (event: WebhookEvent, client: Client) => { try { if (event.type !== 'message' || event.message.type !== 'text') { return; } const { replyToken } = event; const { text } = event.message; if (text === '今日の洋服は?') { const buttonMessage = await buttonMessageTemplate(); await client.replyMessage(replyToken, buttonMessage); } else { const errorMessage = await errorMessageTemplate(); await client.replyMessage(replyToken, errorMessage); } } catch (err) { console.log(err); } }; // 天気予報とファッションレコメンドメッセージを送る関数 const actionFlexMessage = async (event: WebhookEvent, client: Client, weatherApi: string) => { try { if (event.type !== 'message' || event.message.type !== 'location') { return; } const { replyToken } = event; const message = await flexMessageTemplate(event, weatherApi); await client.replyMessage(replyToken, message); } catch (err) { console.log(err); } }; これで完成です! では、デプロイしていきましょう。 デプロイ まずは、npm run buildでコンパイルしましょう。 ターミナル $ npm run build コンパイルされた後は、ビルドしてデプロイしていきましょう。 ターミナル // ビルド $ sam build // デプロイ $ sam deploy --guided 最後に 以前すべて手作業で行いましたが、SAMを使うと効率的にデプロイが行えます。 SAMテンプレートの書き方を学ぶコストは発生しますが、1度作ればそれをそのまま使えるので汎用性も高いのでおすすめです。 サーバーレスアーキテクチャを勉強する方がいましたらぜひSAMも勉強してみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DenoでDynamoDbを操作する

少しハマったので備忘録として。 参考文献 ソース index.ts import { createClient } from "https://denopkg.com/chiefbiiko/dynamodb/mod.ts"; const dyno = createClient({ region: 'ap-northeast-1', credentials: { accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey+H' }, }); const params = { TableName: 'test', Key: { id: 1 }, }; const data = await dyno.getItem(params) console.log('Success', data.Item); 実行コマンド $ deno run --allow-env --allow-net index.ts Check file:///Users/yamashita-kazumasa/git/lambda/deno_lambda/test.ts Success { id: 1, name: "Name" }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む