Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all 8922 articles
Browse latest View live

Andoroid端末上でNodo.jsアプリを実行してみた

$
0
0

概要

コロナの関係で妻と二人在宅勤務を実施中。2階で仕事をしている妻が送ったメッセージ(コーヒーが飲みたい、など)を1階リビングのgoogle homeに喋らせるシステムをNode.js、Node-REDで作成した。自宅に‎Raspberry Piが無いためPC上でシステムを動作させていたが、PCの変わりにスマホ上で動作させられないかと考え、本対応を実施。

環境

  • 端末
    • Android 9
  • Androidにインストールしたアプリ
    • Termux:Android上でLinuxターミナルを実行するためのアプリ。root化不要。無料アプリ。
    • WiFi Keyboard:PCのキーボードでスマホの入力が可能。
    • Termux:Boot:Nodeアプリ、Node-REDの自動起動に利用。有料アプリ。
  • Termux上でインストールしたアプリ
    • Node V14.0.0
    • npm 6.14.4
    • Node-RED

構成

Termuxにnode.js、npm、Express、Node-REDをインストール。

Node-REDのnode-red-contrib-castノードを利用しPOSTしたメッセージをgoogle homeに喋らせるWEB APIを作成。

Expressでメッセージ入力用の簡単なページを作成し、入力されたメッセージをWEB API経由でgoogle homeに送信する。

実施内容

必要なアプリのインストール

  1. Termux
    Android上でNode.jsを動作させるため、初めはDory - node.jsを試したが、Node.jsのバージョンがv6と古く、アップデートする方法がわからなかったため断念。v6だとNode-REDがインストールできない。Termuxは最新のNode.jsがインストールでき、Node-REDも問題なくインストールできた。ちなみにNode-REDの公式ページでもTermuxを利用することが推奨されている。
  2. Node、npm、Express
    Termuxのターミナルから通常の手順でインストール可能。WiFi Keyboardを利用するとコマンド入力が楽になる。
  3. Node-RED
    公式ページ掲載の手順でインストール。フローの編集はPCのブラウザからhttp://スマホのIPアドレス:1880にアクセスして実施
  4. Termux:Boot
    Google Playからインストール(有料)。インストール後、アプリの説明欄に従い、ホームディレクトリは以下に~/.termux/boot/の様にディレクトリを作成。このディレクトリに後ほど自動起動用のスクリプトを作成する。

Node-REDでgoogle homeへのメッセージ送信APIを作成

基本的な手順は以下のページを参考に実施。

https://qiita.com/dozensofdars/items/6cec7db44636374b5382

今回Node.jsアプリからPOSTできるようにするため、node-red-contrib-castノードの前後をhttp inノードとhttp responseノードで挟めばOK。http responseノードを設定しないと、APIを呼び出したままアプリがだんまりになってしまうので注意。アプリからはmessageパラメタにgoogle homeに喋らせたい文言をセットしてPOSTするよう実装する。

メッセージ入力用ページの作成

Expressでメッセージ入力ページ、メッセージ送信結果表示ページを作成。詳しい説明は割愛。

Termux:Bootの設定

NodeのアプリとNode-REDの自動起動のためTermux:Bootの設定を実施。~/.termux/boot/ディレクトリ配下に任意の名前でファイルを作成し、以下の様にコマンドを記載。これでスマポ起動時に自動的にNode-REDとNodeアプリが実行される。

node-red    
node (Nodeアプリ配置フォルダ)/bin/www  

以上


Node(npm)でTypescript導入メモ

$
0
0

ざくっとインストール

npm install typescript

tsconfig.json作成

tsc --init

以上。

Node.js + FirebaseでStatelessなAPIを実装する

$
0
0

留意事項

メモ書き程度なので詳細な説明は割愛します。

使う技術やサービス

  • Firebase Authentication
  • Node.js(FWはExpress)

Firebase側の準備

プロジェクト作成し、Authentication -> Sign-in MethodでGoogleを有効にしておきます。
Firebase側の準備はこれで終わりです。

実装

firebase-toolsをグローバルではなくローカルにインストールして進めていきます(ちょっと訳あり)。

準備

適当なディレクトリを作って

$ npm init -y$ npm i firebase-tools
$ npx firebase login

ログインが終わったら

$ npx firebase init

以下、ログ

?  Are you ready to proceed? (y/N)  y

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices.
 () Database: Deploy Firebase Realtime Database Rules
 () Firestore: Deploy rules and create indexes for Firestore
 (*) Functions: Configure and deploy Cloud Functions
 (*) Hosting: Configure and deploy Firebase Hosting sites
 () Storage: Deploy Cloud Storage security rules
 () Emulators: Set up local emulators for Firebase features

? Please select an option:
> Use an existing project
  Create a new project
  Add Firebase to an existing Google Cloud Platform project 
  Don't set up a default project

? Select a default Firebase project for this directory: (Use arrow keys)
> hogehoge-xxxxx (hogehoge)

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? (Use arrow keys)
> JavaScript
  TypeScript

? Do you want to use ESLint to catch probable bugs and enforce style? (y/N) n
+  Wrote functions/package.json
+  Wrote functions/index.js
+  Wrote functions/.gitignore

? Do you want to install dependencies with npm now? (Y/n) y

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

+  Firebase initialization complete!

バックエンド

functionsディレクトリ内で作業する。

$ npm i express cookie-parser

index.js

constadmin=require("firebase-admin");constcookieParser=require("cookie-parser")();constexpress=require("express");constfunctions=require("firebase-functions");constapi=express();admin.initializeApp();constvalidateFirebaseIdToken=async(req,res,next)=>{if((!req.headers.authorization||!req.headers.authorization.startsWith("Bearer "))&&!(req.cookies&&req.cookies.__session)){res.status(403).send("Unauthorized");return;}letidToken;if(req.headers.authorization&&req.headers.authorization.startsWith("Bearer ")){console.log('Found "Authorization" header');idToken=req.headers.authorization.split("Bearer ")[1];}elseif(req.cookies){console.log('Found "__session" cookie');idToken=req.cookies.__session;}else{res.status(403).send("Unauthorized");return;}try{constdecodedIdToken=awaitadmin.auth().verifyIdToken(idToken);console.log("ID Token correctly decoded",decodedIdToken);req.user=decodedIdToken;next();return;}catch(err){console.error(err);res.status(403).send("Unauthorized");return;}};api.use(cookieParser);api.use(validateFirebaseIdToken);api.get("/",(req,res)=>{res.status(200).send(JSON.stringify(req.user));});exports.api=functions.https.onRequest(api);

フロントエンド

publicディレクトリ内で作業する。

index.html

<!DOCTYPE html><htmllang="en"><head><title>Sample</title></head><body><buttonid="demo-sign-in-button"style="display: none;">
      Sign in with Google
    </button><buttonid="demo-sign-out-button"style="display: none;">
      Sign out
    </button><script src="/__/firebase/7.14.2/firebase-app.js"></script><script src="/__/firebase/7.14.2/firebase-auth.js"></script><script src="/__/firebase/init.js"></script><script src="index.js"></script></body></html>

index.js

document.addEventListener("DOMContentLoaded",function(){signInButton=document.getElementById("demo-sign-in-button");signOutButton=document.getElementById("demo-sign-out-button");signInButton.addEventListener("click",signIn);signOutButton.addEventListener("click",signOut);firebase.auth().onAuthStateChanged(onAuthStateChanged);});constonAuthStateChanged=(user)=>{if(user){console.info(user);signInButton.style.display="none";signOutButton.style.display="block";startFunctionsCookieRequest();}else{console.info("!user");signInButton.style.display="block";signOutButton.style.display="none";}};constsignIn=()=>{firebase.auth().signInWithPopup(newfirebase.auth.GoogleAuthProvider());};constsignOut=()=>{firebase.auth().signOut();document.cookie="__session=";};conststartFunctionsCookieRequest=()=>{firebase.auth().currentUser.getIdToken(true).then(function(token){document.cookie="__session="+token+";max-age=86400";});};

プロダクト開発ではAuthorization: Bearer <token>を使うようにしよう(手抜き)。

動作確認

プロジェクトルートディレクトリで作業する。

$ npx firebase emulators:start

Hostingは5000番ポート、Functionsは5001番ポートで起動しますのでそれぞれ確認。

LINEBotからRaspberryPiで写真を撮ってLINEにおくる!

$
0
0

はじめに

LINEBotでRaspberryPiで写真を撮って送るという、カメラ装置です。
ハッカソンで作成したねこしぇるじゅ-自慢の猫をとにかく拡散!- | ProtoPediaの一部をupdateしたものです。

概要

LINEBotからRaspberryPiを動かして写真を撮り、Gyazoに送って写真をLINEに送るものです。
LINEBotでRaspberryPiを動かしています。
RaspberryPi node.js ngrok Gyazo API という感じです。

できたもの

使い方

RaspberryPiを猫がよくいる場所に置きます。
猫が見たいので LINEBotに「ねこ」と入れます。
(「ねこ」以外のワードには「何が見たいの?」と返してきます。)
写真を撮るのに時間がかかるので、「写真撮ってくるから待っててねー」、とつなぎのワードが入ります。
10秒ほどすると写真が送られてきます。

環境

MacBook Pro macOS Mojave
 Visual Studio Code 1.44.0

RaspberryPi 3B
 Release: 10
 Codename: buster
 Node.js v12.16.3
 npm v6.14.4
Pi camera
洗濯バサミ

Raspberry Piの準備

こちらを参考にnode.jsとnpmの最新版を入れましょう。
Raspberry PiにNode.jsとnpmの最新版をインストールする - Qiita
そのほかで必要なnpmはこちら

//Gyazo API
$ npm i gyazo-api 
// LINEbot SDK
$ npm i @line/bot-sdk express
//ngrok
$ wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip 
//ngrokのzipを解凍する
$ unzip ngrok-stable-linux-arm.zip 

コード

node.js
'use strict';constexpress=require('express');constline=require('@line/bot-sdk');constPORT=process.env.PORT||3000;constGyazo=require('gyazo-api');constgyazoclient=newGyazo('Your access token');constconfig={channelSecret:'***',channelAccessToken:'***'};constapp=express();app.get('/',(req,res)=>res.send('Hello LINE BOT!(GET)'));//ブラウザ確認用app.post('/webhook',line.middleware(config),(req,res)=>{console.log(req.body.events);Promise.all(req.body.events.map(handleEvent)).then((result)=>res.json(result));});constclient=newline.Client(config);asyncfunctionhandleEvent(event){if(event.type!=='message'||event.message.type!=='text'){returnPromise.resolve(null);}letmes=event.message.text;if(event.message.text.match("ねこ")){awaitclient.replyMessage(event.replyToken,{type:"text",text:'写真を撮ってくるからちょっと待っててね'});awaitnyanpi();returnnyancoPic(event.source.userId);}else{returnclient.replyMessage(event.replyToken,{type:"text",text:'何が見たいのかな?'});}}//picameraで写真を撮って送るconstnyanpi=async(userId)=>{constPiCamera=require('pi-camera');constmyCamera=newPiCamera({mode:'photo',output:`${__dirname}/nyan.jpg`,width:640,height:480,nopreview:true,});awaitmyCamera.snap().then((result)=>{console.log("ok");gyazoclient.upload('./nyan.jpg',{title:"my picture",desc:"upload from nodejs"}).then((res)=>{console.log("送った");}).catch((err)=>{console.error(err);console.log("送レナカッタ");});}).catch((error)=>{// Handle your error});}//Gyazoの最新の写真のURLをとってくるconstnyancoPic=async(userId)=>{letgyazoimgUrl='';awaitgyazoclient.list().then(function(response){console.log(response.data[0]);gyazoimgUrl=`${response.data[0].url}`;console.log(gyazoimgUrl);}).catch(function(err){console.error(err);});awaitclient.pushMessage(userId,{type:'image',originalContentUrl:gyazoimgUrl,previewImageUrl:gyazoimgUrl});}// const finpic = async () => {//   await nyanpi();//   await nyancoPic(event.source.userId);// }app.listen(PORT);console.log(`Server running at ${PORT}`);

動いていないところ

最新じゃなくて1個前の写真しか持ってこれない。
await/asyncがうまく使えていない感じ。。。
OKと送ったというconsole.logの間でGyazoに取りに行っちゃう
(ログ↓)

ok
{
  image_id: '***',
  permalink_url: 'https://api.gyazo.com/***',
  url: 'https://i.gyazo.com/f***',
  metadata: {
    app: null,
    title: 'my picture',
    url: null,
    desc: 'upload from nodejs'
  },
  type: 'jpg',
  thumb_url: 'https://thumb.gyazo.com/thumb/200/***ApieIALnjo-jpg.jpg',
  created_at: '2020-05-07T01:06:27+0000'
}
https://i.gyazo.com/f6ab3a37fce7260c19e8f0f49a934a8a.jpg
送った

参考サイト

20200504 raspberrypi handson_honpen
Node.jsでラズパイのカメラモジュールを使う – plog
gyazo-api - npm
raspbian(Raspberry Pi)のバージョン確認方法

感想

Piカメラ音がしないのでお猫様をこっそり撮影できて良い〜
スマホのシャッター音に慣れすぎか。

promise難しい。。。

【Node.js】request promise 使い方

$
0
0

自分が使っていて分からなかったことを備忘録として書き残しておきます。

使い方

request-promise自体は以下のgithubで公開されている。
https://github.com/request/request-promise


インストール

npm経由でinstall

npm install --save request
npm install --save request-promise

javascriptで使うには、requireでrequest promiseを読みこむ。

constrp=require('request-promise')

オプションを作る

  • url

GETしたい挿し先のURLを記入する。queryを入れた状態でもいい。
例 ) https://sample.jp/sample/?query1=AAA

  • method

GET

constoption={url:'https://test.jp/test/',method:'GET',qs:{query1:'AAA',query2:'BBB',}}

POST (HTML Form)

constoption={url:'https://test.jp/test/',method:'POST',form:{query1:'AAA',query2:'BBB'}}

各クエリがなければ省いてもらってかまいません。

  • headers
constoption={...headers:{// curlの-Hに該当します。'content-type':'appilication/json'}}

optionのparamは名前が違うとダメなのでheadersをheaderにしたりしてないかを確認してください。

request

先ほど作ったオプションを元にHTTP通信を行います。

rp(option).then((res)=>{returnJSON.parse(res)}).catch((err)=>{console.log(err)returnnull});

メソッドチェーンで値を取得してからの動きを続けて記述していくことができます。
スクレイピングできたり、APIから〜みたいな派生が無限に生まれるので、今後も使っていきたいですね。

最終形態

sample.js
"use strict"constrp=require('request-promise')/**
 * @constructor
 * @param {Json} query query: data
 * @return {Json} query 結果
 */constrPModule=function(query){this.option={url:'',// 取得先method:'GET',qs=query}}rpModule.prototype.getData=function(){rp(this.option).then((res)=>{returnJSON.parse(res)}.catch((err)=>{console.log(err)}}module.exports=rpModule

こんな感じにするとどこからでも呼び出せるAPIアクセスモジュールみたいな利用ができると思います。

読み込み側

index.js
constrpModule=require('./sample.js')// 読み込みたいパスを指定する...// queryの作成...constrpM=newrpModule(query)letdata=rpM.getData()console.log(data)

読み込み側はこんな感じでしょうか。

以上で、request promiseの備忘録を終了します。

[Node][npm] sudoなしでMacOS/Linux に npm packeage をインストール

$
0
0

Install npm packages globally without sudo on macOS and Linux

npm installs packages locally within your projects by default. You can also install packages globally (e.g. npm install -g <package>) (useful for command-line apps). However the downside of this is that you need to be root (or use sudo) to be able to install globally.

Here is a way to install packages globally for a given user.

1. Create a directory for global packages
mkdir"${HOME}/.npm-packages"
2. Tell npm where to store globally installed packages
npm config set prefix "${HOME}/.npm-packages"
3. Ensure npm will find installed binaries and man pages

Add the following to your .bashrc/.zshrc:

NPM_PACKAGES="${HOME}/.npm-packages"export PATH="$PATH:$NPM_PACKAGES/bin"# Preserve MANPATH if you already defined it somewhere in your config.# Otherwise, fall back to `manpath` so we can inherit from `/etc/manpath`.export MANPATH="${MANPATH-$(manpath)}:$NPM_PACKAGES/share/man"

If you're using fish, add the following to ~/.config/fish/config.fish:

set NPM_PACKAGES "$HOME/.npm-packages"set PATH $NPM_PACKAGES/bin $PATHset MANPATH $NPM_PACKAGES/share/man $MANPATH

If you have erased your MANPATH by mistake, you can restore it by running set -Ux MANPATH (manpath -g) $MANPATH once. Do not put this command in your config.fish.


Check out npm-g_nosudo for doing the above steps automagically


NOTE: If you are running macOS, the .bashrc file may not yet exist, and the terminal will be obtaining its environment parameters from another file, such as .profile or .bash_profile. These files also reside in the user's home folder. In this case, simply adding the following line to them will instruct Terminal to also load the .bashrc file:

source ~/.bashrc

See also: npm's documentation on
"Fixing npm permissions".

原文

https://github.com/sindresorhus/guides/blob/master/npm-global-without-sudo.md

React+Expressで本番環境へデプロイ

$
0
0

はじめに

React+ExpressでWEBサービスを作るにあたって、
本番環境へのデプロイ方法がわからなかったので調べてみました。

対象読者

Reactを導入してみたいけど、Expressでデプロイする場合どうすりゃいいんだ、と悩んでいる方。

方法

ずばり、
ReactプロジェクトでビルドしたファイルをExpressプロジェクトのpublicにぶち込む、です。

手順
①Reactプロジェクトで本番用ファイル群を生成
②生成された/build以下のファイルをExpressプロジェクトの/public以下にデプロイ
③Expressのルーター操作をrenderによる描画からsendfileに変更(index.htmlをクライアントに返すだけにする)

詳細は以下で説明します。

①Reactプロジェクトで本番用ファイル群を生成

まずはプロジェクト作成

create-react-app client

作成したプロジェクトに移動してビルド

cd client
npm run build

ビルドするとbuild以下に本番用ファイル群が生成される。
image.png

②生成された/build以下のファイルをExpressの/public以下にデプロイ

①で生成したファイル群をExpressの公開フォルダ(public)に配置します。

まずはExpressのプロジェクト作成。
(Expressの導入などはhttps://qiita.com/etet-etet/items/1c65b934dbe7fc33490b)

express server

プロジェクトが作成されると以下のファイルが生成される。
image.png

これのpubilc配下に①で生成したファイルをぶち込む。(元々あったimages, javascripts, stylesheetsフォルダは不要なので削除してよい)

image.png

③Expressのルーター操作をrenderによる描画からsendfileに変更

Expressのルーターは、アクセスされたアドレスに基づいて予め定めた動作をします。
例えば、デフォルトだとルート(/)にアクセスされたら
viewsのindex.jadeでhtmlをレンダリングし、クライアントに返します。

routes/index.js(オリジナル)
varexpress=require('express');varrouter=express.Router();/* GET home page. */router.get('/',function(req,res,next){res.render('index',{title:'Express'});});module.exports=router;

これを、
レンダリングは行わずにpubilcフォルダにあるindex.htmlを返すようにしてあげます。

routes/index.js(改)
varexpress=require('express');varrouter=express.Router();/* GET home page. */router.get('/',function(req,res,next){// res.render('index', { title: 'Express' }); //renderによる描画は不要res.sendfile('./public/index.html');//クライアントにindex.htmlを返す});module.exports=router;

以上

参考
https://stackoverflow.com/questions/17911228/how-do-i-use-html-as-the-view-engine-in-express

別の方法(同じプロジェクト内でReactとExpressを連携する方法)

今回ご紹介した方法は、React側でビルドしたファイルをExpressにデプロイする方法でしたが、
ReactとExpressを同じプロジェクト内で連携する方法もあるようです。
Node.js, Express, sequelize, React で始めるモダン WEB アプリケーション入門(Express/sequelize編)

また、上記の記事で言及されてますが、express-react-viewsを使ってExpressのViewエンジンをReact(jsx)にする方法もあるようです。ただし、
クライアントサイドでの React レンダリングやイベント処理が出来ない仕様とのこと。

views ディレクトリ配下に React コンポーネントを使った View を用意できるようになるが、SSR(Server Side Rendering) により static な HTML が出力される。そのため、クライアントサイドでレンダリングしたい場合には利用できない。express-react-view の公式ドキュメントにも下記のとおり、その旨が記述されている。

This is an Express view engine which renders React components on server. It renders static markup and does not support mounting those views on the client.

本番環境はいいけど開発環境はどうすべき?

基本的にはそれぞれ別のプロジェクトで開発し、デプロイするときだけ上記の手順でファイルをExpress側に持ってきてあげるのが良さそうです。

クライアントからAPIを呼ぶ必要がある場合の開発環境については、下記のページが参考になります。
React開発時には、APIサーバーとReactアプリサーバーを別にして、プロキシを使うというベスト・プラクティス

さいごに

今回はReactとExpressを別々のプロジェクトで用意し、
React側でビルドしたファイルをExpressにデプロイする方法をご紹介しました。

一方で、ReactとExpressを同じプロジェクトで管理する方法もあるようですが、
個人的な意見ですが、品質管理の観点から別々のプロジェクトにした方がいいと考えてます。

同じプロジェクトで管理すると、フロントエンドとバックエンドの動きの見通しがよくなるのがメリットですが、
フロントエンドとバックエンドの結合度が高くなり、品質の観点で継続的にメンテするには幾分やっかいな気がします。

1~3人とかの少人数開発ではいいですが、
人数が増えてくると全員がフロントエンドとバックエンドの知識を備えてなければならなく、
言ってしまえば知らなくていい情報やファイルがオープンになってしまってるとも言えます。

高凝集・疎結合はエンジニアリングの基本なので、
なるべく結合度を弱めたプロジェクトの単位がいいのかなーと思ってます。

アドバイスございましたら是非お願いいたします。

人のTwitterアカウントを見つめるやつ

$
0
0

経緯

昔の知人X氏が何かの弾みで道を踏み外し、別の知人A氏やB氏に対して脅迫行為(結局本物の警察沙汰になりました)を始めたので、特定のTwitterアカウントの言動を監視するBOTを作りました。
技術的に特に難しいものではないのですが、せっかくなので公開します。

https://github.com/netebakari/twitter-serverless-watchdog

やること

監視BOTのようなものは動かしていることを忘れてしまうぐらいがちょうどいいのでサーバーレスで実装します。

設定はDynamoDBに登録しておきます。
AWS Lambdaが定期的に対象アカウントのツイートを取得して、指定されたキーワードを検知したら次のようなツイートでお知らせします。

1000000000000000000以降のチェックを行い、N件の監視対象ツイートが見つかりました。 @null宛で引用RTします。
現在の監視アカウント: @kiken_na_hito, @abunai_hito, @kowai_hito
現在の監視キーワード: https://YOUR-BUCKET-NAME.s3-ap-northeast-1.amazonaws.com/path/to/object/keywords.txt

注意点

トークンは用意していないので https://developer.twitter.comで登録して自前で準備する必要があります。またOAuth認証は自分で行ってください。

なぜなに

(Q) なぜ@null宛へのメンション+引用RTで通知するのか?
(A) いきなりこわいツイートがTLに出てきたらびっくりするので
(Q) 監視キーワードがなんでS3へのリンクになっているの?
(A) キーワードが増えると140文字を軽くオーバーしてしまうから
(Q) いったいX氏に何が?
(A) 知りません……


Node.js+mongoDBのDockerアプリをCircleCI+AWS Elastic BeanstalkでCI/CDできるようにする

$
0
0

この記事について

「ローカル環境を汚さずにいろんなツールを入れて試して勉強・アプリを開発してみたい!」「本番環境にアプリをのせるときに、サーバーに直接接続して作業するのではなくて、イケてるデプロイフローを構築して使いたい!」と思う"目指せ初心者脱却マン"は多いと思います。

今回は、開発環境のコンテナ化+AWS Elastic BeanstalkとCircle CIを使った自動デプロイフローを構築するまでの一連の流れを紹介したいと思います。いろんな記事・本を行ったり来たりしないでこの記事だけで完結させることを目標にします。

使用する環境・バージョン

開発環境

  • OS : macOS Mojave 10.14.5
  • git : 2.20.1 (Apple Git-117)
  • EB CLI 3.15.3 (Python 3.7.4)
  • docker : 19.03.8, build afacb8b
  • Docker.app : 2.2.0.5
  • VS Code : 1.44.2
  • VS Code Remote Development : 0.20.0

アプリ

  • Node.js : v12.13.0
  • npm : 6.13.4
  • express : 4.17.1
  • mongoose : 5.9.11

前提条件

  • 以下のことがすでに行われている・インストール済みであることとします。
    • VS Codeと拡張機能 Remote Development
    • Docker
    • AWSのアカウント
    • EB CLI
    • Githubのアカウント
    • Circle CIへのGithub連携サインアップ
  • ローカルアプリのソースコードは一応載せますが、そのコードについて事細かに解説するのは記事の主旨から外れるので今回はなしです。

読者に要求する前提知識

  • 基本的なターミナルコマンドが扱えること
  • gitが問題なく扱えること
  • コンテナ(Docker)という概念と関連用語・コマンドについて知っていること
    • イメージ・ボリューム・ネットワークやコマンドについては今回深く解説しません。
  • TCP/IP等の基本的なネットワークの知識
    • 「何番ポートを開ける」という言葉がわかれば最低限OKです

作業工程

0. アーキテクチャ・ディレクトリ構造

目指すローカル+本番環境は以下の通りです。
architecture.png
また、ローカル環境での最終的なディレクトリ構造は以下のようになります。

/app #アプリのルートディレクトリ
  ├─.circieci # CircleCIの設定ファイルを格納
  │   └─config.yml
  ├─.devcontainer # VSCodeRemoteContainerの設定ファイルを格納
  │   ├─devcontainer.json
  │   └─docker-compose.yml
  ├─.elasticbeanstalk # ElasticBeanstalkの設定ファイルを格納
  │   └─config.yml
  ├─.ebextensions # ElasticBeanstalkのオプション設定ファイルを格納
  │   └─env.config
  ├─.gitignore
  ├─Dockerfile
  ├─docker-compose.yml
  ├─.dockerignore
  ├─node_modules
  │  └─(略)
  ├─package.json
  ├─package-lock.json
  └─src # アプリのソースコード
     ├─controllers
     │  └─personsController.js
     ├─models
     │  └─person.js
     └─main.js

1. ローカルアプリのソースコードを作成

アプリの初期化

アプリのルートディレクトリで以下のコマンドを実行します。

$ npm init -y$ npm i express -S$ npm i mongoose -S

そうしたら、作成されたpackage.jsonを以下のように書き換えます。

package.json
{(略)"main":"src/main.js","scripts":{"test":"echo \"Error: no test specified\"&& exit 1","start":"node src/main"},(略)}

アプリのソースコード作成

今回は環境構築がメインなので、アプリ自体は「ルートにアクセスしたら、mongoDBに格納されている全ての情報をjsonで返す」という簡単なものにします。
以下コードです。

src/main.js
"use strict";constexpress=require("express");constapp=express();constpersonsController=require("./controllers/personsController");constmongoose=require("mongoose");mongoose.connect("mongodb://mongo-db:27017/testdb",{useNewUrlParser:true});constdb=mongoose.connection;db.once("open",()=>{console.log("successfully connected to mongoDB");})app.set('port',3000);app.get("/",personsController.getAllPersons,(req,res,next)=>{console.log("server get request on root");res.send(req.data);});app.listen(app.get('port'),()=>{console.log(`The Express.js server has started and is listening on port number: ${app.get('port')}`);});
src/models/person.js
"use strict";constmongoose=require("mongoose");constpersonSchema=newmongoose.Schema({name:{type:String,required:true},email:{type:String,required:true,lowercase:true,unique:true},zipCode:{type:Number,min:[10000,"Zip code too short"],max:99999},});module.exports=mongoose.model("Person",personSchema);
src/controllers/personsController.js
"use strict";constPerson=require("../models/person");exports.getAllPersons=(req,res,next)=>{Person.find({},(error,persons)=>{if(error)next(error);req.data=persons;next();});};

2. gitの準備をする

アプリのルートディレクトリで以下のコマンドを実行して、gitの初期化と.gitignoreファイルの作成を行います。

$ git init
$ touch .gitignore

.gitignoreファイルに以下のように記述して、これらのファイルをgitの管理対象外にします。

node_modules/
.gitignore

3. アプリケーションレイヤーのDocker化

「Docker化=作ったアプリをdockerのイメージにする」ということです。
ここではNode.jsで動くアプリケーションレイヤーの方のDocker化をまず先に行います。

Dockerfileの作成

Dockerイメージの詳細はDockerfileに記述されます。アプリのルートディレクトリにDockerfileを作成し、以下のように記述します。

FROM node:12WORKDIR /appCOPY package.json .COPY package-lock.json .RUN npm installCOPY . .EXPOSE 3000CMD [ "node", "src/main.js" ]

参考:How To Deploy a Node App on AWS Elastic Beanstalk with Docker
参考:Node.js公式ドキュメント Node.js Web アプリケーションを Docker 化する

記述した設定の意味は以下の通り。

  1. FROM node:12→ node.js ver12の公式コンテナイメージを元にする。
  2. WORKDIR /app→ 以下の作業はコンテナ内のappディレクトリで行う。
  3. COPY ~→ ローカルのpackage.jsonpackage-lock.jsonファイルをコンテナ内のカレントディレクトリにコピー
  4. RUN npm install→ コンテナ内のカレントディレクトリでnpm installを実行
  5. COPY . .→ ローカルの残りのファイルをコンテナ内のカレントディレクトリにコピー
  6. EXPOSE 3000→ コンテナの3000番ポートを開ける
  7. CMD ~→ コンテナ起動時にnode src/main.jsを実行

Dockerfileに記述できるコードについては以下で詳しく説明されています。
参考:Dockerfileの書き方と使い方
参考:Dockerfile リファレンス

.dockerignoreの作成

Dockerfileの設定により「ローカルディレクトリ内の全てのファイルをコンテナ内にコピーする」という動作が行われますが、node_modulesのようなコピーしてたら重くてしょうがないものを対象外にします。
そもそもnodeのモジュールはnpm installで取得する仕様にしたのでわざわざローカルからコピーしなくても問題ないのです。
アプリのルートディレクトリ上に.dockerignoreを作成して以下のように記述します。

node_modules
npm-debug.log

参考:シンプルなNodeアプリをDocker化してElastic Beanstalkに移行してみた

4. mongoDB用のコンテナを作成し、アプリケーションレイヤーコンテナと連携させる

docker-compose.ymlの作成

複数個のDockerコンテナを連携させるためには、そのための設定をdocker-compose.ymlに記述します。
アプリのルートディレクトリにdocker-compose.ymlを作成して以下のように編集します。

docker-compose.yml
# docker-compose記法のバージョンversion:'3.3'# 起動するコンテナの種類を定義services:#以下node.jsのアプリケーションレイヤーの設定。このコンテナには「node-web」と名前がつくnode-web:# build時にカレントディレクトリにあるDockerfileのイメージを使用するbuild:.ports:# localの8888番ポートと、コンテナの3000番ポートをつなげる-"8888:3000"# コンテナ内でのホスト名hostname:web-server# このコンテナは、以下記述する「mongo-db」コンテナが立ち上がってから作成される。depends_on:-mongo-db# 以下mongoDBのコンテナ設定。コンテナ名は「mongo-db」とつく。mongo-db:# mongo:3.6の公式コンテナイメージを元にして作成する。image:"mongo:3.6"ports:-"27017:27017"hostname:db-servervolumes:#コンテナ内の/data/dbの中身を、「db-volume」という名前のvolumeにマウント-db-volume:/data/db-db-config-volume:/data/configdb# 作成するvolumeの名前を列挙volumes:db-volume:db-config-volume:

参考:dockerでnodejs, mongodbのローカル開発環境を構築してVisual Studio Codeでデバッグする
参考:さわって理解するDocker入門第4回 Docker Composeを使った複数コンテナのデプロイ
参考:Docker入門 〜Docker-compose, ネットワーク, ボリューム編〜

一般的にdocker-compose.ymlに記述できるキーについての説明は以下がわかりやすいです。
参考:docker-compose の設定マニュアル最新版を日本語で!!

5. VS Code のRemote Developmentを使ってDockerに乗ったリモート開発環境を作る

開発環境をコンテナ化することで、ローカル環境を汚さなくて済んだり、環境構築に失敗した時の後片付けが簡単だったり(そのコンテナを廃棄すればいいだけ)、開発メンバー間で環境がシェアできたりと何かと便利です。
参考:開発環境をDockerに乗せる方法とメリットを3ステップで学ぶチュートリアル

そのため、ローカルでの環境もDocker化して、そのコンテナにVS CodeのRemote Developmentという拡張機能を用いて入る方法で開発を進めます。

VS Codeでのコンテナへの接続方法

まず、VS Codeの画面左下にある緑色の「><」ボタンを押します。
remote-dev-status-bar.png
画像出典:VS Code 公式ドキュメント Developing inside a Container

開いたコマンドパレットの中から「Remote-Containers: Open Folder in Container…」を選択して、アプリのルートディレクトリをRemote ContainerとしてOpenすると選択します。

その後、コマンドパレットに「How would you like to create your container configuration?」と表示されるので、「From 'docker-compose.yml'」を選択します。こうすることで、docker-compose.ymlに書いた設定通りに開発環境が作られます。
スクリーンショット 2020-05-06 22.37.13.png
選択した後、コマンドパレットに「Select a service」と表示されます。コードをいじるために入って開発したいのはアプリケーションレイヤー側なので、ここではnode-webを選択します。
スクリーンショット 2020-05-06 22.37.21.png
すると、コンテナが自動で作成され、中身をVS Codeで編集できるようになります。

接続した後の状況

接続時のアーキテクチャは以下の図の通りです。
architecture-containers.png
画像出典:VS Code 公式ドキュメント Developing inside a Container

この状態では、Dockerコンテナ上のファイルを直接編集が可能になり、DockerホストとDockerコンテナの間でソースコードを同期する必要がなくなります。
ファイル新規作成/削除・ファイル編集・git操作・npm installまで全て同期されます。
参考:Visual Studio CodeのRemote DevelopmentとDockerで快適な開発環境をゲット

作成される環境は、docker-compose upを実行したときと全く同じです。
アプリケーションレイヤーのnode-webコンテナとDBレイヤーのmongo-dbコンテナ、volumeが2つ(db-volumeとdb-config-volume)、ネットワークが1つ(app_defaultという名前)作られます。
これらはdocker-compose.ymlで設定した通り、

  • mongoDBコンテナにドメイン名mongo-dbでアクセス可能(ping mongo-dbで確認できる)
  • ホストマシンの8888番ポートとアプリ側コンテナの3000番ポートが繋げてある
  • mongoDBの内容は2つのvolumeに入る

という状態なので、コンテナ内でnpm startすれば、ローカルマシンの方でwebブラウザを開き、http://localhost:8888にアクセスすることでアプリの挙動が確認できます。また、ボリュームもマウントされているので、コンテナを一旦停止したとしてもDBの内容は失われません。

参考:VS Code / Remote - Containers で docker-compose を試す
参考:Dockerize a Node.js app connected to MongoDb

VS Code Remote Developmentの設定ファイル

このVS Codeの設定が.devcontainer/devcontainer.json.devcontainer/docker-compose.ymlのファイルの形で保存されます。このファイルは最初に環境を作って接続するときに自動作成されます。

devcontainer/devcontainer.json
//Forformatdetails,seehttps://aka.ms/vscode-remote/devcontainer.jsonorthisfile'sREADMEat://https://github.com/microsoft/vscode-dev-containers/tree/v0.112.0/containers/docker-existing-docker-compose//Ifyouwanttorunasanon-rootuserinthecontainer,see.devcontainer/docker-compose.yml.{"name":"Existing Docker Compose (Extend)","dockerComposeFile":["../docker-compose.yml","docker-compose.yml"],"service":"node-web","workspaceFolder":"/workspace","settings":{"terminal.integrated.shell.linux":null},"extensions":[]}
devcontainer/docker-compose.yml
version:'3.3'services:node-web:volumes:-.:/workspace:cachedcommand:/bin/sh -c "while sleep 1000; do :; done"

設定ファイルをgit管理対象から外す

設定ファイル.devcontainerはgit管理対象外にする方がいいそうです。
参考:VSCode Remote Containerが良い

なので、.gitignoreに以下の一文を追記します。

.devcontainer/

(おまけ)mongoDBと接続しないNode.js単独コンテナの場合

開発環境で使うコンテナが1つだけの場合は、docker-compose.ymlではなくてDockerfileを元にして接続することができます。

「><」ボタン→「Remote-Containers: Open Folder in Container…」までは上記の手順と同じで、次に「From 'Dockerfile'」を選択すれば、単独コンテナの開発環境を構築→VS Codeで接続することができます。
この時、docker-compose.ymlのときとはまた別の、単独コンテナ用の.devcontainer/devcontainer.json設定ファイルが作られます。内容は以下の通り。

devcontainer/devcontainer.json
{"name":"Existing Dockerfile","context":"..","dockerFile":"../Dockerfile","settings":{"terminal.integrated.shell.linux":null},"extensions":[]}

このコンテナの起動直後は、ローカル端末とコンテナーはネットワーク上隔離されているので、コンテナー内のポートへ直接接続ができません。もしもこの中で動かしているアプリの挙動をhttp://localhost:XXXXで確認したいというならば、コンテナのYYYY番ポートとローカルのXXXX番ポートをつなげる設定を.devcontainer/devcontainer.jsonに追加する必要があります。

devcontainer.json
//local:portXXXX,container:portYYYY"appPort":["XXXX:YYYY"]

追加した設定を反映させるには、コンテナをrebuildさせる必要があります(restartではダメ)。
コマンドパレットに「>Remote-Containers: Rebuild Container」と入力して実行すれば、新しい設定に合わせて自動でコンテナが作り直されます。
参考:VS CodeのRemote-Containersでリモートサーバー上のコンテナーの開発を行う方法

6. 本番環境用のmongoDBサーバーを立ち上げる

今回本番環境へアプリをデプロイするのにElastic Beanstalkを使いますが、このElastic Beanstalkというサービスはステートレスなアプリのデプロイをサポートしているもので、mongoDBのようなステートフルのものをこれでやろうとするのはベストプラクティスではないようです。
参考:Running mongodb with Elastic Beanstalk
参考:【初心者向け】ステートフル(Stateful)とステートレス(Stateless)の違い,IPv6やAWSでの考え方

ですので、mongoDBのサーバーはElastic Beanstalkとは別に立てて、あとからアプリサーバーと連携させる形をとります。

EC2インスタンスを作成

AWS EC2の詳しい操作については今回は割愛します。

今回はAmazon Linux2のOSを選びました。
SSH接続用の22番ポートと、mongoDB用の27017番ポートを空けてインスタンスを作成します。

mongoDBのインストール

作成したEC2インスタンスにSSH接続します。

$ ssh ec2-user@<サーバードメイン> -i<path to key>
       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

謎の図形と「Amazon Linux 2 AMI」という文字が表示されれば接続成功です。

接続したら、/etc/yum.repos.d/mongodb.repoを開いて以下のように書き込みます。

$ sudo vim /etc/yum.repos.d/mongodb.repo

# 以下を書き込んで保存[mongodb-org-3.2]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/3.2/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.2.asc

その後、以下のコマンドを打ってmongoDBをインストールします。

$ sudo yum install-y mongodb-org

sudo service mongod startsudo service mongod stopといった、起動・停止コマンドが効けばインストール成功です。
参考:MongoDB On EC2を試してみる

その後、localhost以外からのアクセスを受け付けるように、configファイルを以下のように編集します。

$ sudo vim /etc/mongod.conf

# 以下をコメントアウト
# bind_ip=127.0.0.1

参考:AWSでmongoを動かしてみる(AutoScale対応)

そうしたら、sudo service mongod startでmongoDBを起動させてからサーバーログアウトします。

7. 本番環境用の設定をアプリに反映させる

今のローカルアプリのコードでは、アプリケーションレイヤーは3000番ポートで通信・DBはローカルで作成したコンテナに接続するようになっています。このままだと本番環境に乗せても動きません。
なので、「本番環境を示す環境変数が存在する場合は本番用の設定を参照、そうでないならローカル用の設定を参照」とするようにします

src/main.js
/*
mongoose.connect(
    mongodb://mongo-db:27017/testdb,
    {useNewUrlParser: true}
);
*/constmongoDB=process.env.NODE_ENV==="production"?'mongodb://[mongoDBをインストールしたインスタンスのIP address]:27017/testdb':'mongodb://mongo-db:27017/testdb';mongoose.connect(mongoDB,{useNewUrlParser:true});// app.set('port', 3000);の部分を以下のように修正app.set('port',process.env.PORT||3000);

これで、

  • NODE_ENVという環境変数の値が"production"ならば本番DBサーバーを参照、そうでないならローカルコンテナのDB
  • アプリの通信用PORT番号が定められているならその値を、そうでないなら3000番ポートを開く

という設定になりました。

8. Elastic Beanstalkの設定

Elastic Beanstalkのサービス自体の説明は、以下のAWSのスライドでわかりやすく説明されています。
参考:[AWSマイスターシリーズ] AWS Elastic Beanstalk

今回は、コマンドラインからアプリのデプロイができるように設定していきます。

EB CLIのインストール

以下の公式ドキュメントにやり方が詳しく書かれています。インストールしていない人は入れましょう。
参考:AWS公式ドキュメント macOS で EB CLI をインストールします。

EB CLIのためのアクセスキーを取得する

CLIでコマンドを打ったときに、どのアカウントが使われるのかを設定する必要があります。
このアカウント紐付けに使われるのがアクセスキーです。
アクセスキーを取得していない人は、以下の記事を参考にして、AWSのウェブコンソールから取得してください。
参考:AWS CLIのインストールから初期設定メモ

アクセスキーを取得したら、Elastic Beanstalkの機能をフルで使えるように、「AWSElasticBeanstalkFullAccess」の権限をキーに付けます。
参考:【AWS】Elastic BeanstalkでDjangoアプリをデプロイしてみた
参考:Elastic Beanstalkトラブルシューティング集

eb initでElastic Beanstalkの初期化を行う

eb initのコマンドを叩くことによって、デプロイのための最低限の設定ファイルが作られます。
アプリのルートディレクトリ下で実行します。

$ eb init

# どこのリージョンにアプリをデプロイするか選択
Select a default region
1) us-east-1 : US East (N. Virginia)
2) us-west-1 : US West (N. California)
3) us-west-2 : US West (Oregon)()
9) ap-northeast-1 : Asia Pacific (Tokyo)()(default is 3): 9

# 初めてCLIを使う場合で、アクセスキーとCLIが紐づいていない場合はここで聞かれます。# 一度でもこれを入力したことがある場合はこの項目はスキップされます。
You have not yet set up your credentials or your credentials are incorrect.
You must provide your credentials.
(aws-access-id): your-access-key-id
(aws-secret-key): your-access-secret-key
# 入力したら、~/.aws/configファイルの中に設定が保存されて、以降はそれが参照されます。# アプリの名前を決める
Enter Application Name
(default is "app"): 
Application app has been created.

# Dockerfileがディレクトリ内にあったことから、Dockerアプリだと自動で判断してくれた。# あっているのでYを入力
It appears you are using Docker. Is this correct?
(Y/n): Y

# 使うDockerの種類を選択
Select a platform version.
1) Docker 18.09.9-ce
2) Docker
(default is 1): 

# アプリのコードの置き場所としてCodeCommitを使うかどうか。# Noを選択すると、S3のバケットが自動で用意されてそこに格納されるようになる。
Do you wish to continue with CodeCommit? (y/N)(default is n): n

# アプリがデプロイされるインスタンスにSSH接続できるようにするかの設定# SSH接続できれば、何かあったときにサーバー内に入って調べられるようになるのでYが無難
Do you want to set up SSH for your instances?
(Y/n): Y

# SSH接続するためのキーペアを何にするかを選択
Select a keypair.
1) your-keypair
2)[ Create new KeyPair ](default is 1): 1

参考:Elastic Beanstalk にEB CLIから簡単デプロイ
参考:AWS CLIのプロファイルを切り替えてebコマンドで既存ElasticBeanstalk Applicationにアクセスする

すると、アプリルートディレクトリ下に.elasticbeanstalk/config.ymlが以下のように作成されて、設定が保存されます。

elasticbeanstalk/config.yml
branch-defaults:master:environment:nullgroup_suffix:nullglobal:application_name:appbranch:nulldefault_ec2_keyname:my-keynamedefault_platform:Dockerdefault_region:ap-northeast-1include_git_submodules:trueinstance_profile:nullplatform_name:nullplatform_version:nullprofile:eb-clirepository:nullsc:gitworkspace_type:Application

また、AWSのコンソールでElastic Beanstalkのページを確認すると、きちんとappというアプリができている。この時点でこのアプリに環境はまだ存在しません。

オプション設定を追加

7章で、本番環境ではNODE_ENVという環境変数を設定する仕様にしたので、デプロイしたコンテナにNODE_ENV=productionという値を渡すようにオプション設定をします。

凝った環境のカスタマイズをするためには、アプリのルートディレクトリに.ebextensionsというファイルを用意して、その中に設定ファイルを作ります(ここではenv.configとします)。
環境変数設定のためには以下のように記述します。

ebextensions/env.config
option_settings:
  - option_name: NODE_ENV
    value: production

参考:AWS公式ドキュメント 設定ファイル (.ebextensions) による高度な環境のカスタマイズ
参考:AWS Elastic Beanstalkで環境変数を追加する

Elastic Beanstalkの環境を作成する

Elastic Beanstalkでは、一つのアプリに対して本番環境や検証環境、開発環境など複数個の環境が作られることを想定しているので、アプリ1つに対して複数個環境という関係になっています。
参考:ebコマンドを使ってElasticBeanstalkでデプロイしてみる

gitのブランチ一つに対して一つの環境が紐づくようになっているので、まずは今のコードを全てコミットしておきます。

$ git commit -m"first commit"

そこで、環境を作成→デプロイするためのeb createコマンドをルートディレクトリ上で実行します。

$ eb create
Enter Environment Name
(default is app-dev):

Enter DNS CNAME prefix
(default is app-dev): 

Select a load balancer type
1) classic
2) application
3) network
(default is 2): 

設定の入力が終わったら、起動ログがターミナルに流れます。

Creating application version archive "your-app-deployed-version".
Uploading [your-app-file].zip to S3. This may take a while.
Upload Complete.

Environment details for: app-dev
  Application name: app
  Region: ap-northeast-1
  Deployed Version: your-app-deployed-version
  Environment ID: xxxxxxxxxxx
  Platform: arn:aws:elasticbeanstalk:ap-northeast-1::platform/Docker running on 64bit Amazon Linux 2/0.1.0
  Tier: WebServer-Standard-1.0
  CNAME: app-dev.ap-northeast-1.elasticbeanstalk.com
  Updated: yyyy-mm-dd hh:mm:ss

Printing Status:
yyyy-mm-dd hh:mm:ss    INFO    createEnvironment is starting.
yyyy-mm-dd hh:mm:ss    INFO    Using elasticbeanstalk-ap-northeast-1-[your bucket ID] as Amazon S3 storage bucket for environment data.
yyyy-mm-dd hh:mm:ss    INFO    Created target group named: [your-target-group-name]
yyyy-mm-dd hh:mm:ss    INFO    Created security group named: [your-security-group-name]
yyyy-mm-dd hh:mm:ss    INFO    Created security group named: [your-security-group-name-2]
yyyy-mm-dd hh:mm:ss    INFO    Created Auto Scaling launch configuration named: [your-configuration-name]
yyyy-mm-dd hh:mm:ss    INFO    Created Auto Scaling group named: [your-auto-scaling-group-name]
yyyy-mm-dd hh:mm:ss    INFO    Waiting for EC2 instances to launch. This may take a few minutes.
yyyy-mm-dd hh:mm:ss    INFO    Created Auto Scaling group policy named: [yout-auto-scaling-policy-name]
yyyy-mm-dd hh:mm:ss    INFO    Created Auto Scaling group policy named: [yout-auto-scaling-policy-name-2]
yyyy-mm-dd hh:mm:ss    INFO    Created CloudWatch alarm named: [your-cloudwatch-alarm-name]
yyyy-mm-dd hh:mm:ss    INFO    Created CloudWatch alarm named: [your-cloudwatch-alarm-name-2]
yyyy-mm-dd hh:mm:ss    INFO    Created load balancer named: [your-load-balancer-name]
yyyy-mm-dd hh:mm:ss    INFO    Created Load Balancer listener named: [your-load-balancer-listener-name]
yyyy-mm-dd hh:mm:ss    INFO    Successfully pulled node:12
yyyy-mm-dd hh:mm:ss    INFO    Successfully built aws_beanstalk/staging-app
yyyy-mm-dd hh:mm:ss    INFO    Docker container [docker-ID] is running aws_beanstalk/current-app.

これでアプリのデプロイに成功しました。

EB CLIで実行できる操作

eb statusで環境の状態ステータス(OK, severe, pendingなど)が確認できます。

$ eb status
Environment details for: app-env
  Application name: app
  Region: ap-northeast-1
  Deployed Version: your-app-deployed-version
  Environment ID: xxxxxxxxxxx
  Platform: arn:aws:elasticbeanstalk:ap-northeast-1::platform/Docker running on 64bit Amazon Linux 2/0.1.0
  Tier: WebServer-Standard-1.0
  CNAME: app-dev.ap-northeast-1.elasticbeanstalk.com
  Updated: yyyy-mm-dd hh:mm:ss
  Status: Ready
  Health: Green

eb openで作った環境のURLをブラウザで開くことができます。

eb deployで修正デプロイを行うことができます。
例えば、今の環境に紐づいているブランチにコミットを付け足して、その状態でこのコマンドを叩くと、修正デプロイ実行の様子がログで表示されます。

$ git commit -m"bug fix"$ eb deploy
Creating application version archive "your-app-deployed-version".
Uploading [your-fixed-app-file] to S3. This may take a while.
Upload Complete.
yyyy-mm-dd hh:mm:ss    INFO    Environment update is starting.      
yyyy-mm-dd hh:mm:ss    INFO    Deploying new version to instance(s).
yyyy-mm-dd hh:mm:ss    INFO    Successfully pulled node:12          
yyyy-mm-dd hh:mm:ss    INFO    Successfully built aws_beanstalk/staging-app
yyyy-mm-dd hh:mm:ss    INFO    Docker container [docker ID] is running aws_beanstalk/current-app.
yyyy-mm-dd hh:mm:ss    INFO    New application version was deployed to running EC2 instances.
yyyy-mm-dd hh:mm:ss    INFO    Environment update completed successfully.

eb terminate --allで、Elastic Beanstalkの環境・アプリもろとも全て破棄することができます。ローカルに自動生成された.elasticbeanstalkフォルダも自動でなくなります。

$ eb terminate --all
The application "app" and all its resources will be deleted.
This application currently has the following:
Running environments: xxx
Configuration templates: xx
Application versions: xxx

To confirm, type the application name: app

Removing application versions from s3.
yyyy-mm-dd hh:mm:ss    INFO    deleteApplication is starting.
yyyy-mm-dd hh:mm:ss    INFO    Invoking Environment Termination workflows.
yyyy-mm-dd hh:mm:ss    INFO    The environment termination step is done.
yyyy-mm-dd hh:mm:ss    INFO    The application has been deleted successfully.

参考:AWS公式ドキュメント EB CLI による Elastic Beanstalk 環境の管理
参考:AWS公式ドキュメント Express アプリケーションを Elastic Beanstalk にデプロイする
参考:5分でAWS Elastic Beanstalk(作って、修正して、お片付け)

9. Circle CI の導入

最後にCircle CIを使って、Githubにpushしただけで自動デプロイができるようにします。

Circle CIが使うアクセスキーを取得

8章で作ったものとは別に、Circle CIに渡す専用のアクセスキーを作成します。これにも「AWSElasticBeanstalkFullAccess」の権限を付与します。

Elastic Beanstalkの設定ファイルをgit管理対象に含める

Githubにプッシュした内容を見てCircle CIがデプロイ操作をするので、Elastic Beanstalkの設定はGithub上にのせる必要があります。
デフォルトでは管理外になっているので、.gitignoreに以下の追記をして管理対象に含めます。

!.elasticbeanstalk/config.yml

参考:CircleCI経由でElasticBeanstalkにデプロイする方法

Circle CIの設定ファイルを作る

設定はアプリのルートディレクトリの.circieci/config.ymlに書くことになっています。
なので、ファイルを作成して以下のように記述します。

circleci/config.yml
# circleciのバージョンversion:2# jobの設定jobs:# deployという名前のjobの定義を以下記述deploy:# circleciが検証用に用意するコンテナ内でのソースコードの置き場working_directory:~/app# circleciが建てるdocker imageの種類docker:-image:circleci/node:12# deployというjobを行うためにcircleciのdocker内で動作させるコマンドsteps:-checkout-run:npm install-save_cache:paths:-node_moduleskey:v1-dependencies-{{ checksum "package.json" }}-run:name:Installing EB CLIworking_directory:/command:|sudo apt-get -y -qq updatesudo apt-get install python-pip python-dev build-essentialsudo pip install awsebcli --upgrade-run:name:Deployingcommand:eb deploy [created-my-env-name]# workflow(jobを順番どうりに実行する一連のフロー)の定義workflows:version:2# workflowの名前をbuildとするbuild:# build workflowで実行するjobの順番jobs:-deploy:# deploy jobを行うのは、Githubのmasterブランチにpushが行われたときのみfilters:branches:only:-master

参考:How to deploy your NodeJs app on Amazon Elastic Beanstalk (AWS EB) with CircleCI — Short Tutorial

設定ファイルに書くことのできるキーの意味については以下の記事がわかりやすい。
参考:いまさらだけどCircleCIに入門したので分かりやすくまとめてみた

Githubのリモートリポジトリにpush

Githubのページから新しいリポジトリを作り、ローカルリポジトリと連携させます。

$ git remote add origin <git url>
$ git push -u origin master

Circle CIのプロジェクト作成

Circle CIのページで、先ほど作ったGithubのリモートリポジトリを選択して「set up project」のボタンを押す。
すると、configファイルをどうするかを設定する画面に遷移します。先ほど手動で作ってもうすでにリポジトリ内に存在するので、「add manually」を選択します。
そうすると、projectのページに自動遷移して、ビルドの様子を監視できるようになります。

Circle CIにEB CLIのアクセスキーを渡す

ただ、このままだとCircle CIはAWSにアクセスする権限がないので、デプロイは失敗します。
そのため、先ほど作ったCircle CI用のアクセスキーの情報を環境変数にして渡します。

project settingsを開いて、その中の「Environment Variables」というタブを選択します。
そこで、以下の環境変数を追加します。

  • AWS_ACCESS_KEY_ID: [your-access-key]
  • AWS_SECRET_ACCESS_KEY: [your-secret-access-key]

もう一度アプリをpush

ソースコードのどこかを適当に変えて(挙動に影響が出ない程度に)、Githubのmasterブランチに再びpushします。
すると、pushを感知してCircle CIが動き、アプリのデプロイができます。

もしデプロイ時に「InvalidProfileError」が出るなら、.elasticbeanstalk.config.ymlの設定ファイルのprofile: eb-cliをコメントアウトすれば一応動くようにはなります。
参考:How to setup Elastic Beanstalk Deployment?

まとめ

これでとりあえず最低限の環境構築ができました。お疲れ様でした。あとはアプリの内容を充実させることに集中することができます。
ただし、ここで紹介した手順・構成がベストプラクティスとは限りません。今回は動くことを重視したため、セキュリティ面ではまだまだ手を入れないといけないところがあると思います。その点だけはご了承ください。

Webpackにてhtml-webpack-inline-source-pluginを利用する

$
0
0

はじめに

webpackで出力する際に出力されたJavascriptをHTMLファイルに差し込みたいと思います。

が、html-webpack-inline-source-pluginを使えば良いという点までは辿り着いたものの、公式のやり方では素直に実現出来なかったので発生した問題とその解決方法を記載致します。

検証した環境

- Node.js 13.13.0
    - webpack 4.42.1
    - webpack-cli 3.3.11
    - ts-loader 6.2.2
    - html-webpack-plugin 4.2.0
    - html-webpack-inline-source-plugin 1.0.0-beta.2

今回発生した問題

公開されている通常のhtml-webpack-inline-source-pluginを利用するとバンドル時に以下のようなエラーが発生してしまいます。

Cannot read property 'tapAsync' of undefined

調べた所対象PluginのIssuesに答えがありましたが、現行のバージョンで発生しているバグのようです。 これを回避する方法を記載します。
https://github.com/DustinJackson/html-webpack-inline-source-plugin/issues/63

解決方法

  • html-webpack-inline-source-pluginのバージョンを1.0.0-beta.2で指定する
  • webpack.config.jsの記載方法を公式の書き方から変える。

以下に手順を示していきます。

Node.jsの開発環境を準備しましたらpackage.jsonを以下の通りにします。

package.json
{"name":"name","dependencies":{"html-webpack-inline-source-plugin":"1.0.0-beta.2","html-webpack-plugin":"4.2.0","ts-loader":"6.2.2","typescript":"3.8.3","webpack":"4.42.1","webpack-cli":"3.3.11","webpack-dev-server":"3.10.3"}}

インストールする

npm install

webpack.config.jsを以下の通り準備する

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin");

const mode = process.env.NODE_ENV === "development" ? "development" : "production";
const entry = path.resolve("src", "index.ts");
const outPath = path.resolve("build");
const outFileName = "main.js";
const rules = [{ test: /\.tsx?$/, use: "ts-loader" }];
const resolve = { extensions: [".ts", ".tsx", ".js", ".json"] };
const output = { path: outPath, filename: outFileName };
const devServer = { contentBase: outPath };

const plugins = [
new HtmlWebpackPlugin({ inject: true, inlineSource: '.(js|css)$', template: "./src/index.html" }),
new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),
]

module.exports = [{
  mode,
  entry,
  output,
  module: { rules },
  resolve,
  externals,
  devServer,
  plugins,
}]

ここでポイントは以下の箇所になります。他の所はお好みで記載して良いです。寧ろ私のwebpack.config.jsのこの書き方はかなり我流なのであまり真似しない方が良いかもしれません。

webpack.config.json
constplugins=[newHtmlWebpackPlugin({inject:true,inlineSource:'.(js|css)$',template:"./src/index.html"}),newHtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin),]

通常公式では以下の通り記載することになっていますが、

webpack.config.json
newHtmlWebpackInlineSourcePlugin()

これを以下の通りに変更するとErrorが解消されます。 ※なお今回Installしたプラグインのバージョンで上記公式の記載方法を取ると引数が足りないとErrorになります。

new HtmlWebpackInlineSourcePlugin(HtmlWebpackPlugin)

上記の状態でバンドルしてみてください。上手くインラインされて出力されると思います。

以上

[Node][Yarn] create-react-app したプロジェクトでyarn start でエラー

$
0
0

起こったこと

react.js のcreate-react-appコマンドで作成した新規プロジェクトで
yarn start ( react-scripts start) してみたが、以下エラー。

$ yarn start
...
There might be a problem with the project dependency tree.
It is likely not a bug in Create React App, but something you need to fix locally.

The react-scripts package provided by Create React App requires a dependency:

"babel-loader": "8.1.0"

Don't try to install it manually: your package manager does it automatically.
However, a different version of babel-loader was detected higher up in the tree:

/Users/user/node_modules/babel-loader (version: 8.0.6)

Manually installing incompatible versions is known to cause hard-to-debug issues.

If you would prefer to ignore this check, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That will permanently disable this message but you might encounter other issues.

To fix the dependency tree, try following the steps below in the exact order:

Delete package-lock.json (not package.json!) and/or yarn.lock in your project folder.
Delete node_modules in your project folder.
Remove "babel-loader" from dependencies and/or devDependencies in the package.json file in your project folder.
Run npm install or yarn, depending on the package manager you use.
In most cases, this should be enough to fix the problem.
If this has not helped, there are a few other things you can try:

If you used npm, install yarn (http://yarnpkg.com/) and repeat the above steps with it instead.
This may help because npm has known issues with package hoisting which may get resolved in future versions.

Check if /Users/user/node_modules/babel-loader is outside your project directory.
For example, you might have accidentally installed something in your home folder.

Try running npm ls babel-loader in your project folder.
This will tell you which other package (apart from the expected react-scripts) installed babel-loader.

If nothing else helps, add SKIP_PREFLIGHT_CHECK=true to an .env file in your project.
That would permanently disable this preflight check in case you want to proceed anyway.

P.S. We know this message is long but please read the steps above :-) We hope you find them helpful!

...
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! myapp@0.1.0 start: react-scripts start
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the myapp@0.1.0 start script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:

babel-loader@8.1.0自体はlocal プロジェクトdir にもあり
インストールされている。

解決手順もエラーメッセージにあるが、
試しても上手くいかず。

原因

ようは、以前に brew install nodeした際に
/Users/<user>/node_modules
にpackage ファイルを置くように NODE_PATH が設定されるようだが、
その時にインストールしたもの が残っていた。

そして、
今回yarn で読み込み時に、別のnode_modules pathを優先的に参照しているようのため
それを直せばいい

解決策

とりあえず、参照していた (エラーメッセージにもある通り)
/Users/<user>/node_modules
のnode_modulesディレクトリ自体を移動して、参照しないようにした。(安全確認の後、削除)

bash
$ rm -rf /Users/user/node_modules # 対処例として
$ yarn start
yarn run v1.16.0
react-scripts start
...
Compiled successfully!

yarn start通った。

MacOS はHomebrew, NVM, Nodebrew など バージョン管理ツールがあるが、
環境変数 Path などが違うようなので注意。

参考

https://github.com/facebook/create-react-app/issues/8781

https://stackoverflow.com/questions/59633499/why-cant-i-run-yarn-start

ElasticSearchのindexデータを移行する

$
0
0

はじめに

今回、AWS ElasticSearchからローカルのElasticSearchにデータを移行したかったのでElasticDumpを使って、試しにやってみました。

ElasticSearchはVPCからしかアクセスできなかったので、踏み台のEC2から接続しています。

注意点

※他にいい方法はたくさんありますのであくまでも一つの方法です。

  • index単位なので、indexが多いと面倒です。などで一括でindexをdumpするなんてこともできません。

EC2にNodeをインストール

AWS公式のNodejsインストール手順を元にnvmでインストールします。

# EC2の中# nvmインストール
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash

# nvmを有効化. ~/.nvm/nvm.sh

# nodeのインストール
nvm install node

# Node.js が正しくインストールされ、実行されていることをテストします。
node -e"console.log('Running Node.js ' + process.version)"

ElasticDumpのインストール

# EC2の中
npm install elasticdump -g

dumpする

# EC2の中

# index一覧を確認
curl -XGET {VPCエンドポイント}/_cat/indices?v

# dumpする
elasticdump --input=http://localhost:9200/{index名} --output={ファイル名}.json

EC2からファイルをローカルにダウンロード

# ローカル

scp {ssh接続先}:{path}/{ファイル名}{ローカルのパス}

dumpファイルをインポート

# ローカル
elasticdump --input={ファイル名}.json --output=http://localhost:9200/{index名}

移行完了!

【Node Express】DBのデータをExcellで出力。

$
0
0

使うもの

1)sheet.js
エクセルファイルの書き換え等ができる。
今回はオブジェクトをエクセルファイルに変換するのに使用した。
https://www.npmjs.com/package/xlsx

インストール:
npm install --save xlsx

2)moment.js
時間を扱うのに便利そうだったため。
https://www.npmjs.com/package/moment

インストール:
npm install --save moment

実装

main.js
constexpress=require('express')constapp=express()constmodel=require('モデルのパス')constPREFS=require('定数ファイル').PREFSconstxlsx=require('xlsx')constmoment=require('moment')app.get('/get_excel',async(req,res)=>{try{constselectData=['user_id','user_name','user_pref']constuserSearch=awaituser.getUsers(selectData)if(userSearch){varnewData=userSearch.map(function(record){record.ID=record.user_idrecord.ユーザー名=record.user_name//都道府県をINTで管理していると仮定して。record.都道府県=PREFS[record.user_pref]//数字は返しても仕方ないので消す。deleterecord.user_prefreturnrecord})//エクセルファイルを作成varnewWB=xlsx.utils.book_new()//エクセルシートを作成varnewWS=xlsx.utils.json_to_sheet(newData)//日付をつけた方が資料の管理がしやすそうなので、不要であればいらないです。constcreateTime=moment(Date.now()).format('YYYYMMDD')//ファイル名もといパスを指定constfileName='エクセルファイルの保存先のパス'+createTime+'_ユーザー情報.xlsx'//エクセルファイルにするxlsx.utils.book_append_sheet(newWB,newWS,'ユーザー情報')xlsx.writeFile(newWB,fileName)//エクセルのパスを返すreturnres.status(200).download(fileName)}else{returnres.status(500).json({message:'エラー発生!'})}}catch(err){returnres.status(500).json({message:'エラー発生!'})}})

モデル

model.js
constmysql=require('mysql2')constmodels={}//データベースの設定constconnection=mysql.createConnection({host:ホスト,user:ユーザー,password:パスワード,database:データベース名,port:ポート番号,multipleStatements:true//複数のコールを有効化})//モデルmodel.getUsers=(selectData)=>{returnnewPromise((resolve,reject)=>{connection.query('SELECT ?? FROM user',[selectData],(err,results)=>{if(err)returnreject(err)elsereturnresolve(results)})})}module.exports.model=model

【備忘録】dynamoDBをいじる Node.js

$
0
0

DynamoDBのデータを書き込んだり読み込んだりするときにみるqiita

dynamoDBをlambdaからじゃなくて外部から読み込みたい時もありますよね。
そんな時に使えるやつです

記事を探したけどれが動くかわからなかったので現時点で動くやつを書いておこう
特に詳しくないので動く重視のコードです。

よく使いそうなやつだけサンプルコード載せておきます

  • getItem 一個だけ取得
  • batchGetItem 複数取得
  • putItem 一個だけ追加
  • batchWriteItem 複数追加
  • listTable AWSアカウントに紐づいているテーブル一覧

ドキュメント

ドキュメント見ればだいたいなんでもできる

https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#putItem-property

getItem 一個だけ取得

index.js
constAWS=require("aws-sdk");//インスタンス生成constdynamodb=newAWS.DynamoDB({region:"ap-northeast-1",apiVersion:"2012-08-10",accessKeyId:"xxxxxxxxxxxxxxxx",secretAccessKey:"xxxxxxxxx",});asyncfunctiontest(){varparams={Key:{"<プライマリパーティションキー>":{S:"<キーの値>"}},TableName:"<TableName>"};constData=await(()=>newPromise((resolve)=>{dynamodb.getItem(params,function(err,data){if(err){console.error("Error occured",err);}console.log(data);resolve(data);});}))();if(Data!==undefined){console.log(Data)}}test();

batchGetItem 複数取得

取得するキーの値が同じだとエラーを吐いた。

index.js
constAWS=require("aws-sdk");//インスタンス生成constdynamodb=newAWS.DynamoDB({region:"ap-northeast-1",apiVersion:"2012-08-10",accessKeyId:"xxxxxxx",secretAccessKey:"xxxxxxxx",});asyncfunctiontest(){varparams={RequestItems:{"<TableName>":{Keys:[{"<プライマリパーティションキー>":{S:"<値>"}},{id:{S:"<値>"}}],},},};constData=await(()=>newPromise((resolve)=>{dynamodb.batchGetItem(params,function(err,data){if(err){console.error("Error occured",err);}console.log(data);resolve(data);});}))();if(Data!==undefined){console.log(Data)}}test();

putItem 一個だけ追加

iindex.js
constAWS=require("aws-sdk");//インスタンス生成constdynamodb=newAWS.DynamoDB({region:"ap-northeast-1",apiVersion:"2012-08-10",accessKeyId:"xxxxxxxxxxxxxxxxx",secretAccessKey:"xxxxxx",});asyncfunctiontest(){varparams={Item:{"<項目>":{S:""},"<項目>":{S:""},"<項目>":{S:""}},TableName:"<TableName>"};constData=await(()=>newPromise((resolve)=>{dynamodb.putItem(params,function(err,data){if(err){console.error("Error occured",err);}console.log(data);resolve(data);});}))();if(Data!==undefined){console.log(Data)}}test();

batchWriteItem 複数追加

index.js
constAWS=require("aws-sdk");//インスタンス生成constdynamodb=newAWS.DynamoDB({region:"ap-northeast-1",apiVersion:"2012-08-10",accessKeyId:"xxxxxxxK",secretAccessKey:"xxx",});asyncfunctiontest(){varparams={RequestItems:{"<TableName>":[{PutRequest:{Item:{"<キー>":{S:"<値>"},"<項目>":{S:"<値>"},"<項目>":{S:"<値>"}}}},{PutRequest:{Item:{"<キー>":{S:"<値>"},"<項目>":{S:"<値>"},"<項目>":{S:"<値>"}}}},{PutRequest:{Item:{"<キー>":{S:"<値>"},"<項目>":{S:"<値>"},"<項目>":{S:"<値>"}}}}]}};constData=await(()=>newPromise((resolve)=>{dynamodb.batchWriteItem(params,function(err,data){if(err){console.error("Error occured",err);}console.log(data);resolve(data);});}))();if(Data!==undefined){console.log(Data)}}test();

listTable AWSアカウントに紐づいているテーブル一覧

index.js
constAWS=require("aws-sdk");//インスタンス生成constdynamodb=newAWS.DynamoDB({region:"ap-northeast-1",apiVersion:"2012-08-10",accessKeyId:"xxxxxxxx",secretAccessKey:"xxx",});asyncfunctiontest(){varparams={};constData=await(()=>newPromise((resolve)=>{dynamodb.listTables(params,function(err,data){if(err){console.error("Error occured",err);}console.log(data);resolve(data);});}))();if(Data!==undefined){console.log(Data)}}test();

参考にした記事

http://kakts-tec.hatenablog.com/?page=1499360527

[Microsoft] macOSからMicrosoft 365 (Office 365) を管理する

$
0
0

Microsoft 365 (Office 365) の管理にはPowerShellを使うのが定番です。
PowerShell Core 7.0も出ているし、そろそろmacからもできるかなぁと思いましたが、どうもまだできない様子です。

代わりに、Microsoft Pattern and Practice teamが作っているNodeでできたツールを使ってみます。

あらかじめ必要なもの

  • Nodeまたは Nodenv

はじめかた

npmを使ってインストールします。

npm install --global @pnp/office365-cli

Nodenvを使っている場合は、rehashもします。

nodenv rehash

o365コマンドが使えるようになりました。

使い方

o365 helpとするとヘルプが表示されます。

$o365 help
  Office 365 CLI v2.9.0
  Manage Microsoft Office 365 and SharePoint Framework projects on any platform

  Commands:

    help [command...]  Provides help for a given command.
    exit|quit          Exits application.
    version            Shows the current version of the CLI
    login [options]    Log in to Office 365
    logout [options]   Log out from Office 365
    status [options]   Shows Office 365 login status

  Command Groups:

    aad *              36 sub-commands.
    accesstoken *      1 sub-command.
    cli *              7 sub-commands.
    consent *          1 sub-command.
    flow *             12 sub-commands.
    graph *            5 sub-commands.
    onedrive *         7 sub-commands.
    outlook *          13 sub-commands.
    pa *               5 sub-commands.
    planner *          1 sub-command.
    skype *            3 sub-commands.
    spfx *             3 sub-commands.
    spo *              216 sub-commands.
    teams *            42 sub-commands.
    tenant *           5 sub-commands.
    util *             1 sub-command.
    yammer *           17 sub-commands.

いくつもあるサブコマンドのうち、省略形でわからないものをあげます。

  • aad - Azure ActiveDirectory
  • pa - Power Automate
  • spfx - SharePoint Framework
  • spo - SharePoint Online

ログインする

最初にログインします。

o365 loginとすると、https://microsoft.com/deviceloginへアクセスし、デバイスコードを入力するように促されます。

別途ブラウザで https://microsoft.com/deviceloginへアクセスし、表示されているデバイスコードを入力します。

アプリケーションへのアクセス許可を求められるので許可します。

よくある(?)ユースケース

PowerShell でよくやっていたユースケースがo365 CLIでどうなるのか調べてみました。

テナント(ディレクトリ)の切り替え

できない(?)

ユーザの追加

できないようです。

スケジュールのアクセス権変更

できないようです。

感想

まだこれからのようです。

リンク


aws amplify+nuxt.jsの一から環境構築 in mac

$
0
0

概要

今回はMacOS上でaws amplify+nuxt.jsの一から環境構築していこうと思います。
備忘録ということもあり、わかりにくい部分もあるかもしれません。その際はご指摘頂ければと思います。
今回はAWSのアカウントがある前提で進めていきます。

①Homebrewのインストール

公式にアクセスし、インストールするコマンドを実行

Homebrew公式

% /bin/bash -c"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

インストール後にソフトウェア・アップデート

スクリーンショット 2020-05-08 15.36.24.png

正常にインストールされてるかの確認

% brew --version
Homebrew 2.2.15
Homebrew/homebrew-core (git revision 93a12; last commit 2020-05-08)

②AWS CLI バージョン 2 のインストールと設定

インストール

公式を参考にインストールしていきます。
せっかく公式がインストーラを用意してくれてるので、甘えて使っちゃいましょう。
インストーラを起動すると、選択できる項目があると思いますが、今回は全てデフォルトのままインストールします。

# インストールの確認
% aws --version
aws-cli/2.0.12 Python/3.7.4 Darwin/19.4.0 botocore/2.0.0dev16

設定

公式のセキュリティ認証情報へ遷移し、アクセスキー (アクセスキー ID とシークレットアクセスキー)を選択します。新しいアクセスキーの作成ボタンをクリックし、ポップアップで表示されたアクセスキー IDとシークレットキーをコピーします。

# コピーしたアクセスキー IDとシークレットキーを貼り付ける
% aws configure
AWS Access Key ID [None]: アクセスキー ID
AWS Secret Access Key [None]: シークレットキー
# リージョンを選択。今回はオハイオ
Default region name [None]: us-east-2
# "text","json","table"のどれかを指定可能ですが、今回はjson
Default output format [None]: json

備考:AWSアカウントでセキュリティ向上に伴い、多要素認証(MFA)も設定することをお勧めします。

③javaのインストール

※ Amplify Mockingを使うため、OpenJDK 1.8 以降のJavaのランタイムをインストールします。

# 最新バージョンがインストールされます
% brew install openjdk

# シンボリックリンクの作成sudo ln-sfn /usr/local/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk

# バージョンの確認
% java --version
openjdk 13.0.2 2020-01-14
OpenJDK Runtime Environment (build 13.0.2+8)
OpenJDK 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)

④Node.jsのインストール

Node.js

今回は推奨のNodeバージョンをインストールしたいため、nodebrewをインストールします。
公式にあるLTSバージョンをnodebrewで指定し、インストールします。
公式からインストーラをダウンロードするのもありですが、Nodeのバージョン管理を安易にしたいため、nodebrewを使っていきます。

% brew install nodebrew

# nodebrew用のディレクトリを作成
% mkdir-p ~/.nodebrew/src

# 公式で推奨(LTS)されてるバージョンをインストール
% nodebrew install-binary v12.16.3

# バスを追加# .zshrcファイルがない場合は作成します。
% echo'export PATH=$HOME/.nodebrew/node/v12.16.3/bin:$PATH'>> .zshrc

# terminalを再起動し、インストールの確認
% node -v; npm -v
v12.16.3
6.14.4

⑤aws-amplify/cliとnuxt.jsのインストール

% npm install-g @aws-amplify/cli create-nuxt-app@latest

# バージョン確認
% amplify --version; create-nuxt-app --version
4.18.1
create-nuxt-app/2.15.0 darwin-x64 node-v12.16.3

⑥gitのインストール

% brew install git

% git --version
git version 2.23.0

AWS Lambda Layer(NodeJS)の作り方

$
0
0

NodeJSを動かすのにライブラリが不足する事態発生

  • 説明端折りますが、どうやらLayerというものを入れれば良いと検索して分かる。
  • 作り方が色々書いてあって、最初間違っているサイト(バージョンが古い?)を閲覧してしまったハマったので自分の方法を記載します。

バージョン

闇雲に作ってはいけない。決まりがあった

  • 決まった方法やディレクトリ名で作る
  • ディレクトリ名は「nodejs」※それ以外は多分NG
mkdir nodejs
cd nodejs
  • ディレクトリ内でnpm initをします。 ※全てデフォルト、エンターでOKです。
  • そして、必要なライブラリをインストールします。※例では3つ(require、puppeteer-core、chrome-aws-lambda)をインストールしています。
  • インストール後にls -l すると、「node_modules」ディレクトリが作成され、モジュールが入っていればOKです。
npm init
npm i require puppeteer-core chrome-aws-lambda 
  • インストールしたら、nodejsディレクトリをzipにします。zip名はたぶん何でも良いはずですが、他は試していません。
cd ../
zip -r modules.zip nodejs  

あとはZipをLambdaのLayerにアップロードすれば使えます。

  • 随分ハマったので、どなたかのお役に立てれば幸いです。

【LINE MessagingAPI】アクションが増えすぎてindex.jsがもたない。。

$
0
0

今回のお題

module化して一つ一つの処理を分けたい。

hello -> helloを返すmoduleにするような感じにしてメインのJsをすっきりさせたい。

環境

  • heroku
  • nodejs

以前までの私

switch ~ caseを使った方法でのアクションの実装を行っていました。
しかし、見てわかるようにコードが間延びしていき、すごく見づらくなってしまいました。
少しのアクションメソッドだけなら対応できますが、より多くのイベントを個別にとなってくるとコードが破綻しました。

app.post('/webhook',line.middleware(config),(req,res)=>{Promise.all(req.body.events.map(eventHandler)).then((result)=>res.json(result)).catch((err)=>{console.error(err)res.status(500).end()})})functioneventHandler(event){letclient=newline.Client(this.config)...switch(event.message){case"":returnclient.replyMessage(token,{type:"text",text:replyText})break}...}

ディレクトリ構成

破綻しないためにはどうすればいいか,悩みに悩んだ挙句ディレクトリごとに処理を切り分けたら切ではないかと思い、以下のようなディレクトリ構成を実装しました。
実際に動くアクションの部分を毎回作り替えることで一から書くプログラムの量も少なくなり、他との差別化が測れるのではないかと考えました。

├── message
│   ├── message.js
│   ├── text
│       ├── action
│       │   ├── action1
│       │   │   └── action1.js
│       ├── reply.json
│       └── text.js
├── postback
│   ├── action
│   │   └── template.js
│   └── postback.js
└── replyModule.js  // message typeによる実行moduleの変更
index.js  // webhookの受け取り

イベントタイプごとにディレクトリを区切り、そのイベントがあった際に受け取る窓口を各ディレクトリに用意しておく。

index.js

nodeでexpressを使ってwebhookを用意します。
オリジナルのReplyModuleを読み込み、そこでイベントタイプごとに実行するjsを変更するようにします。

constline=require('@line/bot-sdk')constexpress=require('express')/**
 * オリジナルのリプライモジュールを読み込む
 */constReplyModule=require('./event/replyModule')constapp=express()/**
 * LINEのチャンネルアクセストークンとチャンネルシークレットを適用させる
 */constconfig={channelAccessToken:process.env.CHANNEL_ACCESS_TOKEN,channelSecret:process.env.CHANNEL_SECRET}app.post('/webhook',line.middleware(config),(req,res)=>{Promise.all(req.body.events.map(eventHandler)).then((result)=>res.json(result)).catch((err)=>{console.error(err)res.status(500).end()})})functioneventHandler(event){letreplyModule=newReplyModule(event)returnreplyModule.replyHandller()}

 replyModule.js

イベントタイプごとに実行するModuleを切り替えます。
中身は単純に連想配列で実行する関数を切り替えているだけです。
switch文ではなく、LINEのイベントごとに実行する関数を切り替えることで、どのイベントで何をやるを明確に分けることができるので使いやすいのではないかとおもいます。

replyModule.js
"use strict"constline=require('@line/bot-sdk')/**
 * @constructor
 */constReplyModule=function(req){this.event=req;this.execFunc={..."message":this.execMessage.bind(ReplyModule,this.event),"follow":this.execFollow.bind(ReplyModule,this.event),...}}/**
 * exec message function
 * @return json
 */ReplyModule.prototype.execMessage=(event)=>{// 別moduleにするもよしletMessageModule=require('./message/message')letmessageModule=newMessageModule(event)returnmessageModule.messageHandller()}/**
 * exec follow function
 * @return json
 */ReplyModule.prototype.execFollow=(event)=>{// そのままリプライするもよしletconfig={channelAccessToken:...,channelSecret:...}letclient=newline.Client(config)returnclient.replyMessage(event.replyToken,{type:"text",text:"登録してくれてありがとう!"}}/**
 * @return json
 */ReplyModule.prototype.replyHandller=function(){lettype=this.event.typereturnthis.execFunc[type]()}module.exports=ReplyModule;

あとは、message typeごとに切り分けたりすることでそれぞれのModuleを実行してくれるので,このイベントはこれ!のようにうまく分けられたりするのではないかと思います。
postbackイベントとtextイベントはそれぞれ似たような作りにできるかと思います。
おそらく言語解析系のサービスに接続されると思いますので、各サービスに合わせた形でtextModuleのようなものを作成し、
言語解析系のアクションごとに切り分ける方法も有用かと思います。

学び始めて日が浅いので拙いコーディングですが、誰かの参考になれば幸いです。

Express Generator で作成されたファイルを触って Express を理解したい3:use,get,post,all,パスの表記

$
0
0

主旨

Express 4.x が対象です。express app_name --view=pugとして生成されたファイルを見ながら、expressの仕組みを理解していく。

公式ドキュメントの対応箇所は以下です:
- Express アプリケーションで使用するミドルウェアの作成

前提

以下の記事は、express-generatorを使って下記のコマンドで app.js以下のファイルを生成していると想定している。

$ express appname --view=pug

生成されたファイルは以下の通り。

project_dir
├── app.js          # アプリのメインファイル
├── bin
│   └── www            # yarn start 時に node bin/www として実行されるファイル
├── package.json       # ライブラリ等の依存関係やバージョン情報を格納したファイル
├── public          # static なファイルを置くフォルダ
│   ├── images         # http://localhost:8000/images 
│   ├── javascripts    # http://localhost:8000/javascripts 
│   └── stylesheets    # http://localhost:8000/stylesheets
│       └── style.css  # http://localhost:8000/stylesheets/style.css
├── routes          # router (ミドルウェア) 置き場
│   ├── index.js       # http://localhost:8000/ (トップページ)
│   └── users.js       # http://localhost:8000/users
└── views           # テンプレートファイル置き場
    ├── error.pug      # エラー時のテンプレート
    ├── index.pug      # index.js 用のテンプレート
    └── layout.pug     # index.pug や error.pug に読みこまれるテンプレート

実行時には 8000 番ポートを指定して使うものとする。

$ PORT=8000 yarn start

実行後、ブラウザで http://localhost:8000にアクセスしたときに、下記の表示になっているものとする。

app / router の関数

よく使うのは以下の5つくらい。

  • app.use
  • app.get
  • app.post
  • app.route
  • app.all

get, post, all 関数

app.getapp.postは、それぞれ HTTP リクエストの "GET" と "POST" に対応している。app.useは GET も POST も拾うが、app.getの場合は GET リクエストだけを拾う。

app.get('/',function(req,res){res.send("GET");});app.use('/',function(req,res){res.send("USE");});

たとえば、上記のコードの場合、curl でアクセスすると、下記のような結果になる。

$ curl -X GET https://localhost:8000
GET
$ curl -X POST https://localhost:8000
USE

use と get の行の順序を逆にすると、

$ curl -X GET https://localhost:8000
USE
$ curl -X POST https://localhost:8000
USE

こうなる。useは GET も POST も両方拾っており、getは GET だけを拾っている。

allはすべてのリクエストを拾う。useと似ているが、パスにマッチしたあとの挙動が少し異なる。

varrouter=express.Router();router.use('/aaa',function(req,res){res.send('aaa');});router.use('/all/bbb',function(req,res){res.send('bbb');});app.use('/use',router);app.all('/all',router);

route 関数

routeは少し特殊で、下記のような書き方ができる。

app.route('/').post(function(req,res){res.send("POST");}).get(function(req,res){res.send("GET");}).all(function(req,res){res.send("ALL");});

ひとつのパスに対して、複数のリクエストを処理する関数を定義できる。上のコードの場合、下記の実行結果になる。

$ curl -X GET https://localhost:8000
GET
$ curl -X POST https://localhost:8000
POST
$ curl -X PUT https://localhost:8000
ALL

.get, .allなどの関数の順序は、実行結果に影響する。先に書かれたリクエストの処理が優先される。下記のように記述すると、

app.route('/').all(function(req,res){res.send("ALL");}).post(function(req,res){res.send("POST");}).get(function(req,res){res.send("GET");});

実行結果は下記のようになり、all で定義された関数が、他の関数より優先されて実行される。

$ curl -X GET https://localhost:8000
ALL
$ curl -X POST https://localhost:8000
ALL
$ curl -X PUT https://localhost:8000
ALL

app と router の違い

get, useなどの関数については、app (express) と router (experss.Router) に特に違いはない。approuterの大きな違いは、routerappの関数の引数にできるが、その逆はできないという点である。だから、app 内からミドルウェアを呼びだす時は routerを使いましょう、ということらしい。

で、appはアプリケーションに一つしか存在できないからそうするものだと思ってたけど、下記のようなコードを書いたら問題なく動いてしまった。

app.js
varapp=express();varapp2=express();app2.use('/bbb',function(req,res){res.send('app2')});app.use('/aaa',app2);

実行すると、下記のようになる。

$ curl -X GET http://localhost:8000/aaa
エラー
$ curl -X GET http://localhost:8000/aaa/bbb
app2
$ curl -X GET http://localhost:8000/bbb
エラー

app2routerに変更しても同じ挙動になる。あれ、じゃあ router使う意味って何だろう。そもそもappを二つ作ったら、何か混乱が起こるかもしれないと思っていたけど、上の結果を見ると問題なさそう。先に作った appuseがちゃんとルートを認識していて、app2appの子になっていて、問題なく動いている。(app2は exports されてないから当然といえば当然)

いろいろ調べてみた結果、明確な情報は見つけられなかった。おそらくだけど、appより routerのほうがコンパクトなオブジェクトになっているので、appを使うより routerを使うほうがリソースが節約できる、ということだと思う(完全に推測です)。

余談: app.use のパス表記

パス (ルートパス) とは、たとえば http://localhost:8000/xxxx/yyyy/zzz.html?aaa=bbb&ccc=dddという URL でサーバにアクセスされたときに、 /xxx/yyy/zzz.htmlの部分を解析するために使われる文字列のことを言う。app.useapp.getなどのルートハンドラーの第一引数として指定される。

たとえば、下記の //usersはいずれも「ルートパス」にあたる。

app.js
app.use('/',indexRouter);app.use('/users',usersRouter);

この「(ルート)パス」の表記は、bash などで使える正規表現や、XPATH とも違うし、JSON のパス表記とも微妙に違うという、やや独特な表記法を使うようになっている(どうしてこうなったの?)。そのため、要点を押さえていないと、まったく想定していない URL にマッチしてしまったり、マッチさせたい URL にどうしてもマッチしてくれなかったりする。

ここでは、間違いやすいルートパスの表記についてのみ記載する。ルートパス表記に関する詳しいドキュメントは下記にある。

ルーティングに関する公式のドキュメント:
- https://expressjs.com/ja/guide/routing.html

ルートパスのマッチングの詳細:
- https://www.npmjs.com/package/path-to-regexp

なお、この記事や関連記事では、ルートパスのことを単に「パス」と書いている(混乱がない限り)。

余談: app.use のパスのマッチング

以下は、PORT=8000 yarn statとしてアプリを実行しているものとする。なお、以下は app.useを使用する場合であって、後述するように app.allapp.getなどは下記とは異なるマッチをする。これは結構な罠なので要注意!

最後の例のように、正規表現をパスとして使う時はシングルクオート ''は書かない。一見して、正規表現を表わす /がパスの区切りの /と同じなので、つい '/aaa/'と書いてしまったりする。逆に '/aaa/'とするべきところを /aaa/と書いてしまってハマる(ハマった)。

正規表現まわりについては、言語によって違う部分も多いので公式のドキュメントをよく読もね(自戒)。

下記も参考までに;
- 基本的なルーティング

余談: Router を使う場合のパス表記

app.userouter.useを使って、二段階のパスのマッチをするように処理を書くと、app.useに直接 functionを書いた場合とは異なる挙動をする。

以下、例示してみる、

app.js
app.use('/aaa',function(req,res){res.send('超Lチカ');});

routes/app.jsに上記のような /aaaのパスに対する処理の記述がある場合、下記の URL にアクセスしたときのみ、ブラウザに "超Lチカ" と表示される。

ここで、下記のように書きかえて実行してみる。

app.js
varrouter=express.Router();router.use(`/`,function(req,res){res.send('超Lチカ');});app.use('/aaa',router);

このようにしても、http://localhost:8000/aaaにアクセスしたときに "超Lチカ" と表示される。
これは、以下のように処理がなされるためである。

  • app.use('/aaa', ...)でまず /aaaにマッチする。
  • URL がそのまま routerに送られる、
  • router側の router.use('/', ... )の行では、送られてきた URL から、送り元(親)の app でマッチした文字列("/aaa" の部分)を取り除いた上で、パスのマッチ処理をする。
  • その結果 "/" にマッチする。(URL が空の場合は "/" と見なされる)
  • function内の res.sendが実行される。

これをさらに、下記のように書きかえてみる。

app.js
varrouter=express.Router();router.use(`/bbb`,function(req,res){res.send('超Lチカ');});app.use('/aaa',router);

この場合、http://localhost:8000/aaa/bbbにアクセスしたときのみ "超Lチカ" と表示される。一方で http://localhost:8000/aaaはエラーが表示される。

app.use('/aaa', ...)で "/aaa" にマッチしているにも関わらずエラー表示が出てしまうのは、次のように処理がなされるからである。

  • app.use('/aaa', router );で第一引数の '/aaa' が URL の "/aaa" の文字列にマッチして、routerが呼びだされる。
  • URL がそのまま routerに送られる、
  • router側の router.use('/', ... )の行では、送られてきた URL から、送り元(親)の app でマッチした文字列("/aaa" の部分)を取り除いた上で、パスのマッチ処理をする。
  • routerには router.use('/', ... )という記述がない。つまり、"/" というパスにマッチする処理がないため、何の処理も行なわれない。(空の URL は "/" と見なされる)
  • この結果 app.jsに処理が戻る。app.use('/aaa', router );の行の処理は、この時点で終わる。
  • app.jsの下記のコードで "/aaa" にマッチして (パスが指定されていないため、すべてのパスにマッチする)、404 エラーが生成される。
app.use(function(req,res,next){next(createError(404));});
  • 最終的に、res.render(error)の行まで処理が進んで、エラー画面がブラウザに表示される。

app.use側でパスにマッチしても、処理先の router側に書かれているいずれのパスにもマッチしない場合は、あたかも app.useで何もマッチしなかったかのような挙動になる。

この例で、"/aaa" に対しても処理を行ないたい場合は、router側に "/" のパスに対する処理を書くか、app.js側にさらに "/aaa" に対する処理を書くかの、いずれかを行なう必要がある。

前者の場合は、下記のようにする。

app.js
varrouter=express.Router();router.use(`/bbb`,function(req,res){res.send('超Lチカ');});// 以下の行を追加router.use(`/`,function(req,res){res.send('ただのLチカ');});app.use('/aaa',router);

後者の場合は、下記のようにする。

app.js
varrouter=express.Router();router.use(`/bbb`,function(req,res){res.send('超Lチカ');});app.use('/aaa',router);// 以下の行を追加app.use(`/aaa`,function(req,res){res.send('ただのLチカ');});

いずれの場合でも、http://localhost:8000/aaaにアクセスしたときに「ただのLチカ」と表示されるようになる。同じ結果にはなるけど、パスの処理を階層化するという観点からは、前者のほうが良い気がする。

app.useの第二引数で routerを指定した場合は、マッチの処理そのものが router側に移譲される形になる。router側でのマッチ処理の結果、いずれのパスにもマッチしないと、あたかも app.use自体でマッチしなかったかのように、処理がスルーされるという結果になる。

このような、approuterのマッチの挙動を理解していないと、予想外のマッチが起こったり、どうしてもマッチしないというバグに繋りやすい。一方で、この仕組みを十分に理解していれば、app.jsの記述を減らして、パスのマッチの記述を router 側のコードに効率的に分散することができる。

余談: app.get や app.all でのパスのマッチ

app.getapp.allは、app.useとマッチのパスの処理がかなり異なっている。

たとえば、下記のコードで、curl でいろいろリクエストを送ると…

app.js
app.get('/aaa',function(req,res){res.send('getのLチカ');});app.use('/bbb',function(req,res){res.send('useのLチカ');});
$ curl -X GET http://localhost:8000/aaa
getのLチカ
$ curl -X GET http://localhost:8000/bbb
useのLチカ
$ curl -X GET http://localhost:8000/aaa/ccc
-> エラー
$ curl -X GET http://localhost:8000/bbb/ccc
useのLチカ

このようになる。これは、下記の理由による。

  • app.use('/aaa', ...)は "/aaa", "/aaa/bbb", "/aaa/ccc/bbb" のいずれにもマッチする。
  • app.get('/aaa', ...)"/aaa" にしかマッチしない。

次に、下記のようなコードを書いて curl で試してみる。

varrouter=express.Router();router.get('/',function(req,res){res.send('get:/');});router.use('/',function(req,res){res.send('use:/');});app.get('/get',router);
$ curl -X GET http://localhost:8000/get
use:/

router.get('/'、 ...)がスルーされて useでマッチしている。一方で、下記のようにすると get:/getが表示される。

varrouter=express.Router();router.get('/',function(req,res){res.send('get:/');});router.get('/get',function(req,res){res.send('get:/get');});router.use('/',function(req,res){res.send('use:/');});router.use('/get',function(req,res){res.send('use:/get');});app.get('/get',router);
$ curl -X GET http://localhost:8000/get
get:/get
$ curl -X GET http://localhost:8000/get/get
エラー

さらに、下記のように app.getapp.useにすると、'get:/' が表示される。

varrouter=express.Router();router.get('/',function(req,res){res.send('get:/');});router.get('/get',function(req,res){res.send('get:/get');});router.use('/',function(req,res){res.send('use:/');});router.use('/get',function(req,res){res.send('use:/get');});app.use('/use',router);
$ curl -X GET http://localhost:8000/use
get:/
$ curl -X GET http://localhost:8000/use/get
get:/get

これらのことから、app.getapp.useのパスのマッチの処理は、以下のように行なわれていていると推測できる。

  • app.useは、'/use' のパス記述で "/use" にも "/use/get" にもマッチする。
  • app.getは、'/get' のパス記述で "/get" にはマッチするが "/get/get" にはマッチしない。
  • app.useの第二引数の routerの中では、app側でマッチした文字列 (が取り除かれたかのような状態で扱われる。(router側では "/use/get" から先頭の "/use" が取り除かれて、"/get" であると見なされる)
  • ただし router.useに限り、 app.get( path, router )とされていても、app.use( path, router )とされていても、app側でマッチした文字列が URL から取り除かれているかのようにして、パスのマッチを行なう。
  • router.getでは、app.get( path, router )とされたか、app.use( path, router )とされたかでパスのマッチの挙動が変わる。

このように、複雑なことになっている。そもそも、get や all は、パスの処理を行なう router を呼びだすことは前提とされてない実装になってるんだと思う (ドキュメントの use 関数の記述にそんな雰囲気を感じる…むしろ明言してほしいところ)。

app.getの中で、パスの処理をするような router呼び出すと、上記のようにパスの評価で混乱がおこるので、全くオススメできない。app内で getを使う場合は「内部でパスの処理を行なわず、なおかつ next で処理を続けるようなミドルウェア(関数)」 (logger のような)の使用に限定しておいたほうが安全そうでだ。(下記参照)

なお、app.allapp.postなども app.getと同じ挙動をする。

(Express 5.x になったらこのあたりは整理されるのだろうか…)

余談: URL 中のファイル名を変数に読みこむ

マッチしたパスの一部を、変数として取り出すこともできる。

app.js
app.use('/:val',function(req,res){res.send(req.params.val);});

app.jsapp.use('/', ... )の記述を上記のように書きかえて、PORT=8000 yarn statとしてから、http://localhost:8000/hogeにブラウザでアクセスすると、下記のように表示される。

パスの中に :valと記述しておくと、その部分にマッチした文字列をプログラム中から req.params.valという変数で参照できるようになる。変数名は valでなくても良い。変数名には英数文字とアンダースコア _が使える。このような、URL の中から文字列を取りだせる仕組みのことは、ルートパラメータと呼ばれる。

ルートパラメータは、パス表記の中に複数書くこともできる。たとえばこんな感じ。

app.js
app.use('/aaa/:val/bbb/:file',function(req,res){res.send('val: '+req.params.val+', file: '+req.params.file);});

image.png

ルートパラメータを使うことで、URL で指定されたファイル名を読み出すようなこともできる。

詳しくは下記を参照のこと。
- https://expressjs.com/ja/guide/routing.html

つづく?

ESLint v7.0.0 の変更点まとめ

$
0
0

v6.8.0 | 次 (2020-05-22 JST)

ESLint 7.0.0がリリースされました。
多数の互換性のない変更、ルール追加、オプション追加、そしてバグ修正が含まれています。

以下は主要な変更点のまとめです。

ユーザー向けの変更:

プラグイン開発者向けの変更:

ツール開発者 (エディタ拡張等) 向けの変更:

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

🏢 日本語 Issue 管理リポジトリ
👫 日本語サポート チャット
🏢 本家リポジトリ
👫 本家サポート チャット


[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。

💥 互換性がない変更

Node.js 8.x/11.x のサポートを終了します

🔖 RFC044, #12700

Node.js 8.x は 2019-12-31 に、11.x は 2019-06-01 に寿命を迎えたため、ESLint もそれらのバージョンのサポートを停止しました。

ESLint 7.0.0 がサポートする Node.js のバージョンは ^10.12.0 || >=12.0.0となります。

動作を元に戻したい場合:

この変更は元に戻せません。
古い Node.js 環境をアップグレードするまで ESLint 7 を利用しないようにしてください。

overrides設定に一致するファイルを自動的にリント対象にします

🔖 RFC020, #12677

これまで、eslint libのようにディレクトリを指定したとき、ESLint はディレクトリ内の *.jsファイルをチェックしていました。Node.js が *.cjs/*.mjs拡張子を導入したり、TypeScript や Vue.js でも ESLint を使うようになったりしたので、この仕様は不便でした。

ESLint 7.0.0 からは *.jsに加えて overrides設定にマッチするファイルもチェックします。例えば、

.eslintrc.yml
overrides:-files:"*.js"extends:my-config-js-files:"*.ts"extends:my-config-ts

のような設定がある場合、eslint libコマンドは libディレクトリ内の *.tsファイルもチェックします。

なお、eslint lib/**のように Glob パターンを指定した場合は今まで通りに動作しますのでご注意ください。overrides設定にかかわらず Glob パターンにマッチする全てのファイルをチェックします。

プラグイン開発者へ:

あなたが管理するプラグインが *.js以外のファイルを対象にするルールを提供する場合、recommended設定に overridesを追加すると利用者は便利かもしれません。

動作を元に戻したい場合:

今まで通り overrides設定にかかわらず *.jsだけをチェックしたい場合は、コマンドに --ext jsを付与してください。--ext CLI オプションはこの新しい動作を上書きします。

--config CLI オプションによる設定ファイル内の相対パスの扱いが変わります

🔖 RFC037, #12887

ignorePatterns, overrides[].files, overrides[].excludedFiles設定に相対パスがあった場合、その相対パスは設定ファイルの場所を基準に解決されます (共有設定内のそれら相対パスは、利用元の.eslintrc.*ファイルの場所を基準に解決されます)。

ESLint 7 でも基本的には同様です。しかし、--config CLI オプションで読み込んだファイルのみ、それら相対パスは CWD を基準に解決されるようになります。これは CWD にある仮想的な .eslintrc.*ファイルから extendsされたのと同じ動作になります。

動作を元に戻したい場合:

この変更は元に戻せません。
もし、この変更が原因で意図しない挙動になった場合は、設定ファイル内の相対パスを書き換えてください。

--ignore-path CLI オプションによる無視ファイル内のパスの扱いが変わります

🔖 RFC037, #12887

.eslintignoreファイル内のパスは、そのファイルの場所を基準に解決されます。

ESLint 7 でも基本的には同様です。しかし、--ignore-path CLI オプションで読み込んだファイルのみ、それらパスは CWD を基準に解決されるようになります。

動作を元に戻したい場合:

この変更は元に戻せません。
もし、この変更が原因で意図しない挙動になった場合は、無視ファイル内のパスを書き換えてください。

プラグインの読込元ディレクトリが変わります

🔖 RFC047, #12922

ESLint 6 はすべての設定ファイルに書かれた plugins設定のプラグインを $CWD/node_modulesディレクトリから読み込みます。この動作のため、エディタ拡張等に適切な CWD を設定しなければプラグインを利用できず、Monorepo スタイルの開発などで不便でした。

ESLint 7 では、設定ファイルに書かれた plugins設定のプラグインは、各設定ファイルの場所を基準に読み込まれます。つまり、packages/alice/.eslintrc.ymlファイルに書かれたプラグインは packages/alice/node_modulesディレクトリから読み込まれます。

⚠️ 共有設定に書かれた plugins設定のプラグインは、それを extendsする利用元の .eslintrc.*ファイルの場所を基準に読み込まれます。そのため、共有設定からプラグインを利用する場合は引き続き peerDependenciesを利用する必要があります。

動作を元に戻したい場合:

以前と同様に $CWD/node_modulesディレクトリからすべてのプラグインを読み込みたい場合は --resolve-plugins-relative-to . CLI オプションを利用してください (末尾のドットに注意)。
--resolve-plugins-relative-to CLI オプションはこの新しい動作を上書きします。

デフォルトで無視するファイルが変わります

🔖 RFC051, #12888

ESLint 6 がデフォルトで無視するファイルは、以下のパターンにマッチするファイルでした。

  • .* (dotfiles)
  • /node_modules/** (CWD にある node_modulesディレクトリ内)
  • /bower_components/** (CWD にある bower_componentsディレクトリ内)

ESLint 7 では、以下のように変更されます。

  • .!(eslintrc.*) (.eslintrc.*設定ファイルを除く dotfiles)
  • /**/node_modules/** (node_modulesディレクトリ内)

動作を元に戻したい場合:

以前のように .eslintrc.*/bower_components/**を無視したい、またはサブディレクトリの node_modulesディレクトリを無視したくない場合、.eslintrc.*設定ファイルの ignorePatterns設定を利用してください。

.eslintrc.yml
ignorePatterns:-"!/*/**/node_modules/*"-".eslintrc.*"-"/bower_components/*"

ディレクティブコメントに説明を書けます

🔖 RFC033, #12699

ディレクティブコメントとは、/* eslint-disable */のような ESLint の動作変更を指示するためのコメントのことです。ルールを無効にする場合、その理由を明示しておくことは良いことです。しかし、ディレクティブコメントの理由を書くための標準的な方法はありませんでした。

ESLint 7 では、空白文字に囲まれた2つ以上のハイフン --を使ってディレクティブコメントに説明を追加できます。

/* eslint-disable a-rule -- このルールはバグっている */doSomething()/* eslint-disable another-rule
----------------
ここでは...(略
 */

動作を元に戻したい場合:

この変更は元に戻せません。
もし、既存のディレクティブコメントに--が含まれていて壊れてしまった場合は、その設定を設定ファイルの overrides等に移してください。

eslint:recommended設定が有効にするルールが増えます

🔖 #12920

新たに 3 つのルールが有効になります。

  • no-dupe-else-if ... if-else ifチェーンにおいて、前の if文の条件を満たすために絶対に真にならない if文を報告します。
  • no-import-assign ... import文で作成した変数への代入 (実行時エラーになる) を報告します。
  • no-setter-return ... セッター内にある値を返す returnを報告します。

動作を元に戻したい場合:

上記ルールを有効にしたくない場合は、設定ファイルに無効化する設定を追加してください。

extends:eslint:recommendedrules:no-dupe-else-if:"off"no-import-assign:"off"no-setter-return:"off"

複数のルールのチェックが厳しくなります

🔖 #12195, #12490, #12608, #12701, #12757, #12765, #12806, #12816, #12837, #12876, #12913, #12915, #12919, #12927

以下のルールはより多くのエラーを報告するようになりました。

  • accessor-pairs ... オブジェクト リテラルだけでなく、クラス メンバーもチェックするようになりました。
  • array-callback-return ... ES2019 で追加された flatMap()メソッドもチェックするようになりました。
  • computed-property-spacing ... オブジェクト リテラルだけでなく、クラス メンバーもチェックするようになりました。
  • func-names ... デフォルト エクスポートの関数宣言も名前が省略可能であるため、関数式と同じように名前の有無をチェックするようになりました。
  • no-constant-condition ... 埋込式がないか、全ての埋込式が定数であるテンプレートリテラルも定数であると認識するようになりました。
  • no-dupe-class-members ... 計算されたプロパティ名が定数だった場合は重複チェックをするようになりました。
  • no-extra-parens ... 代入演算子の左側についてもカッコの有無をチェックするようになりました。
  • no-implied-eval ... window.evalのようなグローバルオブジェクトのプロパティアクセスも認識するようになりました。
  • no-iterator ... 計算されたプロパティ名が定数だった場合も認識するようになりました。
  • [no-magic-number] ... BigInt リテラルも認識するようになりました。
  • no-proto ... 計算されたプロパティ名が定数だった場合も認識するようになりました。
  • no-restricted-modules ... 埋込式がないか、全ての埋込式が定数であるテンプレートリテラルも定数であると認識するようになりました。
  • quote-props ... プロパティ名としての BigInt リテラルも認識するようになりました。
  • radix ... parseInt()関数の第二引数が無効な数値 (2未満 or 36より大きい) の場合もエラーを報告するようになりました。
  • use-isnan ... case句の NaNもエラー報告するようになりました。
  • yoda ... BigInt リテラルとテンプレートリテラルも認識するようになりました。

他にもあるかも...

動作を元に戻したい場合:

この変更は元に戻せません。
もし、新しくエラーが報告された場合はコードを修正してください。

複数の Node.js/CommonJS 向けルールが廃止されます

🔖 #12898

Node.js/CommonJS 向けのルールが非推奨になりました。
代わりに eslint-plugin-nodeの対応するルールをご利用ください。

非推奨代わりのルール
callback-returnnode/callback-return
global-requirenode/global-require
handle-callback-errnode/handle-callback-err
no-mixed-requiresnode/no-mixed-requires
no-new-requirenode/no-new-require
no-path-concatnode/no-path-concat
no-process-envnode/no-process-env
no-process-exitnode/no-process-exit
no-restricted-modulesnode/no-restricted-require
no-syncnode/no-sync

動作を元に戻したい場合:

廃止されたルールは引き続き利用することができます。
ただし、廃止されたルールのコードは凍結され、今後はバグ修正や機能追加が行われません。早めの移行をお勧めします。

個人設定の利用がランタイム警告を出力します

🔖 RFC032, #12678

個人設定とは、OS のホームディレクトリ (例: /home/foo/, C:\Users\foo\) に配置された .eslintrc.*設定ファイルのことです。ESLint はプロジェクト内に設定ファイルが見つかると個人設定を (親ディレクトリであっても) 無視し、プロジェクト内に設定ファイルが見つからないと個人設定を (親ディレクトリでなくても) 利用します。

この個人設定の仕組みは ESLint 6.7.0 で非推奨になりました

ESLint 7 では、個人設定の仕組みを利用すると標準エラー出力に非推奨警告を出力するようになります。

動作を元に戻したい場合:

この変更は元に戻せません。
もし、引き続きホームディレクトリの設定を利用したい場合は、--config CLI オプションで利用する設定を明示的に指定してください。もしくは、ホームディレクトリ以下に作業ディレクトリを作ってください (ESLint は親ディレクトリからも設定ファイルを探すので)。

RuleTesterのチェックが厳しくなります

🔖 RFC025, #12096, #12955

RuleTesterがより多くの誤りを見つけるようになります。

  • ルールが非標準の node.start/node.endプロパティを参照していた場合にテストが失敗するようになります。代わりに node.rangeプロパティを利用してください。
  • ルールが自動修正を提供するにもかかわらず、自動修正のテスト (output) が書かれていない場合にテストが失敗するようになります。
  • 期待されるエラー内容を指示するerrorsプロパティ内に未知のプロパティがあったとき、テストが失敗するようになります (typo でテストできてなかったという事がなくなります)。

動作を元に戻したい場合:

この変更は元に戻せません。
もし、新たにテストが失敗するようになった場合は、そのルールの処理かテスト内容を更新してください。

新しい ESLintクラスが CLIEngineクラスを置き換えます

🔖 RFC040, #12939

Node.js アプリケーションから ESLint を利用するための API として CLIEngineクラスが提供されています。しかし、CLIEngineクラスは同期 API を提供するため、非同期の処理が必要な並列処理・ES Modules サポート・進捗表示などの機能追加の妨げになっていました。
また、CLIEngineという名前は最初に使うべきクラスであるとの印象を抱きにくく、ブラウザ向けの機能限定版 API である Linterクラスを先に試して「意図通りに動かない」と issue を作る人が多かったです。

ESLint 7 では、この CLIEngineクラスを廃止して、後継となる ESLintクラスを導入しました。ESLintクラスは CLIEngineと同等のメソッドを提供しますが、Promiseを返す非同期メソッドになっています。メソッドの対応は以下の通りです。

CLIEngineESLint
executeOnFiles(patterns)lintFiles(patterns)
executeOnText(text, filePath, warnIgnored)lintText(text, options)
getFormatter(name)loadFormatter(name)
getConfigForFile(filePath)calculateConfigForFile(filePath)
isPathIgnored(filePath)isPathIgnored(filePath)
static outputFixes(results)static outputFixes(results)
static getErrorResults(results)static getErrorResults(results)
static getFormatter(name)(削除)
addPlugin(pluginId, definition)pluginsコンストラクタ オプション
getRules()(未実装)
resolveFileGlobPatterns()(削除)

動作を元に戻したい場合:

現在 CLIEngineクラスは広く利用されているため、少なくとも1年間は削除されないはずです。引き続き利用することができます。
しかし、今後 CLIEngineのコードは凍結され、バグ修正や機能追加が行われません。例えば、ESLint 7.x では並列処理・ES Modules サポート・進捗表示の機能が追加される予定ですが、それらは ESLintクラスのみに実装され、CLIEngineクラスからは利用できません。早めの移行をお勧めします。

✨ 本体への機能追加

RuleTestersuggestionsテストでmessageId/dataを利用できます

🔖 #12635

RuleTestersuggestionsのテストにて、messageプロパティの代わりにmessageId/dataプロパティペアを用いてテストできるようになりました。

const{RuleTester}=require("eslint")construle=require("../../../lib/rules/a-rule")newRuleTester("a-rule",rule,{valid:[],invalid:[{code:"foo",errors:[{messageId:"error",data:{name:"foo"},suggestions:[{messageId:"suggest",data:{name:"foo"},output:"bar"}],}],},],})

💡 新しいルール

default-case-last

🔖 #12668

default句をswitch文の末尾以外に書くことを禁止するルールが追加されました。

/*eslint default-case-last: error *///✘ BADswitch(foo){default:breakcase0:breakcase1:break}

» Online Demo

no-restricted-exports

🔖 #12546

特定の名前の ES Modules エクスポートを禁止するルールが追加されました。

/*eslint no-restricted-exports: [error, { restrictedNamedExports: [foo, bar] }] *///✘ BADexportfunctionfoo(){}letbar=0export{bar}

» Online Demo

no-useless-backreference

🔖 #12690

正規表現中の無意味な後方参照を禁止するルールが追加されました。

/*eslint no-useless-backreference: error *///✘ BADleta=/foo\1(bar)/

» Online Demo

🔧 オプションが追加されたルール

array-callback-return: checkForEach

🔖 #12646

forEach()メソッドのコールバックについてもreturn文をチェックするオプションが追加されました。

/*eslint array-callback-return: [error, { checkForEach: true }] *///✘ BADlist.forEach(x=>{returnx*2})list.forEach(x=>x*2)//✔ GOODlist.forEach(x=>{x*2})

» Online Demo

array-element-newline: ArrayExpression and ArrayPattern

🔖 #11796

配列リテラルと分割代入の配列パターンとで別々の設定を行うためのオプションが追加されました。

/*eslint array-element-newline: [error, { ArrayExpression: always, ArrayPattern: never }] *///✘ BADletfoo=[1,2]let[a,b]=foo

» Online Demo

indent: offsetTernaryExpressions

🔖 #12556

三項演算子の2項目と3項目のインデントをつけるオプションが追加されました。

/*eslint indent: [error, 4, { offsetTernaryExpressions: true }] *///✘ BADletfoo=cond?()=>{doSomething()}:()=>{doSomething()}//✔ GOODletbar=cond?()=>{doSomething()}:()=>{doSomething()}

» Online Demo

indent: outerIIFEBody:"off"

🔖 #12706

トップレベルの即時関数呼び出しのインデントを検証しないオプションが追加されました。

/*eslint indent: [error, 4, { outerIIFEBody: "off" }]*///✔ GOOD(function(){functionfoo(x){returnx+1;}})();

» Online Demo

no-extra-boolean-cast: enforceForLogicalOperands

🔖 #12734

論理式のオペランドでも不要な明示的な boolean型変換を禁止するオプションが追加されました。

/*eslint no-extra-boolean-cast: [error, { enforceForLogicalOperands: true }] *///✘ BADif(!!bar||Boolean(baz));

» Online Demo

no-magic-numbers: BigInt in the ignore option

🔖 #12701

ignoreオプションが新たに文字列を受け入れるようになりました。各文字列は BigInt リテラルとして有効な文字列である必要があります。

/*eslint no-magic-numbers: [error, { ignore: ["123n"] }] *///✔ GOODif(foo===123n);//✘ BADif(foo===456n);

» Online Demo

no-void: allowAsStatement

🔖 #12613

式文の最初に書くvoid式を許可するオプションが追加されました。

/*eslint no-void: [error, { allowAsStatement: true }] *///✔ GOODvoiddoSomething()void(async()=>{doSomething()})().catch(handleError)//✘ BADleta=voiddoSomething()

» Online Demo


Viewing all 8922 articles
Browse latest View live