AWSのメンテナンス[EC2編]

AWSからのメンテナンス通知

AWSからたまに送られてくる以下のような定期メンテナンス実施のアナウンスメール。

Dear Amazon EC2 Customer,

One or more of your Amazon EC2 instances is scheduled for maintenance on 2017-**-** for 2 hours starting at 2017-**-** 12:00:00 UTC UTC. During this time, the following instances in the ******* region will be unavailable and then rebooted:

************

Your instances will return to normal operations after maintenance is complete and all of your configuration settings will be retained. To continue normal operation and avoid any unavailability or reboots during this time, you can migrate the instances listed above to replacement instances. Replacement instances will not be affected by this scheduled maintenance. Otherwise, no action is required on your part.

以下省略

AWSではEC2群を頻繁にアップデートしており、多くのパッチやアップグレードの適用を行なっています。
今回も指定期間のうちにメンテナンスをするから、インスタンスが再起動されるよ。と言うお知らせでした。

再起動にかかる時間は通常、数分程度ですが少しでもサービスが止まってしまうと困る!と言う場合は指定のメンテナンス期間より前の任意の時間で再起動を行なってしまえばOKです。

メンテナンス対象はコンソールからも確認可能

コンソール>EC2>左側のメニュー上部の[イベント]

EC2メンテナンス01.png

現在予定されているメンテナンス対象が表示されます。

EC2メンテナンス02.png

予定の他に、進行中や完了ステータスになっているものも確認できます。
指定期間に実行されたインスタンスの状況も確認できるので安心です!

EC2メンテナンス03.png

システムリブートとインスタンスリブート

メンテナンスの内容によって、システムリブートとインスタンスリブート対応の仕方が少し異なります。
予定されたメンテナンスがどちらに該当するかはイベントの[イベントタイプ]から確認可能です。

システムリブートの場合は、stop → start
以下のように、インスタンスを完全に停止させてから開始します。

EC2メンテナンス04.png

インスタンスリブートの場合は、再起動でOKです。
が、stop → startの対応しておけばより確実。

続きを読む

terraform管理下にないインフラをterraformingしようとする場合に遭遇しやすい(?)Plan error : Resource ‘data.terraform_remote_state.not_immutable’ does not have attribute ‘vpc-xxx-yyy’ for variable ‘data.terraform_remote_state.not_immutable.vpc-xxx-yyy’

TL;DR

terraform管理下にないインフラをterraformingしようとする場合、
いわゆるStagingなどと呼ばれる環境で本番稼働環境を再現してから、本番環境に適用したいと思うわけです。
VPCやSubnetのような、壊すと大変なnot immutableなりソースはterraform importを駆使して、現行環境を壊さないよう、そーーーっとterraform管理下に引き込みます。(いずれQiita書く)
そしていざimmutableなりソースを–targetオプションを使いながら、ひとつひとつ本番環境に適用する際に遭遇しがちな下記エラー

$ terraform plan -out=apply.plan --target=module.compute.hoge.fuga

..(snip)..

Error: Error running plan: 1 error(s) occurred:

* module.compute.var.vpc-xxx-yyy: Resource 'data.terraform_remote_state.not_immutable' does not have attribute 'vpc-xxx-yyy' for variable 'data.terraform_remote_state.not_immutable.vpc-xxx-yyy'

を(強引に)解決するお話。

issueもあがっております。
https://github.com/hashicorp/terraform/issues/12316

環境

$ terraform version
Terraform v0.10.8

ディレクトリ構成

Terraform Best Practices in 2017

immutableなリソースから、VPCなどのnot immutableなリソースのremote stateを参照しています。

backend.tf
data "terraform_remote_state" "not_immutable" {
  backend = "s3"
  config {
    bucket = "bucket_name"
    key    = "env:/${terraform.env}/tfstate/not_immutable/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

対処方法

必要なresource定義を、手で書く or Stagingなどのterraform化検証環境のtfstateからコピペして、リソースIDを本番環境のものに書き換える

まずは最新のtfstateをremoteからpull。

$ terraform state pull > ./terraform.tfstate.d/$(terraform workspace show)/terraform.tfstate

./terraform.tfstate.d/$(terraform workspace show)/terraform.tfstate
に必要なリソースを追記。

terraform.tfstateより抜粋
            "resources": {
                "data.terraform_remote_state.not_immutable": {
                    "type": "terraform_remote_state",
                    "depends_on": [],
                    "primary": {
                        "id": "2017-11-15 02:25:07.174946468 +0000 UTC",
                        "attributes": {
                            "backend": "s3",
            ..(snip)..
                            "config.%": "3",
                            "config.bucket": "bucket_name",
                            "config.key": "env:/prod/tfstate/not_immutable/terraform.tfstate",
                            "config.region": "ap-northeast-1",
                            "environment": "default",
                            "id": "2017-11-15 02:25:07.174946468 +0000 UTC",
                            "vpc-xxx-yyy.%": "5",
### 今回はここから
                            "vpc-xxx-yyy.subnet-private-a": "subnet-pra",
                            "vpc-xxx-yyy.subnet-private-c": "subnet-prc",
                            "vpc-xxx-yyy.subnet-public-a": "subnet-pua",
                            "vpc-xxx-yyy.subnet-public-c": "subnet-puc",
                            "vpc-xxx-yyy.vpc_id": "vpc-xxx-yyy-id"
### ここまでのresource idを本番環境のものに書き換えました。
                        },
                        "meta": {},
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": ""
                }
            },

tfstateをremoteへpush。

$ terraform state push ./terraform.tfstate.d/$(terraform workspace show)/terraform.tfstate

そして再度terraform plan。

$ terraform plan -out=apply.plan --target=module.compute.hoge.fuga

..(snip)..

Plan: X to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

This plan was saved to: apply.plan

To perform exactly these actions, run the following command to apply:
    terraform apply "apply.plan"

エラーは消えました。
めでたしめでたし。

続きを読む

Apexを使ってgoでAWS Lambda functionを作ってみる

はじめに

AWS Lambdaの管理ツールであるApexを使ってgoを実行する
LambdaFunctionの作り方と使い方のメモ。
割とapex.run に載ってる内容ではありますがが。

AWS Lambdaとは

いわゆるサーバーレスアーキテクチャの一種でFaas(Function as a Service)
公式

  • コードをアップするとAWSInfra上でサーバー用意したり、プロビジョニングしなくても実行する事ができるコンピューティングサービス。

    • 現状はjs,java,c#,pythonなどをサポートしてる(近々goもやるらしい)
    • apexだとNodeでgoを実行してくれるのでgoもいける
  • AWS内の他サービスで定義されたトリガーに紐付けてpubsubみたいな事もできる

    • s3,dynamoの変更通知を受け取ってゴニョゴニョしてあげたり、code deployのデプロイ完了のトリガーを使ってなにがし、みたいに。

簡単に言うとLambdaでサポートしてる言語なら、デプロイとか実行環境整備したりとか不要でEC2インスタンスわざわざ立てなくても
実行できるし、他のAWSサービスと連携しやすいよって感じ。

ウェブサーバーやバッチサーバーみたいなIaasを必要とするようなものではなく、イベントに引っ掛けて
ちょっとした処理をするくらいのものに使うのがいい感じ

インストール

apexのインストール

curl https://raw.githubusercontent.com/apex/apex/master/install.sh | sh

awscliのインストール

aws使うなら入れてる人が大半だと思いますが
apexの動作にaws credentialが必要なので…

公式の引用

curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
sudo python get-pip.py
sudo pip install awscli

sixの依存関係errorが出る場合は

 sudo pip install --ignore-installed awscli

あたりで無視しちゃうと早いです。

credentialの設定

公式の引用。
regionは自分の使ってるもので。

$ aws configure
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE #
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-west-2
Default output format [None]: json

プロジェクト作成

apex cliで雛形作成ができるのでサクッと作ってしまいましょう。

$ mkdir your_lambda_function_dir
$ cd your_lambda_function_dir
$ apex init

             _    ____  _______  __
            /   |  _ | ____ / /
           / _  | |_) |  _|    /
          / ___ |  __/| |___ /  
         /_/   __|   |_____/_/_

  Enter the name of your project. It should be machine-friendly, as this
  is used to prefix your functions in Lambda.

    Project name: xxx_notify

  Enter an optional description of your project.

    Project description: notification for xxx

  [+] creating IAM xxxx_function role
  [+] creating IAM xxxxx policy
  [+] attaching policy to lambda_function role.
  [+] creating ./project.json
  [+] creating ./functions

  Setup complete, deploy those functions!

    $ apex deploy

テンプレートな構成を作ってくれます。
terraform使ってない場合はIAMを求められるので、作るor指定が必要。

 tree
.
├── functions
│   └── hello
│       └── index.js
└── project.json

goでコードを書く

特にgoを使うための細かな設定などは不要です。apexできる子。
今回は受け取ったsnsのbodyをsqsに投稿するサンプルを。

  • .apexignoreでdeployしないファイルを指定する(バイナリをあげれば良いので*.goは無視)
cat .apexignore
*.go
  • snsで受け取ったbodyをsqsに送るコードを書く
import (
    "bytes"
    "encoding/json"
    "strings"

    apex "github.com/apex/go-apex"
    apexSns "github.com/apex/go-apex/sns"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
    "github.com/aws/aws-sdk-go/service/sqs"
)

func main() {
    apexSns.HandleFunc(func(event *apexSns.Event, ctx *apex.Context) error {
        body := event.Records[0].SNS.Message
        //bodyをsqsに送るよ
        awscli := sqs.New(sess, &aws.Config{Region: aws.String("ap-northeast-1")})
        queueUrl := "your_sqs_queue_url"
        params := &sqs.SendMessageInput{
            QueueUrl:    &queueUrl,
            MessageBody: &body,
        }
        _, err = awscli.SendMessage(params)
        return err
    })
}

こんな感じ。
apexでのgo実行は、プロセス立ち上げてnodeでgoのバイナリを実行してるだけなので
importとかはローカルで解決してれば大丈夫です。

aws-sdk-goがいい感じにIAMの権限を使ってくれるので
403とか出る場合はlambdaを実行するIAMのロールにsqsのアクセス権を与えてあげてください。

イメージ湧きづらい場合は
公式をみると良いです。

deployと実行

  • deploy
apex deploy function_name
  • 実行
apex invoke function_name
  • logを見る
apex logs function_name -f
/aws/lambda/*** 2017-11-15T06:05:30.238Z    fb0b2dbb-c9ca-11e7-afc4-6762e3c5815e    {"errorMessage":"hoge"} // handlerFuncで返したerrがprintされる
/aws/lambda/*** END RequestId: fb0b2dbb-c9ca-11e7-afc4-6762e3c5815e
/aws/lambda/*** REPORT RequestId: fb0b2dbb-c9ca-11e7-afc4-6762e3c5815e  Duration: 936.31 ms Billed Duration: 1000 ms    Memory Size: 128 MB M

[応用] vpc内のリソースにアクセスする

公式のやり方でもいいし、
package json に以下を追加する感じでもおkです

{
  "vpc": {
    "securityGroups": ["xxx"],
    "subnets": ["aaaa", "bbbb"]
  }
}

続きを読む

Amazon CloudSearch の domain を AWS SDK for PHP で作成する

ゴール

Cloud formation は CloudSearchをサポートしていなさそうなので、AWSのコンソールから行える CloudSearch のドメイン作成と同等のことを AWS SDK for PHP で実現します。

環境

実行環境

  • AWS SDK for php version 2.8.31
  • php 7.0.21
  • PHPはEC2で実行しました。EC2には CloudSearchFullAccess を付与したIAMロールが設定してあります。

composer.json

composer.json
    "require": {
        "aws/aws-sdk-php": "2.*"
    }

CloudSeachのドメインを作成する PHPスクリプト

繰り返して実行すると、設定が更新されます。「既に存在するのでダメ」なエラーは発生しませんでした。

<?php

require_once 'vendor/autoload.php';

use AwsCloudSearchCloudSearchClient;
use AwsCommonEnumRegion;


const DOMAIN_NAME = '[作成するドメイン名]';

$client = CloudSearchClient::factory([
    'profile' => 'cloudsearch',
    'region'  => Region::US_WEST_2  // Oregon
]);

// ドメイン作成
$doamin = $client->createDomain(['DomainName'=>DOMAIN_NAME]);
// var_dump($doamin);

// Scaling Options でインスタンスサイズを設定
$client->UpdateScalingParameters([
    'DomainName'=>DOMAIN_NAME,
    'ScalingParameters' => [
        'DesiredInstanceType' => 'search.m1.small',
        'DesiredReplicationCount' => 1,
        // 'DesiredPartitionCount' => 1, // only search.m3.2xlarge
    ]
]);

// Availability Options でマルチAZ を無効 に設定
$client->UpdateAvailabilityOptions([
    'DomainName'=>DOMAIN_NAME,
    'MultiAZ' => false
]);

// Access Policies で指定したIPアドレスからのアクセスを許可
$client->UpdateServiceAccessPolicies([
    'DomainName'=>DOMAIN_NAME,
    'AccessPolicies' => accessPolicy(),
]);

// フィールドの設定
foreach( defineFields() as $name => $f )
{
    $f['IndexFieldName'] = $name;
    $client->defineIndexField([
        'DomainName' => DOMAIN_NAME,
        'IndexField' => $f,
    ]);
}

$indexFiled = $client->indexDocuments(['DomainName' => DOMAIN_NAME]);
// var_dump(indexFiled);

/**
 * アクセス ポリシー
 */ 
function accessPolicy()
{
    $policy = <<<EOM
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "cloudsearch:*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": "[接続を許可するIPアドレス]"
        }
      }
    }
  ]
}
EOM;
    return $policy;
}

/**
 * フィールドの定義
 */
function defineFields()
{
    // AnalysisScheme
    // http://docs.aws.amazon.com/ja_jp/cloudsearch/latest/developerguide/text-processing.html
    return [
        'tweet_id' => [
            'IndexFieldType'=> 'literal',
            'LiteralOptions' => [
                'SearchEnabled' => true,
                'FacetEnabled' => false,
                'ReturnEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'tweet_url' => [
            'IndexFieldType'=> 'text',
            'TextOptions' => [
                'AnalysisScheme' => '_en_default_',
                'ReturnEnabled' => true,
                'SortEnabled' => false,
                'HighlightEnabled' => false,
            ],
         ],
         'user_id' => [
            'IndexFieldType'=> 'text',
            'TextOptions' => [
                'SearchEnabled' => true,
                'FacetEnabled' => false,
                'ReturnEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'user_screen_name' => [
            'IndexFieldType'=> 'text',
            'TextOptions' => [
                'AnalysisScheme' => '_en_default_',
                'ReturnEnabled' => true,
                'SortEnabled' => true,
                'HighlightEnabled' => false,
            ],
         ],
         'created_at' => [
            'IndexFieldType'=> 'date',
            'DateOptions' => [
                'FacetEnabled' => true,
                'ReturnEnabled' => true,
                'SearchEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'text' =>[
            'IndexFieldType'=> 'text',
            'TextOptions' => [
                'AnalysisScheme' => '_ja_default_',
                'ReturnEnabled' => true,
                'SortEnabled' => true,
                'HighlightEnabled' => true,
            ],
         ],
         'retweet_count' => [
            'IndexFieldType'=> 'int',
            'IntOptions' => [
                'SearchEnabled' => true,
                'FacetEnabled' => true ,
                'ReturnEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'retweeted' => [
            'IndexFieldType'=> 'literal',
            'LiteralOptions' => [
                'SearchEnabled' => true,
                'FacetEnabled' => false,
                'ReturnEnabled' => true,
                'SortEnabled' => true,
            ],
         ],
         'hash_tags' => [
            'IndexFieldType'=> 'text-array',
            'TextArrayOptions' => [
                'SearchEnabled' => true,
                'AnalysisScheme' => '_ja_default_',
                'HighlightEnabled' => true,
                'ReturnEnabled' => true,
            ],
         ],
         "hash_tags_facet" => [
            'IndexFieldType'=> 'literal-array',
            'LiteralArrayOptions' => [
                'FacetEnabled' => true,
                'ReturnEnabled' => false,
                'SourceFields' => 'hash_tags',
            ],
         ],
         'urls' => [
            'IndexFieldType'=> 'text-array',
            'TextArrayOptions' => [
                'SearchEnabled' => true,
                'AnalysisScheme' => '_ja_default_',
                'HighlightEnabled' => true,
                'ReturnEnabled' => true,
            ],
         ],
    ];
}

実行後の Indexing Options の画面キャプチャ
スクリーンショット 2017-11-12 11.53.27.png

TwitterのAPIでツイートを取得して、主に ツイートのメッセージとハッシュタグで検索します。

メモ

インスタンス・タイプ

今回はデータ量が少ないので、明示的に search.m1.small にしています。

インスタンス・タイプを決めるにあたり、参考になる情報がドキュメントにあります。

一括アップロードを実行するには、以下に従います。

・バッチのサイズを制限の 5 MB にできるだけ近付けてください。小さなバッチを大量にアップロードすると、アップロードとインデックス作成の処理速度が低下します。
・必要なインスタンスタイプを、デフォルトの search.m1.small より大きなインスタンスタイプに設定します。使用できるアップロードスレッドの数は、ドメインで使っている検索インスタンスのタイプ、データの性質、インデックス作成オプションによって異なります。インスタンスタイプが大きいほど、アップロード容量が大きくなります。search.m1.small インスタンスにバッチを並列アップロードしようとすると、通常は、高い確率で 504 または 507 エラーが発生します。必要なインスタンスタイプの設定の詳細については、「スケーリングオプションの設定」を参照してください。

〜 省略 〜

データが 1 GB 未満のデータセットまたは 1,000,001 KB 未満のドキュメントの場合は、スモール検索インスタンスで十分です。1~8 GB のデータセットをアップロードするには、アップロードする前に、必要なインスタンスタイプを search.m3.large に設定することをお勧めします。8~16 GB のデータセットは、search.m3.xlarge で始まります。16~32 GB のデータセットは、search.m3.2xlarge で始まります。アップロードのサイズが 32 GB を超える場合は、インスタンスタイプに search.m3.2xlarge を選択し、データセットに対応できるように、望ましいパーティション数を増やします。各パーティションに最大 32 GB のデータを格納できます。さらに多くのアップロード容量が必要な場合、または 500 GB を超えるインデックスを作成する場合は、Service Increase Limit Request を送信します。

http://docs.aws.amazon.com/ja_jp/cloudsearch/latest/developerguide/uploading-data.html#bulk-uploads

TextField の AnalysisScheme

管理コンソールから操作する時にプルダウンで表示される言語別のテキスト処理の一覧です。
http://docs.aws.amazon.com/ja_jp/cloudsearch/latest/developerguide/text-processing.html

目的の言語の デフォルトの分析スキーム を AnalysisScheme に指定します。日本語は _ja_default_ 、英語は_en_default_ です。

日本語(ja)
アルゴリズム語幹解釈オプション: full
アルゴリズム複混合が有効
オプションのトークン分割ディクショナリ
デフォルトの分析スキーム: __ja_default__
・アルゴリズム語幹解釈: full
・デフォルトのストップワードディクショナリ

続きを読む

awscli ECS 関連コマンドメモ

主にコピペ用。

コマンドメモ

### 0. 環境変数
# CLUSTER_NAME_OR_ARN はそれぞれの環境に合わせて書き換えること
export CLUSTER_NAME_OR_ARN=<ECSクラスター名またはARN>

### 1. ヘルプ表示
aws ecs help
aws ecs <COMMAND> help

### 2. ECSクラスター一覧表示
aws ecs list-clusters
aws ecs list-clusters --output text 
aws ecs list-clusters --output text | sed -e 's/s/ /g' | cut -d ' ' -f2

### 3. ECSクラスター詳細表示
aws ecs describe-clusters --cluster $CLUSTER_NAME_OR_ARN
aws ecs describe-clusters --cluster $CLUSTER_NAME_OR_ARN --output text

# ECSクラスターは --cluster または --clusters オプションにてスペース区切りで列挙することで複数指定可能
# 例: 
# aws ecs describe-clusters --clusters <CLUSTER_NAME_OR_ARN> <CLUSTER_NAME_OR_ARN2> <CLUSTER_NAME_OR_ARN3> ...

### 4. ECSサービス一覧表示
aws ecs list-services --cluster $CLUSTER_NAME_OR_ARN
aws ecs list-services --cluster $CLUSTER_NAME_OR_ARN --output text
aws ecs list-services --cluster $CLUSTER_NAME_OR_ARN --output text | sed -e 's/s/ /g' | cut -d ' ' -f2

#### 4.1. ECSサービスARN環境変数セット
export ECS_SERVICE_ARN=arn:aws:ecs:<REGION>:<ACCOUNT_ID>:service/<SERVICE_NAME>
# or
export ECS_SERVICE_NAME=<ECSサービス名>
export ECS_SERVICE_ARN="$(aws ecs list-services --cluster $CLUSTER_NAME_OR_ARN --output text | sed -e 's/s/ /g' | cut -d ' ' -f2 |grep $ECS_SERVICE_NAME)"

### 5. ECSサービス詳細表示
aws ecs describe-services --cluster $CLUSTER_NAME_OR_ARN --services $ECS_SERVICE_ARN

### 6. ECSタスク一覧表示
aws ecs list-tasks --cluster $CLUSTER_NAME_OR_ARN --service-name $ECS_SERVICE_ARN
aws ecs list-tasks --cluster $CLUSTER_NAME_OR_ARN --service-name $ECS_SERVICE_ARN --output text
aws ecs list-tasks --cluster $CLUSTER_NAME_OR_ARN --service-name $ECS_SERVICE_ARN --output text | sed -e 's/s/ /g' | cut -d ' ' -f2

#### 6.1 ECSタスク環境変数セット
export ECS_TASK_ARNS="$(aws ecs list-tasks --cluster $CLUSTER_NAME_OR_ARN --service-name $ECS_SERVICE_ARN --output text | sed -e 's/s/ /g' | cut -d ' ' -f2 | tr 'n' ' ')"

### 7. ECSタスク詳細表示
aws ecs describe-tasks --cluster $CLUSTER_NAME_OR_ARN --tasks $(echo $ECS_TASK_ARNS)
aws ecs describe-tasks --cluster $CLUSTER_NAME_OR_ARN --tasks $(echo $ECS_TASK_ARNS) --output text

随時追加予定。

参考

続きを読む

AWS+Reactアプリ作成入門(ログイン後のAdmin編)

AWS+Reactアプリ作成入門(Cognito編)
AWS+Reactアプリ作成入門(S3編)
AWS+Reactアプリ作成入門(DynamoDB編)
AWS+Reactアプリ作成入門(IAM Role編)
AWS+Reactアプリ作成入門(ログイン後のAdmin編)

今回作成したアプリ ==>久喜SNS

 AWS+Reactアプリ作成入門は今回で最後です。書き残したことで重要なところを書きたいと思います。

 今回特に難しかったところはCognitoの使い方でした。一通り「AWS+Reactアプリ作成入門(Cognito編)」に書きましたが、重要な点をまだ書いていません。Reactアプリは複数のComponentファイルから成り立っています。「AWS+Reactアプリ作成入門(Cognito編)」で示したApp Componentを参照してください。App.jsファイルがロードされたときに非ログインユーザとして権限を持ち、LoginView Componentでログインした時にログインユーザとしての権限を持つようになり、Admin ComponentでS3やDynamoDBに画像ファイルやドキュメントをputするわけです。明記しているドキュメントが見つからず、ログイン状態はAdmin.jsでもそのまま保持されるのかが疑問でした。いろいろ試行錯誤した結果、答えはYesです。
 LoginView Componentで一度ログインすれば、その他のComponentのプログラムはログインユーザとして動作します。ユーザIDはAWS.config.credentials.identityIdでグローバルに参照できます。

1.Admin Component

 Admin Componentはログイン後にメニューに現れる管理画面で、画像掲示板の投稿・編集・削除を行う場所です。ログインユーザとしての権限を最大限に発揮できる画面です。特にCognito認証関係のコードを書かずに権限を実行できます。

src/views/Admin.jsの一部
import AWS from "aws-sdk";
import React from 'react';

---

export default class Admin extends  React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = {

---

  _fetch(identityId) {
    const _self = this;
    var dynamo = new AWS.DynamoDB.DocumentClient();
    var param = {
      TableName : tablename,
      ScanIndexForward: false, //queryには効くが、scanには効かない
      KeyConditionExpression : "identityId = :identityId",
      ExpressionAttributeValues : {":identityId" : identityId}
    };
    dynamo.query(param, function(err, data) {
        if (err) {
            console.log("### Error="+err);
        } else {
            //console.log("### data="+JSON.stringify(data.Items));
            _self.setState({items: data.Items});
        }
    });
  }

  componentWillMount() {
    const _self = this;
    const email = localStorage.getItem('email');
    const username = localStorage.getItem('username');
    var identityId = AWS.config.credentials.identityId;

    if( !email ) {
        handleErrorFunc('エラー:ログインしていません');
        return;
    }
    this.setState({identityId: identityId});
    this.setState({email: email});
    this.setState({username: username});
    this._fetch(identityId);
  }

---

  postAdd() {
    const _self = this;
    const identityId = _self.state.identityId;

//-------------------------------
// Date
//-------------------------------
    let uploadTime = 0;
    let uploadDate = "";
    let partitionYear = 0;
    if( !this.state.updateItem ) { //新規投稿
        const date = new Date() ;
        uploadTime = date.getTime();
        uploadDate = toLocaleString(date);
        partitionYear = date.getFullYear();
    } else {                        //編集投稿
        uploadTime = _self.state.updateItem.uploadTime;
        uploadDate = _self.state.updateItem.uploadDate;
        partitionYear = _self.state.updateItem.partitionYear;
    }


//-------------------------------
// S3 put
//-------------------------------
    let filepath = noimage;
    let thumbnail = noimage;
    let fileType = noimage;
    if( _self.state.imageOverwrite && !!_self.state.file) { 
        if (!_self.state.file.name.match(/^[0-9a-zA-Z._-]*$/)) {
            handleErrorFunc('エラー:ファイル名は小文字の英数字と . - _ しか使えません: '+_self.state.file.name );
            return;
        }
        filepath = 'contents/images/'+identityId+'/'+_self.state.file.name;
        thumbnail = filepath.replace(/images/, 'thumbnail');
        fileType = _self.state.file.type;
        console.log("filepath="+filepath);
        var params = {
            Bucket: bucketname,
            Key: filepath,
            ContentType: _self.state.file.type,
            Body: _self.state.file,
            Metadata: {
              data: JSON.stringify({
                identityId: identityId,
                uploadTime: uploadTime,
                uploadDate: uploadDate
              })
            }
        };
        var s3 = new AWS.S3();
        s3.putObject(params, function(err, data) {
            if(err) {
                console.log("Err: upload failed :" +err);
            } else {
                console.log("Success: upload ok");
                let url = 'http://'+bucketname+'.s3-'+appConfig.region+'.amazonaws.com/'+filepath;
                console.log("######11 imgurl="+url);
                _self.setState({imgurl: url});
            }
        });
    } else if ( _self.state.updateItem && !_self.state.imageOverwrite ) { //編集投稿で上書きアップロード無し
        filepath = _self.state.updateItem.filename;
        thumbnail = _self.state.updateItem.thumbnail;
        fileType = _self.state.updateItem.fileType;;    
    }
//-------------------------------
// DynamoDB putItem
//-------------------------------
    const title = escape_html(_self.state.title);
    let story = escape_html(_self.state.story);
    story = story.replace(/((http:|https:)//[x21-x26x28-x7e]+)/gi, "<a href='$1'>$1</a>");


    var docClient = new AWS.DynamoDB.DocumentClient();
    var params = {
        TableName: tablename,
        Item:{
             identityId: identityId, // ★prime partition key
             email: _self.state.email,
             username: _self.state.username,
             filename: filepath,
             thumbnail: thumbnail,
             type: fileType,
             title: title,
             story: story,
             imageOverwrite: _self.state.imageOverwrite,
             mapUse: _self.state.mapUse,
             position: _self.state.position,
             uploadTime: uploadTime, // ★prime & secondary sort key
             uploadDate: uploadDate,
             partitionYear: partitionYear, //★secondary partition key
             refCounter: 0
        }
    };
    docClient.put(params, function(err, data) {
        if(err) {
            console.log("Err: table put :" +err);
        } else {
            console.log("Success: table put ok");
        }
    });

//-------------------------------
// Clear form
//-------------------------------
    this.handlePostNew();

    this._fetch(identityId);

  }

---


 LoginView Componentでログインしているので、このAdmin ComponentでもログインユーザとしてS3やDynamoDBにputする権限を持っています。一応念のためにcomponentWillMount()でログインしているかの確認をしています。localStorageにemailがセットされているか否かで判断しています。LoginView Componentの以下のコードが効いています。再掲載していきます。

    AWS.config.credentials = new AWS.CognitoIdentityCredentials({
        IdentityPoolId: appConfig.IdentityPoolId,
        Logins : {
            // Change the key below according to the specific region your user pool is in.
            'cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xn9ihTu0b' : result.getIdToken().getJwtToken()
        }
    });

 見直しや、修正を入れること張りますが、以上で「AWS+Reactアプリ作成入門」を終わります。

続きを読む

AWSでterraformによる環境構築を自動化(docker)

やりたいこと

  • AWSに立てたCIサーバ(EC2・jenkins)でterraformを叩いてインフラのコード化
  • terraformはVMの環境に左右されないようコンテナ上で実行
  • imageはECRに保存しておく

設計

以下二つのジョブをjenkins上で作成
1. ECRへのイメージ自動pushのジョブ
2. イメージからコンテナを立ててterraformを実行するジョブ
スクリーンショット 2017-11-09 17.54.09.png

1に関して

ひとまずaws loginしてからpushする

Jenkinsfile
#!groovy
pipeline {
    agent any
    triggers {
        pollSCM('H/3 * * * 1-5')
    }
    //environment {}
    stages {
        stage('Master Branch <pushing>') {
            when {
                branch 'master'
            }
            steps {
                ansiColor('xterm') {
                    echo '<<< start pushing >>>'
                    sh 'aws ecr get-login --no-include-email --region ap-northeast-1 > temp.sh'
                    sh '''
                        echo "***@***.co.jp" | sudo sh temp.sh
                        sudo docker build -t ***** .
                        sudo docker tag *****:latest *****.ap-northeast-1.amazonaws.com/*****:latest
                        sudo docker push *****.ap-northeast-1.amazonaws.com/*****:latest
                    '''
                }
            }
        }
    }
}
Dockerfile(コピペなのでいらないもの入っているかもしれません…)
FROM python:3.6

ARG TERRAFORM_VERSION=0.10.0

RUN apt-get clean && \
    rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/* && \
    apt-get update && \
    apt-get -y upgrade && \
    apt-get install -y --no-install-recommends unzip zip jq ca-certificates curl lsb-release gawk

# install python modules
COPY requirements.txt .
RUN pip install -r requirements.txt

# install Terraform
RUN mkdir /tmp/terraform && \
    cd /tmp/terraform && \
    curl -O -sS -L https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
    unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
    rm terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \
    mv terraform* /usr/local/bin && \
    rm -rf /tmp/terraform

RUN apt-get clean && \
    rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*

2に関して

jenkinsfileのagentでコンテナを指定しようとするも失敗
– ECRからpullするには先にloginしておく必要がある
– localのイメージを使おうとしてもdocker pullをしてしまう(alwaysPull falseも効果なし)

愚直にコンテナを起動して内部でterraformを叩く…

Jenkinsfile
#!groovy
pipeline {
    agent any
    triggers {
        pollSCM('H/3 * * * 1-5')
    }
    stages {
        stage('Pull Request <Plan>') {
            when {
                branch 'PR-*'
            }
            steps {
                ansiColor('xterm') {
                    echo '<<< start planning >>>'
                    sh "docker run --name tmp-jenkins -id ***** | echo 'ignore failure'"
                    sh "docker start tmp-jenkins | echo 'ignore failure'"
                    sh "docker exec tmp-jenkins mkdir /tmp/terraform | echo 'ignore failure'"
                    sh "docker exec tmp-jenkins rm -rf /tmp/terraform/*"
                    sh "docker cp ./ tmp-jenkins:/tmp/terraform/"
                    sh "docker exec tmp-jenkins ls /tmp/terraform/"
                    sh "docker exec tmp-jenkins sh -c 'cd /tmp/terraform/ && terraform init -backend-config \"bucket=*****\"'"
                    sh "docker exec tmp-jenkins sh -c 'cd /tmp/terraform/ && terraform env select dev'"
                    sh "docker exec tmp-jenkins sh -c 'cd /tmp/terraform/ && terraform plan'"
                    sh "docker exec tmp-jenkins sh -c 'cd /tmp/terraform/ && terraform apply'"
                    sh "docker stop tmp-jenkins"
                }
            }
        }
    }
}

結果

  • ひとまずterraformを動かすことができた
  • ただ何かを作ろうとするとエラーが出る…
     * provider.aws: dial unix /tmp/plugin392670482|netrpc: connect: no such file or directory
  • あとこれを解決すれば完成… =>terraformを 0.10.0 -> 0.10.8にすることで解決

続きを読む

Serverless FrameworkでLambdaのコールドスタート対策を行う

この記事で扱うテーマ

AWS Lambdaで発生するコールドスタートを軽減させる為の対策を実施します。

前提条件

Serverless Framework でAWS Lambdaの開発を行っている事。

コールドスタート?

実業務でAWS Lambdaを使っている方なら分かると思いますが、AWS Lambdaにはコールドスタートという、Lambda関数のレイテンシが極端に悪化する現象が発生します。

コールドスタートは主に以下の原因で発生します。

  • 実行可能なコンテナが1つも存在しない時
  • 利用可能な数以上の同時リクエストが送信された時
  • コードや設定変更を行った時(デプロイを行った時)

このあたりに関しては既に詳しくまとめてある記事がありますので、リンクを共有させて頂きます。

対策

実行可能なコンテナが消えないように定期的にAWS Lambdaを実行する事でコールドスタートの可能性を軽減します。

自分で0から作っても良いのですが、 こちら で紹介されていた、serverless-plugin-warmup というPluginを利用すると楽なので、こちらを利用していきます。

導入方法

serverless-plugin-warmup の通りに進めていけば問題ありません。

はじめに npm install serverless-plugin-warmup --save-dev を実行してpackageのインストールを行います。

※ yarn を利用している場合は yarn add serverless-plugin-warmup --dev でインストールします。

pluginを読み込ませる設定を追加

serverless.yml に以下の設定を追加します。

serverless.yml
plugins:
  - serverless-plugin-warmup

IAM Roleの追加

以下の記述を追加します。

既に lambda:InvokeFunction の権限がある場合はこの手順は省略出来ます。

serverless.yml
iamRoleStatements:
  - Effect: 'Allow'
    Action:
      - 'lambda:InvokeFunction'
    Resource:
    - Fn::Join:
      - ':'
      - - arn:aws:lambda
        - Ref: AWS::Region
        - Ref: AWS::AccountId
        - function:${self:service}-${opt:stage, self:provider.stage}-*

定期実行の対象にしたい関数に warmup: true を指定

以下のように記述します。(helloを対象にする場合)

serverless.yml
functions:
  hello:
    warmup: true

これが基本ですが、他にも特定のstageの時だけ定期実行の対象にしたり、定期実行の時間間隔を調整(デフォルトは5分間隔で実行)したり出来ます。

詳しくは GitHub を見ると良いでしょう。

私の場合はデプロイ後に対象の関数を実行する prewarmtrue に設定しました。

serverless.yml
custom:
  warmup:
    prewarm: true

デプロイ

設定が終わったら serverless deploy を実行します。

デプロイ完了後、マネジメントコンソールでLambda関数の一覧を見ると、 [サービス名]-[stage]-warmup-plugin という関数が生成されている事が確認出来ます。

warmup-plugin
"use strict";

/** Generated by Serverless WarmUP Plugin at 2017-11-09T01:54:08.198Z */
const aws = require("aws-sdk");
aws.config.region = "ap-northeast-1";
const lambda = new aws.Lambda();
const functionNames = "[サービス名]-[stage]-[関数名]".split(",");
module.exports.warmUp = (event, context, callback) => {
  let invokes = [];
  let errors = 0;
  console.log("Warm Up Start");
  functionNames.forEach((functionName) => {
    const params = {
      FunctionName: functionName,
      InvocationType: "RequestResponse",
      LogType: "None",
      Qualifier: process.env.SERVERLESS_ALIAS || "$LATEST",
      Payload: JSON.stringify({ source: "serverless-plugin-warmup" })
    };
    invokes.push(lambda.invoke(params).promise().then((data) => {
      console.log("Warm Up Invoke Success: " + functionName, data);
    }, (error) => {
      errors++;
      console.log("Warm Up Invoke Error: " + functionName, error);
    }));
  });
  Promise.all(invokes).then(() => {
    console.log("Warm Up Finished with " + errors + " invoke errors");
    callback();
  });
}

serverless logs コマンド等で見てみると定期実行されている事が確認出来るかと思います。

まとめ

以上が serverless-plugin-warmup を利用したコールドスタート軽減対策でした。

これから正式に導入しようと考えているので、また知見が溜まったら何かで共有させて頂きます。

最後まで読んで頂きありがとうございました。

続きを読む