新しいことにはウェルカム

[技術] [電子工作] [ガジェット] [ゲーム] のメモ書き

ESP8266の動作を安定させる方法

ESP8266に電源をつないでも、うまく起動しない事がちょいちょい発生していました。

調べてみるとESP8266の動作を安定させる方法についての記事を発見。

今まで、回路は秋月電子のESP-WROOM-02開発ボードを参考に作っていたので問題ないだろうと思っていたのですが、「電源→ENピン」の順でHIにならないといけないらしく、安定してそうなるようにするには、ENピンに抵抗とコンデンサを接続する必要がありました。

実際にやってみると起動に失敗することがなくなりました!

上記を踏まえたESP8266の回路は下記になります。

ESP8266の安定動作回路

シェルスクリプトでコマンドを並列実行する方法

ファイル毎に記録されたデータのバッチ処理を、シェルスクリプトで実行したりしています。

ファイルが大量にあった場合、1つのファイルの処理が終わるのを待ってから次のファイルの処理を行うといった、逐次処理をしているととても時間がかかることがありました。

何かいい方法はないかなぁと調べていたら「GNU parallel」という、シェルスクリプトでコマンドを並列実行できるものがあったので、今はそれを使っています。

GNU parallel

インストール

パッケージで配布されているのでインストール

$ sudo apt-get parallel

使い方

色々な書き方ができるのですが、一番汎用的な引数一覧をパイプで渡す書き方を紹介します。

<引数一覧> | parallel '<実行コマンド>'

<実行コマンド>で、引数が埋め込まれる箇所は{}で記載します。

$ seq 5 -1 1 | parallel --no-notice 'sleep {} && echo {}'
4
5
2
3
1
  • parallelを呼び出すと、毎回注意文が表示されるので、--no-noticeで表示しないようにします。
  • {}の位置に引数が埋め込まれます。
  • デフォルトでは、CPUのコア数だけ同時並列実行されます。
    • 手持ちの実行マシンはコア数が2だったので、結果は2つづつ同時並列実行され「4 5」「2 3」「1」の順で表示されました。

同時並列実行数

同時並列実行数は-jオプションで指定できます。

$ seq 5 -1 1 | parallel --no-notice -j 10 'sleep {} && echo {}'
1
2
3
4
5

同時並列実行数を10にしたので、5個全てが同時並列実行され、早く終わる1から順に表示されます。

実行結果取得

実行結果は変数$?に格納(0成功、1失敗)されます。

全て成功

$ echo -e "1\n2\n3" | parallel --no-notice 'sleep {} && echo {}'
1
2
3

$ echo $?
0

どれか1つでも失敗

$ echo -e "1\nA\n3" | parallel --no-notice 'sleep {} && echo {}'
sleep: `A': 無効な時間間隔です
Try 'sleep --help' for more information.
1
3

$ echo $?
1

AWS CloudFormation を使ってみたが、人が扱える限界を超えていた話

タイトルそのままですが、AWS CloudFormation を使ってみた感想です。

きっかけ

AWS LambdaをURLから呼び出す時は、AWS API GatewayのGUIを使って、「このURLのこのメソッドの時はこのLambdaを呼び出す」などのマッピングをポチポチやっていくのですが、これが地味に面倒です。

最初のうちは物珍しくて面白いのですが、同じ様な作業の繰り返しになり、そのうち飽きて面倒になってきます。

GUIでの操作は手作業のため、何かと抜け・漏れ・ミスが発生しやすく、また、作業手順を記録して後で再現するのも注意を要します。

そんな時こそ AWS CloudFormation を使えばいいのでは!?と思い試してみました。

AWS CloudFormationとは?

AWSの各種サービスは、GUIやコマンドラインツールで作成・設定するのですが、サービスによってやり方はマチマチです。

CloudFormationはそれらの設定を、統一されたフォーマットのJSON・YAMLのファイル(=テンプレート)にして、そのテンプレートを元にサービスを一気に作成・更新することができます。

サービスをコマンドラインツールで直接作成するのに比べ、CloudFormationでは抽象度が上がっており、複数の異なる種類のサービスを関連付けてまとめて作成したり、テンプレートの一部をパラメータ化して、CloudFormation実行時にパラメータ値を解決するようにできたりします。

手順と言いたいところですが…

LambdaとAPI Gatewayを作成するサンプルテンプレート(概要)を記載しました。(かなり長いので一番最後に載せました)

記事用に色々省略して数百行…。実際は千行超えているので、本格的にやるとなると数千行レベルになるかと思います……。

人が編集できる量の限界を超えています……。 というお話でした……。

テンプレートを簡単に説明すると

CloudFormationでは、Lambda・API Gateway・EC2等、作成する個々のサービスを「リソース」と呼び、「Resources」で定義します。

どの種類のリソースなのかは「Type」で指定し、そのリソースの設定は「Properties」で定義します。

どいうリソースがあって、どういうパラメータを持つかはマニュアルに記載されているので、テンプレートの編集の際は、マニュアルと各リソースのGUIを参照しながら設定値を入れていくことになるかと思います。

そして、CloudFormationでテンプレートを指定して実行すると、定義されたリソースが作成されます。また、テンプレートを変更して再度CloudFormationを実行すると、作成済みのリソースに変更が適用されるという仕組みです。

注意点

CloudFormationを使用するにあたり、結構重要な注意点があります。

  • リソース作成後、CloudFormation以外でそのリソースの設定を変更すると整合性が取れなくなり、その後CloudFormationによる変更ができなくなることがあります。

  • CloudFormationは作成・更新だけでなく、削除も行えます。テンプレート単位でCloudFormationが作成されるので、それを削除すると、テンプレートに記載されていたリソース全てが削除されます。

  • 設定変更が変更レベルを超えている場合は、リソースの置換、つまり、リソースを削除して、新たに一からリソースを作成し直します。例えばリソースとしてデータベースを作成していた場合、変更のつもりが、データもろとも削除されてしまうことがあるので注意が必要です。

感想

AWSのリソースを生成するためのローレベルAPIといった印象です。

CloudFormationを使えば全てのリソースを生成できますが、CloudFormationを直接操作することはあまりせず、何かしらジェネレーターを介して間接的に使うたぐいのものかなぁと感じました。

例えばきっかけとなった「Lambda+API Gateway」に関しては、結局Serverless Frameworkを使うことにしたのですが、Serverless Frameworkも、CloudFormationのテンプレートを生成して、CloudFormationを使ってLambda+API Gatewayを作成しています。

一旦CloudFormationを使うと決めたなら、変更も必ずCloudFormationを介して行うようにするのがベターかなと感じました。また、リソースが作り直されても問題ないような設計・運用が必要だなと感じました。

DockerやLambdaの様に、更新で置き換えるスタイルをよく見かけるので、今風と言えば今風なのかも知れません。

CloudFormationには「CloudFormation デザイナー」というGUIでテンプレートが作成できるツールがあります。パット見かっこいいので、これを使えばドラッグアンドドロップで簡単にテンプレートが作れるのではと期待してしまうのですが、できることは限られていて、テンプレートの手編集は必須です。

AWS CloudFormation デザイナー

更に、CloudFormation デザイナーが出力するテンプレートは自動整形され、かつ、GUI情報が付加されていて読みにくくなってしまうので、使用はオススメしません。

ジェネレーターを介して使用する場合でも、そのジェネレーターがサポートしていない設定を行うために、テンプレートをいじる必要があったりするので、TypeとPropertiesのマニュアルの場所は知っておいて損はないかと思います。

サンプルテンプレート

長いのでかなり省略しています。

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Resources": {
        "ldLogin": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "FunctionName": {
                    "Fn::Sub": "cf-test-lambda-login-${paramStage}"
                },
                "Handler": "login.handler",
                "Runtime": "nodejs8.10",
                "Code": {
                    "S3Bucket": "cf-test",
                    "S3Key": {
                        "Fn::Sub": "lambda/${paramLambdaZipFileName}"
                    }
                },
                "Role": "arn:aws:iam::xxxx:role/cf-test-role-lambda-${paramStage}"
            }
        },
        "api": {
            "Type": "AWS::ApiGateway::RestApi",
            "Properties": {
                "Name": {
                    "Fn::Sub": "cf-test-api-${paramStage}"
                },
                "EndpointConfiguration": {
                    "Types": [
                        "REGIONAL"
                    ]
                }
            }
        },
        "ptLogin": {
            "Type": "AWS::ApiGateway::Resource",
            "Properties": {
                "ParentId": {
                    "Fn::GetAtt": [
                        "api",
                        "RootResourceId"
                    ]
                },
                "PathPart": "login",
                "RestApiId": {
                    "Ref": "api"
                }
            }
        },
        "mtLoginGET": {
            "Type": "AWS::ApiGateway::Method",
            "Properties": {
                "HttpMethod": "GET",
                "RestApiId": {
                    "Ref": "api"
                },
                "Integration": {
                    "Type": "AWS",
                    "Uri": {
                        "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ldLogin.Arn}/invocations"
                    },
                    "PassthroughBehavior": "NEVER",
                    "IntegrationHttpMethod": "GET",
                    "RequestTemplates": {
                        "application/json": {
                            "Fn::Sub": "${paramMappingTemplate}"
                        }
                    },
                    "IntegrationResponses": [
                        {
                            "ResponseTemplates": {
                                "application/json": ""
                            },
                            "StatusCode": "200",
                            "ResponseParameters": {
                                "method.response.header.Access-Control-Allow-Origin": "'*'"
                            }
                        }
                    ]
                },
                "ResourceId": {
                    "Ref": "ptLogin"
                },
                "MethodResponses": [
                    {
                        "ResponseModels": {
                            "application/json": "Empty"
                        },
                        "ResponseParameters": {
                            "method.response.header.Access-Control-Allow-Origin": true
                        },
                        "StatusCode": "200"
                    }
                ]
            },
            "DependsOn": [
                "ldLogin"
            ]
        },
        "mtLoginOPTIONS": {
            "Type": "AWS::ApiGateway::Method",
            "Properties": {
                "ResourceId": {
                    "Ref": "ptLogin"
                },
                "RestApiId": {
                    "Ref": "api"
                },
                "AuthorizationType": "NONE",
                "HttpMethod": "OPTIONS",
                "MethodResponses": [
                    {
                        "StatusCode": "200",
                        "ResponseModels": {
                            "application/json": "Empty"
                        },
                        "ResponseParameters": {
                            "method.response.header.Access-Control-Allow-Headers": true,
                            "method.response.header.Access-Control-Allow-Methods": true,
                            "method.response.header.Access-Control-Allow-Origin": true
                        }
                    }
                ],
                "Integration": {
                    "Type": "MOCK",
                    "RequestTemplates": {
                        "application/json": "{\"statusCode\": 200}"
                    },
                    "IntegrationResponses": [
                        {
                            "ResponseTemplates": {
                                "application/json": ""
                            },
                            "StatusCode": "200",
                            "ResponseParameters": {
                                "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'",
                                "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'",
                                "method.response.header.Access-Control-Allow-Origin": "'*'"
                            }
                        }
                    ]
                }
            }
        },
        "stage": {
            "Type": "AWS::ApiGateway::Stage",
            "Properties": {
                "StageName": {
                    "Ref": "paramStage"
                },
                "DeploymentId": {
                    "Ref": "deployment"
                },
                "RestApiId": {
                    "Ref": "api"
                }
            },
            "DependsOn": [
                "mtLoginOPTIONS",
                "mtLoginGET"
            ]
        },
        "deployment": {
            "Type": "AWS::ApiGateway::Deployment",
            "Properties": {
                "RestApiId": {
                    "Ref": "api"
                }
            },
            "DependsOn": [
                "mtLoginOPTIONS",
                "mtLoginGET"
            ]
        }
    },
    "Parameters": {
        "paramStage": {
            "Type": "String",
            "Default": "cloudformation"
        },
        "paramLambdaZipFileName": {
            "Type": "String"
        },
        "paramMappingTemplate": {
            "Type": "String",
            "Default": "{}"
        }
    },
    "Outputs": {
        "url": {
            "Value": {
                "Fn::Sub": "https://${api}.execute-api.${AWS::Region}.amazonaws.com/${paramStage}"
            }
        }
    }
}

Kubernetes(GKE)でLet's Encryptを自動更新する方法

Kubernetes(GKE)でLet's Encryptを自動更新するのに「cert-manager」+DNS認証を使うと、サービス側での処理なしで自動更新できて便利だったので、その導入手順メモです。

手順(cert-managerインストールまで)

Helmインストール

「cert-manager」はKubernetesのパッケージマネージャ「Helm」を使ってインストールするため、まず Helmのインストールと、Helmのアカウントを作成します。 (詳しくは https://github.com/ahmetb/gke-letsencrypt に記載されています)

Helmクライアントをインストール

  1. https://github.com/kubernetes/helm/releases からダウンロードして解凍
  2. パスを通して実行可能にする
mv linux-amd64/helm /usr/local/bin/helm
sudo chown root:root /usr/local/bin/helm
sudo chmod 755 /usr/local/bin/helm

Helmアカウント(tiller)をKubernetesに作成、権限付与、およびHelmアップデート

kubectl create serviceaccount -n kube-system tiller
kubectl create clusterrolebinding tiller-binding \
    --clusterrole=cluster-admin \
    --serviceaccount kube-system:tiller
helm init --service-account tiller 
helm repo update

cert-managerインストール

helm install \
    --name cert-manager \
    --namespace kube-system \
    stable/cert-manager

全体の流れ

以上でcert-managerのインストールまで完了します。次の手順に進む前に、ざっくりと全体の流れを説明します。

cert-managerの仕組み

cert-managerは大きく「Issuer」と「Certificate」で構成されています。 「Certificate」は主にドメイン・証明書に関する設定が記載されており、「Issure」は証明書取得の手段やアカウントに関する設定が記載されています。 「Certificate」の証明書の取得を「Issuer」で実行し、「Certificate」で定義されているシークレットに保存することにより、証明書を利用できるようにします。

証明書利用の仕組み

上記で取得した証明書はシークレットとしてKubernetesに保存されます。 そして、そのシークレットをIngressから参照することにより証明書を利用します。 (Ingressを用いた証明書の利用方法については別記事「Kubernetes(GKE)でHTTPS通信する方法(Ingress編)」にまとめました)

Let's Encrypt DNS認証の仕組み

Let's Encrypt DNS認証では、Let's Encryptは、DNSレコードに指定した値が書き込まれたかをチェックして、請求者がそのドメインの所有者かをチェックします。 そこで、cert-managerは、GCPのDNSレコードを編集できるサービスアカウントをIssuerと紐付けることによって、IssuerがDNSレコードを編集できるようにし、Let's Encryptからのチェックをパスできるようにします。

手順(残り)

GCPサービスアカウント作成

DNSレコードを編集できるサービスアカウントを作成します

  1. [GCP]-[IAMと管理]-[サービスアカウント]-[サービスアカウントを作成]で任意のアカウントを作成
  2. [GCP]-[IAMと管理]-[IAM]-[追加]で作成したアカウントを追加(役割を[DNS]-[DNS管理者]にする)
  3. [GCP]-[APIとサービス]-[認証情報]-[認証情報を作成]-[サービスアカウントキー]でキーJSONファイルを作成(サービスアカウントを選択してキータイプをJSONにしてキーファイルを保存)

cert-managerからキーファイルの内容を参照できるよう、Kubernetesのシークレットにキーファイルを登録します。

kubectl create secret generic <シークレット名> \
    --from-file=key.json=<キーファイルパス>

Issuer作成

issuer.yaml

apiVersion: certmanager.k8s.io/v1alpha1
kind: Issuer
metadata:
  name: letsencrypt-issuer
  namespace: default
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <Let's Encryptに登録するemail>
    privateKeySecretRef:
      name: letsencrypt-issuer
    dns01:
      providers:
        - name: clouddns
          clouddns:
            serviceAccountSecretRef:
              name: <DNSサービスアカウントのキーファイルのシークレット名>
              key: key.json
            project: <GCPプロジェクト名>
kubectl apply -f issuer.yaml

Certificate作成

certificate.yaml

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: example-com-certificate
  namespace: default
spec:
  secretName: example-com-certificate    # Ingressから参照するシークレット名
  issuerRef:
    name: letsencrypt-issuer  # Issuer名
    kind: Issuer
  commonName: example.com  # ドメイン名
  dnsNames:
  - example.com  # ドメイン名
  acme:
    config:
    - dns01:
        provider: clouddns  # Issuerのprovider名
      domains:
      - example.com  # ドメイン名
kubectl apply -f certificate.yaml

確認

kubectl describe certificate,issuer,clusterissuer --all-namespaces

で状態をチェックできます。エラーが出ていないようなら証明書の取得が成功しています。 エラーが出ているようなら、下記の方法でcert-managerのログをチェックして原因究明します。

  • [GCP]-[Kubernetes Engine]-[ワークロード]-<フィルターOFF>-<cert-manager>-[管理対象ボット]-<ボット名>-[ログ]-[Container Logs]

証明書の利用

Ingressのサンプルを記載します。 tlsのシークレットを、Certificateで設定したシークレット名にすることにより、IngressとCertificateの証明書を紐付けます。 (Ingressを用いた証明書の利用方法については別記事「Kubernetes(GKE)でHTTPS通信する方法(Ingress編)」にまとめました)

ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
  annotations:
    kubernetes.io/ingress.allow-http: "false"
    kubernetes.io/ingress.global-static-ip-name: "<static ip名>"
spec:
  tls:
  - secretName: example-com-certificate
  backend:
    serviceName: nginx
    servicePort: 80

補足説明

トライアルアンドエラーを繰り返していると、Let's Encryptの制限に引っかかることがあるので、はじめは本番ではなくステージングでテストした方がよいかと思います。 ステージングは「issuer.yaml」の「server」を「https://acme-staging-v02.api.letsencrypt.org/directory」にするとなります。 ステージングで取得した証明書は怪しいサイトとなりますが、アクセスは可能です。

また、「cert-manager」のアンインストール、再インストールは下記で行えます。

helm list  # インストールされているパッケージ一覧
helm delete <cert-manager名>  # <cert-manager>削除
helm install \
    --replace \    # 同じ名前で再インストールするには「--replace」をつけます
    --name <cert-manager名> \
    --namespace kube-system \
    stable/cert-manager

参考情報

スマホ用手袋を買った感想

持っている手袋が大きすぎて使い勝手が悪かったので、新しい手袋を買いました。

せっかくなので、前から興味があったスマホ用の手袋にしてみました。

無印良品 ブークレタッチパネル手袋 フリーサイズ・黒 無印良品 ブークレタッチパネル手袋 フリーサイズ・黒

全ての指がタッチパネル対応になっているものと、親指と人差し指だけ対応になっているものがあるのですが、親指と人差し指でしか操作しないので、親指と人差し指だけのものにしました。

使用感

反応したりしなかったりで、ストレスがたまり、実用的ではないですね。

手袋の厚みと、実際に皮膚がタッチパネルに触っていないのもあって、どこにタッチしているのかが分からず、細かい操作は難しいです。

慣れの問題かなと、1周間ほど色々試しながら使ってみましたが、操作感をつかむことはできませんでした。

むしろ使っているうちに、毛糸のモケモケが増えていって反応が鈍くなっていきました。

結論

普通の手袋として使用することにしました。

タッチパネル対応になっている箇所は濃いグレーになっているので、そこが目立たない黒にして良かったです。

もし購入を検討している方がいらっしゃいましたら、実際に店頭で試してみるのが一番いいかと思います。

店頭で試して違和感を感じるなら、購入してもその違和感は続くと思います。

Kubernetes(GKE)でHTTPS通信する方法(Ingress編)

Google Cloud Platform のKubernetes(GKE)でHTTPS通信する方法についてのまとめです。

はじめに

Kubernetesが外部と通信できるようにするには

  • Serviceによる方法
  • Ingressによる方法

の2通りのやり方があります。

Serviceによる方法では、外部IPはGCPのロードバランサーに接続され、外部からのデータはロードバランサーの機能により、KubernetesのServiceに直接届けられます。ロードバランサーは単純にデータを分配するだけで、外部からのデータはそのままServiceに流されるため、HTTPSの処理はデータを受け取ったKubernetesの内部のPodで処理する必要があります。

Ingressによる方法では、外部IPはServiceによる方法と同様、GCPのロードバランサーに接続されるのですが、そこからIngressというKubernetesのルーティングを行う所に接続され、Ingressは設定に応じて、KubernetesのServiceにデータを分配します。Ingressは内部的にはNginxで作成されており、外部からのHTTPSをHTTPに変換してServiceに流すといった、通信内容に応じたルーティングが可能になります。

本記事では、Ingressを使ったHTTPS通信を行い、IngressでHTTPSをHTTPに変換し、KubernetesにはHTTPを送るように設定します。

手順

外部IP

Ingressの外部IPは「global」である必要があり、「regional」では動作しません。そこで「global」なstatic IPを用意します。

シークレットの作成

証明書と鍵のセットをシークレットとしてKubernetesに登録し、そのシークレットを参照する形でIngressを作成します。 まずは、シークレットの作成を行います。

kubectl create secret tls <シークレット名> \
    --key <鍵ファイル> \
    --cert <証明書ファイル>

Ingressの作成

ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
    name: ingress
    annotations:
        kubernetes.io/ingress.allow-http: "false"
        kubernetes.io/ingress.global-static-ip-name: "<外部IP名>"
spec:
    tls:
    - secretName: <シークレット名>
    backend:
        serviceName: <接続先Service名>
        servicePort: <接続先ポート番号>

(HTTPSのみを許可し、HTTP通信は不可としています)

「ingress.yaml」でIngressを作成

kubectl create -f ingress.yaml

ヘルスチェックの確認

以上でIngressが作成され、外部からHTTPSで通信できるようになります。 Ingressが作成されると、Kubernetesはロードバランサーを作成し、ヘルスチェックの設定も行います。

  • [GCP]-[ネットワークサービス]-[負荷分散]-[<ロードバランサー名>]-[ヘルスチェック]

でヘルスチェックの詳細が見れます。

このヘルスチェックをパスしないと、Ingressへデータが流されず、動作しませんので注意が必要です。

デフォルトのヘルスチェックはHTTPで「/」にアクセスして200が返ってくるかをチェックしていますので、接続先ServiceでHTTPで「/」にアクセスできるようにしておく必要があります。

JavaScriptのasync/awaitをもう少しちゃんと理解する

今までJavaScriptのasync/awaitを、理解が曖昧なまま何となく使っていて、うまく行かない時はPromiseを使ったりしていました。

しかし、最近はasync/awaitが使われているのをよく目にするようになってきたため、もう少しちゃんと理解しないといけないかなぁと思いはじめ、その理解メモです。

async/awaitの基本

まずは基本から。async/awaitを使う目的は、非同期処理を同期処理っぽく、順番に記述できるところでしょうか。

基本的な機能は下記のようなところです。

  • Promiseを返す関数をawaitで呼び出すと、次の処理には移らず、Promiseが返るまで待っていてくれる。
  • awaitは、PromiseからPromiseに格納した実値を取り出して返してくれる。
  • Promiseを返す関数がrejectした場合は、awaitは例外を返す。
  • awaitの例外はtry/catchのcatchで受ける。
  • awaitを使う関数はasync関数でなければいけない。

コードにすると下記のようになります。よく見るパターンです。

// Promiseを返す関数
function test_promise(v){
    return new Promise((resolve, reject)=>{
        if(v){
            resolve("OK");
        } else {
            reject("ERROR");
        }
    });
}

(async ()=>{
    try{
        const ret = await test_promise(true);
        console.log(ret);    // 「OK」と表示される
    }catch(err){
        console.log(err);    // 「ERROR」と表示される
    }
})();

async関数とは?

async関数とは要約すると「Promiseを返す関数の別の書き方」ってとこでしょうか。

  • return ~が、Promiseでいうところのresolve(~)に相当する。
  • throw ~が、Promiseでいうところのreject(~)に相当する。

例えば先程のPromiseを返す関数test_promise()をasyncで書き直すと下記のようになります。

async function test_async(v){
    if(v){
        return "OK";
    } else {
        throw "ERROR";
    }
}

async関数の注意点

じゃあPromiseを全てasync関数に置き換えらるかというと残念ながら無理です…。

async関数の中で非同期関数を呼び、その中からPromise・throwを返すことができません。

例えば

function test_promise(v){
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            if(v){
                resolve("OK");
            } else {
                reject("ERROR");
            }
        }, 1000);
    });
}

のようなPromiseを返す関数を

async function test_async(v){
    setTimeout(()=>{
        if(v){
            return "OK";    // test_async()の戻り値じゃない
        } else {
            throw "ERROR";
        }
    }, 1000);
}

と書いてもうまくいきません。

無理やりasyncでも成立するようにさせると下記のようになります。

async function test_async(v){
    try{
        const ret = await new Promise((resolve, reject)=>{
            setTimeout(()=>{
                if(v){
                    resolve("OK");
                } else {
                    reject("ERROR");
                }
            }, 1000);
        });
        return ret;
    }catch(err){
        throw err;
    }
}

もはや、Promiseでいいじゃんといった感じです。

async関数の中では、awaitを使い逐次処理していき、最後に明示的にreturn/throwで結果を返すのが、一番オーソドックスな使い方のようです。

asyncの中で使う関数全てがPromiseを返してくれるのだったなら、awaitだけでシンプルに書けるので、asyncを使うかPromiseを使うかはケースバイケースかなと思います。

Array.map()の中で非同期処理を行う

配列の各要素を使って非同期処理を行い、全ての処理が終わるまで待ってもらい、処理結果を配列で受け取るやり方です。このパターンはちょいちょい見かけます。

方法

  • mapから呼び出す関数をasyncにする。
  • async関数では実行結果をreturnで返す(async関数なので、returnで返された値はPromiseとなって返される)。
  • mapから生成された配列をPromise.allに格納する。
  • Promise.allをawaitで受ける。

コードにすると下記のようになります。

function test_promise(v){
    new Promise((resolve, reject)=>{
        setTimeout(()=>{
            resolve(v*10);
        }, 1000);
    });
}

(async ()=>{
    try{
        const array = [1, 2, 3];
        const ret = await Promise.all(
            array.map( async (v)=>{
                return test_promise(v);
        } ));
        console.log(ret);    // [10, 20, 30] と表示される
    }catch(err){
        console.log(err);
    }
})();

async/awaitがどう動作しているか見ていく前に、Promise.all()についておさらいを…

Promise.all()についておさらい

  • Promise.all()は、Promiseの配列を引数に取る。
  • 全てのPromiseが成功した場合は、then(val)で受けて、引数(val)は結果の配列となる。
  • 1つでも失敗した場合は、catch(err)で受けて、引数(err)は最初の失敗のreject(err)の値が入る。

以上を踏まえてコードを振り返ると、

  1. array.map()は各要素に対して非同期処理を実行し、その実行結果をPromiseとして返すので、array.map()はPromiseの配列となる。
  2. Promise.all()で上記のPromiseの配列を受ける。
  3. 全ての処理が成功した場合は、awaitがPromiseの配列を、結果の配列へと展開する。
  4. 1つでも処理が失敗になった場合は、awaitで例外が投げられて、catchで受ける。

の一連の処理が行われ、結果の配列がawaitから返され、retへと代入されます。

Array.forEach()の中で非同期処理を行う

Array.map()がうまくいったので、Array.forEach()でもうまくいくかも!?と思いたいところですが、結論から言うとできないです。

例えば、

function test_promise(v){
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            console.log(v);
            resolve();
        }, 1000);
    });
}

(async ()=>{
    try{
        const array = [1, 2, 3];
        await array.forEach( async (v)=>{
            await test_promise(v);
        });
        console.log("END");
    } catch(err){
        console.log(err);
    }
}

の結果は

1
2
3
END

とはならず

END
1
2
3

になります。

意図した挙動にするには、Array.forEach()は諦めて、地道にfor/ofを使うしかないです。

(async ()=>{
    try{
        const array = [1, 2, 3];
        for(const v of array){
            await test_promise(v);
        }
        console.log("END");
    } catch(err){
        console.log(err);
    }
}

ザックリですが、以上である程度async/awaitは使えるようになるのではないでしょうか。

感想

async/awaitを使えばPromiseは必要無くなるかと思ったのですが、そうはいかないようですね。

awaitで全体の見通しが良くなるのですが、並列処理できるところをついついawaitで逐次処理にしてしまって、速度が遅くなることもありますね。