テックメモ

技術的に気になったことをメモしていきます

Golang dep ensureがハングした時にやったこと

はじめに

たまにGolangを使います。最近は依存関係のマネージメントツールは Glide ではなく dep を使っているのですが dep ensure が延々と終わらない状況となり、一応の解決を迎えたので備忘録です。

起こったこと

以下のようなコードを書き dep ensure をしたところ、10分以上処理が終わらない状況になりました。

package main

import (
    "fmt"
    _ "github.com/aws/aws-sdk-go"
)

func main() {
    fmt.Println("hello world")
}

depのバージョンはこちらです。

$ dep version
dep:
 version     : v0.4.1-161-g8b166f24
 build date  : 2018-05-02
 git hash    : 8b166f24
 go version  : go1.10
 go compiler : gc
 platform    : darwin/amd64
 features    : ImportDuringSolve=false

対応

まずは詳細情報を得るため dep ensure -v を実行してみたところ、以下の表示でハングしていました。

$ dep ensure -v
Root project is "xxxx/xxxx"
 1 transitively valid internal packages
 1 external packages imported from 1 projects
(0)   ✓ select (root)

この状態でどのようなプロセスが起動されているのか調査した結果がこちらです。

$ pstree

 |       \-+= 94723 ginyon dep ensure -v
 |         \-+= 94805 ginyon git ls-remote ssh://git@github.com/aws/aws-sdk-go
 |           \--- 94806 ginyon /usr/bin/ssh git@github.com git-upload-pack '/aws/aws-sdk-go'

試しに git ls-remote ssh://git@github.com/aws/aws-sdk-go を叩いたところホストキーの受け入れを確認されました。

$ git ls-remote ssh://git@github.com/aws/aws-sdk-go
The authenticity of host 'github.com (192.30.255.112)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)?

yes としてホストキー受け入れ後、再度 dep ensure -v を実行した結果がこちらです。

$ dep ensure -v
Root project is "xxxx/xxxx"
 1 transitively valid internal packages
 1 external packages imported from 1 projects
(0)   ✓ select (root)
(1) ? attempt github.com/aws/aws-sdk-go with 1 pkgs; 410 versions to try
(1)     try github.com/aws/aws-sdk-go@v1.13.54
(1) ✓ select github.com/aws/aws-sdk-go@v1.13.54 w/1 pkgs
  ✓ found solution with 1 packages from 1 projects

Solver wall times by segment:
     b-source-exists: 5.001233802s
         b-list-pkgs: 3.256008604s
              b-gmal: 3.198661178s
     b-list-versions: 2.016181548s
             satisfy:  13.335691ms
         select-atom:  11.367302ms
            new-atom:     82.554µs
         select-root:     61.103µs
               other:     18.659µs
  b-deduce-proj-root:      2.296µs

  TOTAL: 13.496952737s

(1/1) Wrote github.com/aws/aws-sdk-go@v1.13.54

無事に導入できました。というメモです。

ElasticBeantalkで作成されるリソース間の参照方法

はじめに

今年はブログたくさん書きたいなぁと思っていたらもう4月でした。
これで今年2本目のブログです。やばい。

直近でAWSのElasticBeanstalkで使える ebextensions について調べたのでメモしておきます。

ebextensionsのこと

そもそもみなさんはElasticBeanstalkを使ったことはあるでしょうか? docs.aws.amazon.com

たまにPaaSと表現されたりもしますが、僕はAWSリソースのプロビジョニングとアプリケーションのデプロイを統合して面倒を見てくれる、それはそれは素敵で業の深いサービスだと思っています。
ElasticBeanstalkの酸いも甘いも経験された方、一杯飲みに行きましょう。

さて、肝心の ebextensions について。

これはアプリケーションのソースコード内に .ebextensions というフォルダを作り、そこへ xxx.config という感じで命名した設定ファイルを放り込むことで、環境設定、パッケージの導入、CloudFormation記法を使ってのリソース作成、などが出来る控えめに言って黒魔法な存在です。
個人的にはあまり黒魔法すぎない程度(パッケージの導入やロケールの設定)に使うと良いと思っているのですが、バランス良く使うのはとても難しいです。

そんな ebextensions の、リソース作成にフォーカスした話題です。

本題

ElasticBeanstalkで作成されるリソース間の参照方法をまとめたいと思います。
まずElasticBeanstalkのリソースには、以下の2つのタイプが存在する事を意識しておくと良いです。

  • ElasticBeanstalkによって作成される標準リソース
  • ebextensionsResources プロパティによって作成されるカスタムリソース

標準リソースはこちらのドキュメントを参考にしてください。 docs.aws.amazon.com

カスタムリソースはこちらのドキュメントを参考にしてください。 docs.aws.amazon.com

さて、もちろんAWSリソースを作成する際には依存関係のあるリソース定義も必要になります。
この時は、標準リソースであっても、カスタムリソースであってもリソース情報を参照する術が必要となります。

上述したドキュメントには両者の答えとなる素晴らしいサンプルが記述されています。
そちらを抜粋したのが下記となります。

Resources:

  hooktopic:
    Type: AWS::SNS::Topic
    Properties:
      Subscription:
        - Endpoint: "my-email@example.com"
          Protocol: email

  lifecyclehook:
    Type: AWS::AutoScaling::LifecycleHook
    Properties:
      AutoScalingGroupName: { "Ref" : "AWSEBAutoScalingGroup" }
      LifecycleTransition: autoscaling:EC2_INSTANCE_TERMINATING
      NotificationTargetARN: { "Ref" : "hooktopic" }
      RoleARN: { "Fn::GetAtt" : [ "hookrole", "Arn"] }

これはLifeCyclehookとSNS Topicの作成例です。

着眼していただきたいのは、カスタムリソースの lifecyclehook 作成部分になります。
ここには、標準リソースの参照方法と、カスタムリソースの参照方法のエッセンスが詰まっています。

標準リソースの参照方法

カスタムリソースの lifecyclehook を作成する為に、ElasticBeanstalkが作成する標準リソースの AutoScalingName が必要としています。

ここの部分です。

      AutoScalingGroupName: { "Ref" : "AWSEBAutoScalingGroup" }

通常 AutoScalingName はElasticBeanstalkにより作成されるため不定の値となるので、これを標準リソース AWSEBAutoScalingGroup から Ref 関数で参照するようにしています。

つまり、標準リソースからは Ref 関数を用いることでリソース情報を参照できます。

カスタムリソースの参照方法

またまたカスタムリソースの lifecyclehook を作成する部分を見ていくと、今度はカスタムリソースの hooktopic を必要としている記述があります。

ここの部分です。

      NotificationTargetARN: { "Ref" : "hooktopic" }

これは直前で定義されている以下 hooktopic のARNを Ref で参照しています。

  hooktopic:
    Type: AWS::SNS::Topic
    Properties:
      Subscription:
        - Endpoint: "my-email@example.com"
          Protocol: email

また、データを参照する方法は Ref 以外にもあります。

以下のように Fn::GetAtt を使って各リソースのアトリビュートも取得可能です。(この辺りはCloudFormationの世界ですね)

      RoleARN: { "Fn::GetAtt" : [ "hookrole", "Arn"] }

つまり、カスタムリソースからは RefFn::GetAtt でリソース情報が参照できます。

WAFを適用するスニペット

最後に、ググってもヒットしなかったのでElasticBeanstalkにWAFを適用できるスニペットを貼っておきます。

Resources:
  webACLAssociation:
    Type: AWS::WAFRegional::WebACLAssociation
    Properties:
      ResourceArn: { "Ref": "AWSEBV2LoadBalancer" }
      WebACLId: { "Ref": "wafACLId" }

AWSEBV2LoadBalancerは標準リソースからの参照です。
wafACLIdは別途パラメーターに定義するなどしてご利用ください。

S3の署名付きURLを短縮URLでアクセス可能にするプロキシサービスをサーバーレスに構築する

はじめに

S3の署名付きURLをご存知でしょうか?
署名を付けたURLにアクセスすることで、プライベートなS3オブジェクトに対してダウンロードやアップロードを許可することが可能なしくみです。

ただ、この署名部分って結構長いんです。
この署名付きURLをどのように短縮できるのか、というのが今回のテーマです。

アーキテクチャ

[s3 presignedurl shortener]でググってみると、AWSのcompute blogにいい感じの記事が見つかりました。

aws.amazon.com

このアーキテクチャは素晴らしくて、S3のリダイレクト機能を上手く利用してなんとサーバーレスで短縮URLの仕組みを実現しています。
通常、短縮URLといえばリダイレクトで実現することが多いと思いますので、上記のブログも是非見ていただきたいです。

私は都合によりリダイレクトは利用できなかったため、この構成は見送りました。
リダイレクトをせずに署名付きURLを短縮したい。できればサーバーレスで。ということで今回のアーキテクチャはこちらです。

f:id:ginyon:20180110142128p:plain

CloudFrontをS3の前段に配置、Lambda@EdgeでCloudFrontからS3へのリクエストをインターセプトしてリクエストを加工することで、CloudFront - S3間の通信は署名付きURLによる通信としています。ユーザーは短縮URLでCloudFrontにアクセスするだけです。

Lambda@Egdeを利用するアイデアid:kikumotoさんから頂きました!ありがとうございます。

構築手順

CloudFormationのテンプレートを用意しました。
AWSコンソールにログインしていれば、こちらのボタンからStackの作成が可能です。

Create Stack

また、こちらのテンプレートを使うと、DynamoDBへ短縮URLを含んだItemを登録するLambdaが生成されるため、動作確認が簡単に行えます。
(このLambdaは構成図には含まれていません)

一応テンプレートをテキストでも貼っておきます。

AWSTemplateFormatVersion: "2010-09-09"
Description: Serverless Presinged URL Proxy. Must be deploy to us-east-1.

Outputs:
  S3BucketName:
    Description: "Amazon S3 bucket name."
    Value: !Ref S3Bucket
  CloudFrontURL:
    Description: CloudFront Distribution DomainName.
    Value: !Sub "https://${CloudFrontDistrib.DomainName}/"
  DynamoDBTableName:
    Description: DynamoDB TableName.
    Value: !Ref DynamoDBTable

Resources:
  S3Bucket:
    Type: "AWS::S3::Bucket"
    DeletionPolicy: Delete
    DependsOn: LambdaInvokePermission
    Properties:
      BucketName: !Ref "AWS::NoValue"
      NotificationConfiguration:
        LambdaConfigurations:
          -
            Event: "s3:ObjectCreated:*"
            Function: !GetAtt LambdaGenerateS3PresingedURL.Arn 

  DynamoDBTable:
    Type: "AWS::DynamoDB::Table"
    Properties:
      AttributeDefinitions:
        -
          AttributeName: ShortURL
          AttributeType: S
      KeySchema:
        -
          AttributeName: ShortURL
          KeyType: HASH
      ProvisionedThroughput: 
        ReadCapacityUnits: "5"
        WriteCapacityUnits: "5"

  LambdaExecRole:
    Type: "AWS::IAM::Role"
    Properties:
      Policies:
        -
          PolicyName: LambdaExecRolePresignedURLProxy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              -
                Effect: Allow
                Action: "logs:*"
                Resource: "arn:aws:logs:*:*:*"
              -
                Effect: Allow
                Action: [ "s3:GetObject", "s3:PutObject", "s3:DeleteObject" ]
                Resource: !Sub "arn:aws:s3:::*"
              -
                Effect: Allow
                Action: [ "dynamodb:GetItem", "dynamodb:PutItem" ]
                Resource: !GetAtt DynamoDBTable.Arn
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Sid: ""
            Effect: Allow
            Principal:
              Service: [ "lambda.amazonaws.com", "edgelambda.amazonaws.com"]
            Action: "sts:AssumeRole"

  CloudFrontDistrib:
    Type: "AWS::CloudFront::Distribution"
    Properties:
      DistributionConfig:
        Origins:
          -
            DomainName: !GetAtt S3Bucket.DomainName
            Id: S3Origin
            S3OriginConfig:
              OriginAccessIdentity: ""
        Comment: CloudFront distribution used as a front end to the server-less Presinged URL Proxy. 
        Enabled: true
        DefaultCacheBehavior:
          AllowedMethods: [ HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH ]
          CachedMethods: [ HEAD, GET ]
          Compress: true
          DefaultTTL: 60 
          TargetOriginId: "S3Origin"
          ViewerProtocolPolicy: "redirect-to-https"
          ForwardedValues:
            QueryString: 'false'
          LambdaFunctionAssociations:
            -
              EventType: origin-request
              LambdaFunctionARN: !Ref LambdaProxyDeployVersion

  LambdaProxy:
    Type: "AWS::Lambda::Function"
    Properties:
      Handler: index.handler
      MemorySize: 512
      Role: !GetAtt LambdaExecRole.Arn
      Runtime: nodejs6.10
      Timeout: 3
      Code:
        ZipFile:
          !Sub
            - |
              'use strict';

              const aws = require('aws-sdk');
              const docClient = new aws.DynamoDB.DocumentClient({region: '${AWS::Region}'});

              exports.handler = (event, context, callback) => {
                  const request = event.Records[0].cf.request;
                  const httpVersion = request.httpVersion;
                  const uri         = request.uri;
                  var params = {
                      TableName: '${DynamoDBTableName}',
                      Key: {
                          'ShortURL': uri.slice(1)
                      }
                  };
                  docClient.get(params, function(err, data) {
                      const isEmpty = function(obj) {
                          for(var key in obj) {
                              if(obj.hasOwnProperty(key)) {
                                  return false;
                              }
                          }
                          return true;
                      };
                      if (err || isEmpty(data)) {
                          const response = {
                              status: '404',
                              statusDescription: 'Not Found.',
                              httpVersion: httpVersion,
                              body: "404 Not Found.",
                          };
                          callback(null, response);
                          return;
                      }
                      const util = require('util');
                      /* Rewrite uri */
                      request.uri = '/' + data.Item.S3Key;
                      /* Added Query String */
                      request.querystring = data.Item.S3PrisignedQuery;
                      callback(null, request);
                  });
              };
            -
              { DynamoDBTableName: !Ref DynamoDBTable }

  LambdaProxyDeployVersion: 
    Type: "AWS::Lambda::Version"
    Properties: 
      FunctionName: !Ref LambdaProxy

  LambdaGenerateS3PresingedURL:
    Type: "AWS::Lambda::Function"
    Properties:
      Handler: index.handler
      MemorySize: 512
      Role: !GetAtt LambdaExecRole.Arn
      Runtime: nodejs6.10
      Timeout: 30
      Code:
        ZipFile:
          !Sub
            - |
              'use strict';
              
              const aws = require('aws-sdk');
              const endpoint = new aws.Endpoint("s3.amazonaws.com");
              const s3 = new aws.S3({ apiVersion: '2006-03-01', signatureVersion: 'v4', endpoint: endpoint });
              const docClient = new aws.DynamoDB.DocumentClient({region: '${AWS::Region}'});
              const url = require('url');

              exports.handler = (event, context, callback) => {
                  const bucket = event.Records[0].s3.bucket.name;
                  const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
                  const expireSec = 60 * 10;

                  const presingedURL = s3.getSignedUrl('getObject', {
                      Bucket: bucket,
                      Key: key,
                      Expires: expireSec
                  });
                  console.log('Generate PresingedURL.');
                  console.log(presingedURL);
                  const presignedQuery = url.parse(presingedURL).query;
                  const params = {
                      TableName: '${DynamoDBTableName}',
                      Item: {
                          'ShortURL': Math.random().toString(36).slice(-8),
                          'S3Key': key,
                          'S3PrisignedQuery': presignedQuery
                      }
                  };
                  docClient.put(params, function(err, data) {
                      if (err) {
                          console.error("Unable to add item. Error JSON:", JSON.stringify(err, null, 2));
                      } else {
                          console.log("Added item:", JSON.stringify(data, null, 2));
                      }
                  });
              };
            -
              { DynamoDBTableName: !Ref DynamoDBTable }

  LambdaInvokePermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      FunctionName: !GetAtt LambdaGenerateS3PresingedURL.Arn 
      Action: "lambda:InvokeFunction"
      Principal: s3.amazonaws.com
      SourceAccount: !Ref AWS::AccountId

動作確認

上記テンプレートを使用した事を前提として動作確認を行っていきます。

Cloud FormationのOutputタブを確認

f:id:ginyon:20180110210534p:plain 出力されている項目を控えておきましょう。

  • CloudFrontURL
  • DynamoDBTableName
  • S3BucketName
S3バケットにオブジェクトをアップロード

S3BucketNameで表示されたバケットにオブジェクトをアップロードします。
今回はhello worldと書かれただけのhtmlをアップロードしました。

$ echo "hello world" > hello.html
$ aws s3 cp hello.html s3://presingedurlproxy2-s3bucket-s98otf4r11ov
upload: ./hello.html to s3://presingedurlproxy2-s3bucket-s98otf4r11ov/hello.html
DynamoDBの登録内容を確認する

DynamoDBTableNameで表示されたテーブルを確認します。
S3 PUTをトリガーに起動されたLambdaにより、新しいItemが登録されていることが確認できます。

$ aws dynamodb scan --table-name PresingedURLProxy2-DynamoDBTable-6PNHTTGS8MNM
{
    "Items": [
        {
            "S3Key": {
                "S": "hello.html"
            },
            "ShortURL": {
                "S": "pee6zuxr"
            },
            "S3PrisignedQuery": {
                "S": "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAJTONTYX635YGUGFQ%2F20180110%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20180110T081138Z&X-Amz-Expires=600&X-Amz-Security-Token=FQoDYXdzEOn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDIm2aaU%2BCSkXPOo8IyKWAim%2B6fln3qiscBisqXqbF09Ofxn62mNkovBdkxw1riLMxKM%2BumxIwUq19QppQNrmnSyhKBjfvpM5FnLKB441Pc0bmzcuaykjfKUHmiekTtqxKj03gbM%2B8BgwAdaXJbS%2BgeDU%2BB9sAwl2w9j%2BzsqtYSuNF8MtPwmner2eV0jxineURCNIODeklgk%2FyRUKz4U%2BssIbk4%2Bf6qdyNcFBkHV8JO5wsXfduSEjxUPBrqTFt%2FlFFK8QoE01x1b9rl8jZ%2BCfsWMj%2FN6c8boY1QBwQspg4gTd9wC31ROvRc1QCq9B7hU0ie78DPS%2BkGBt%2B5m2Xbt0oAlpgnMr8sPRvKN2wXA2mvY1jYvUeiFoxmMjG0d2ngob7xVrkKCwKImI19IF&X-Amz-Signature=218d4973fc2502ae942f06002ee17f1a582ddf711265da583858ec1ff5b86e67&X-Amz-SignedHeaders=host"
            }
        }
    ],
    "Count": 1,
    "ScannedCount": 1,
    "ConsumedCapacity": null
}

ShortURLを控えましょう。

CloudFrontに短縮URLでアクセスする

それでは短縮URLにアクセスしてみます。
ドメインにはCloudFrontURLを使ってください。

$ curl -vvv -X GET https://d14gehme3xkgip.cloudfront.net/pee6zuxr
*   Trying 13.32.50.11...
* Connected to d14gehme3xkgip.cloudfront.net (13.32.50.11) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.cloudfront.net
* Server certificate: Symantec Class 3 Secure Server CA - G4
* Server certificate: VeriSign Class 3 Public Primary Certification Authority - G5
> GET /pee6zuxr HTTP/1.1
> Host: d14gehme3xkgip.cloudfront.net
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Content-Length: 12
< Connection: keep-alive
< Date: Wed, 10 Jan 2018 08:21:28 GMT
< Last-Modified: Wed, 10 Jan 2018 08:11:37 GMT
< ETag: "6f5902ac237024bdd0c176cb93063dc4"
< Accept-Ranges: bytes
< Server: AmazonS3
< X-Cache: Miss from cloudfront
< Via: 1.1 9063af643f5f74dbc0e44494f142a87f.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: VYFNZjPSCdntReqTnIGdx6jnD76Tz2lFrMConknNiTnVveRp4xKVRg==
<
hello world
* Connection #0 to host d14gehme3xkgip.cloudfront.net left intact

成功です。

それではS3オブジェクトのパスであるhello.htmlでアクセスするとどうなるか。該当するShortURLがDynamoDBに登録されていないため404になるはずです。

$ curl -vvv -X GET https://d14gehme3xkgip.cloudfront.net/hello.html
*   Trying 54.192.233.250...
* Connected to d14gehme3xkgip.cloudfront.net (54.192.233.250) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.cloudfront.net
* Server certificate: Symantec Class 3 Secure Server CA - G4
* Server certificate: VeriSign Class 3 Public Primary Certification Authority - G5
> GET /hello.html HTTP/1.1
> Host: d14gehme3xkgip.cloudfront.net
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 404 Not Found
< Content-Length: 14
< Connection: keep-alive
< Server: CloudFront
< Date: Wed, 10 Jan 2018 08:23:09 GMT
< X-Cache: Error from cloudfront
< Via: 1.1 2fe788985cac89c0ef661ff7cd8edf63.cloudfront.net (CloudFront)
< X-Amz-Cf-Id: tyQof7QnAQy9p5XzryvT0F6jyEFdoBx2dzYoor3H0PmA-bKvSin_AQ==
<
* Connection #0 to host d14gehme3xkgip.cloudfront.net left intact
404 Not Found.

404になりましたね。

なお、今回は試していませんが同じ要領でS3へPUTすることも可能です。
興味がある方はぜひ試してみてください。

解説

今回の構成についての解説です。

署名計算 + DynamoDBへのItem登録処理

S3 Putをトリガーとして起動したLambdaの処理について少しだけ解説です。

まずは、短縮URLに相当する文字列の生成と署名の計算を行います。
そして、DynamoDBへ短縮URL、署名、S3オブジェクトのパスを保存して終了です。

やっていることはこれだけなのですが、署名計算時に一点注意点があります。
署名計算時のホスト名は必ず、バケット名.s3.amazonaws.comとして計算をしてください。
Cloud FrontがS3 Originにアクセスする時にホスト名のリライトをしてくれるのですが、この時に使われるホスト名の形式が上記となるようです。

docs.aws.amazon.com

この形式以外のホスト名を使うと、署名が合わなかったり(署名計算にホスト名を使うため)と色々とハマるポイントが生まれます。
検証時、ap-northeast-1のエンドポイントを使ってしまった為、随分とハマりました。。。

署名付きURLプロキシ

CloudFront + Lambda@Edge + DynamoDB + S3 の構成について。

CloudFront + Lambda@Edge

CloudFrontのオリジンがS3Originとして構成されていると、CloudFrontによりHost名のリライト(cloud frontからs3への変換)が自動的に行われます。
まずはここで第一のリクエストの加工が発生します。

CloudFrontでは、リクエストに該当するキャッシュが無かった場合のみLambda@Edgeがトリガーされます。これはオリジンリクエストで動くLambda@Edgeの仕様です。
トリガーされたLambda@Edgeは、短縮URLをキーにDynamoDBからアイテムを取得します。
該当するアイテムが無ければ404のレスポンスを返しますが、アイテムが取得できた場合はS3へ渡すリクエストを更に加工していきます。

加工するポイントは2つです。
まず、リクエストのURI短縮URLのままなので、S3オブジェクトへのパスへと変換します。
最後に、リクエストのクエリストリングに署名情報を突っ込みます。

これでCloudFrontからS3へのリクエストは、署名付きURLによるリクエストに置き換わりました。
実際に運用される場合は、DynamoDBのTTLや署名の期限なども考慮した方が良いと思います。

さいごに

Lambda@Edgeの上手い使い方がわからなかったのですが、今回はなかなか面白い構成ができたと思っています。
他にもS3にベーシック認証掛けたりもできるようなので、アーキテクチャの選定時にはLambda@Edgeを忘れないでおこうと思います。

EC2-Classicな環境のハマりどころ(EC2編)

はじめに

EC2-Classicをご存知でしょうか。
まだVPCがデフォルトでは無かった時代に作成したAWSアカウントで利用可能な環境で、EC2-Classic環境上で構築したELBのセキュリティグループは基本フルオープンだったりします。

Classicを冠しているその名からも解る通り、今となっては古い環境ですので名前だけは知っているが実際に触れた事の無い方も多いのではないかと思います。

本題

とはいえ、今もどこかで元気に稼働しているEC2-Classicな環境もあるわけで、当然EC2のSystem Rebootなどのメンテナンス通知なども届くわけですね。
そんな時VPC Defaultな感覚でrebootやstop->startをすると少しハマるかもしれない、というのが今回のお話です。

docs.aws.amazon.com

上記ドキュメントの、インスタンスの停止と起動を見てみましょう。

インスタンスがEC2-Classicで実行されている場合、インスタンスは新しいプライベートIPv4アドレスを受け取ります。

そうなんです、プライベートIPが変わるんですね。
VPCの場合は停止していてもIPアドレスが保持されますが、EC2-Classicの場合は保持されません。

次、

つまり、プライベート IPv4 アドレスに関連付けられていた Elastic IP アドレスは、インスタンスとの関連付けが解除されます。

そう、パブリックIPも解除されてしまうんですね。EIP付けてるから安心だろう、というのは甘いということですね。これは意外とハマるのではないでしょうか。

なお、これはEC2の停止と起動を行った場合のお話で、再起動の場合は問題ありません。

System Rebootの対応として、EC2-Classicな環境でEC2のstop->startをすることがあれば思い出したいポイントですね。

ACMでDNSによるドメイン検証が可能になりました(自動更新が個人的に楽になった)

はじめに

AWSACMに関する以下のリリースを発表しました。

https://aws.amazon.com/jp/about-aws/whats-new/2017/11/aws-certificate-manager-easier-certificate-validation-using-dnsaws.amazon.com

今まで、ACMドメイン検証をする場合は、メール受信による検証だけだったのですが、これにDNSレコードの登録(CNAMEレコード)による検証が加わりました。

正直、今まではこれだけのためにメール受信環境を構築することもあり、TerraformとかCloud Formation等でテンプレ化をしていても面倒だったのでかなり嬉しいリリースです。

DNSレコード検証の自動更新プロセスについて詳細な内容にアップデートをしました (1/22)

何が嬉しいか

メールの受信環境を構築しなくて良いのは楽ですね。

また、DNSレコード検証専用の自動更新プロセスも誕生しました。

実は私はこちらの自動更新プロセスの方が嬉しいです。
メール検証の場合、証明書の自動更新の際に以下のプロセスが発生していました。

  1. ACMから対象ドメインの対してHTTPSの接続要求による検証を行う
  2. HTTPSの接続が確認できなかった場合はドメインオーナーに対して確認用Eメールを送付する

ACMからの接続による検証が通ると勝手に更新されていて、ACM最高だわ!となるのですが、そもそもSGなどで接続を制限している環境で運用をしていると、更新時にメール検証の一手間が発生してしまい、あー。ってなっていました。

では、DNSレコード検証ではどのようなプロセスとなるのか。

DNS を使用したドメインの所有権の検証 - AWS Certificate Manager

証明書は使用中で CNAME レコードが残っている状態であれば、証明書は ACM によって自動的に更新されます。

とあります。つまり以下の2点をクリアできれば自動的に更新されます。

  1. CNAMEレコードによる検証を行う
  2. 証明書が使用中であるか検証を行う

また「証明書が使用中」であることは以下のいずれかで確認可能です。

  • コンソールで確認したときに「使用中?」が「はい」になっている
  • describe-certificate コマンド InUseBy に何かしらのARNが存在する

上記で検証が失敗した場合はAWS Accountに対して確認用Eメールが送付されます。

Eメール認証で取得した証明書からドメイン認証へ乗り換える方法

これはよくある質問に答えが載っていました。

既存の証明書を E メールでの検証から DNS での検証に変更できますか?
できません。しかし、無料の証明書を新たに ACM からリクエストして、この新しいものに対して DNS での検証を選択できます。

つまり、Eメール認証で取得したドメイン名の証明書をDNS認証で新しく取得して、各AWSリソースとの紐づけを順次切り替えていけば良いです。

ACMで自動更新ができない件について調べてみた

はじめに

先日クラスメソッドさんのこちらのブログでAWS ACMで自動更新に失敗するケースがあることを知りました。

dev.classmethod.jp

自動更新に失敗することがあると聞いた事はありつつも、詳しい条件は知らなかった為なるほどなぁーとつらつらと眺めていると衝撃的な一文を目にしました。

纏めると次の条件に当てはまる証明書は自動更新が実施されません。 ワイルドカード証明書を利用している

まじか!それは面倒くさいよ!と思い調査をしたところ、AWSのドキュメントがアップデートされており少し状況が変わっている事が判明しました。

ACMで自動更新されない条件(2017/7/3版)

サクッと紹介します。

まず、ACMで自動更新を有効にする条件を見ていきましょう。 参照したドキュメントは以下になります。

docs.aws.amazon.com

証明書を自動的に更新するには、以下の条件が満たされている必要があるとあります。

  • ACM によって証明書の各ドメインHTTPS 接続を確立できる必要があります。
  • 各接続では、返される証明書が ACM が更新している証明書と一致する必要があります。

さらに更新の可能性を高める為には次の操作を行うようにとあります。

  • AWS リソースで証明書を使用すること
  • インターネットからの HTTPS リクエストを許可するようにリソースを設定します
  • ドメイン名を ACM 証明書をホストするリソースにルーティングするように DNS を設定すること

ここまではクラスメソッドさんのブログにある通りだと思います。

ACMワイルドカード証明書を自動更新させる条件(2017/7/3版)

次にACMドメイン検証の仕組みを確認しましょう。 参照したドキュメントは以下になります。

docs.aws.amazon.com

おそらく、ここに関するドキュメントがアップデートされたのだと思います。 自動ドメイン検証の仕組みには以下の記載があります。

ACM は、ワイルドカードドメイン名 (*.example.com など) を親ドメインと同じく処理します。次の表の例を参照してください。

肝心の表を参照してみると・・・・

証明書のドメイン ACM 自動検証に使用するドメイン
example.com example.com / www.example.com
www.example.com www.example.com / example.com
*.example.com example.com / www.example.com

なんとワイルドカードドメインにも自動検証に使用するドメインが記載されています!

念のためAWSサポートの方にも確認したところ、ワイルドカードドメイン証明書の場合は表の通りapexまたはwwwドメインに対して検証を行うとの事でした。
(ただしワイルドカードのみの証明書の場合はRFC 6125に違反する為、不正な証明書とみなされるので注意!!)

つまり、ワイルドカード証明書の場合は以下3点の条件を満たしていれば自動更新される事になります。

  1. 証明書がELBやCloudFrontに設定されている
  2. インターネットからそのリソースに対してアクセスができる
  3. www.もしくはapexドメインでそのリソースにアクセスできるようにDNSでルーティングされている

ELBだと固定費がかかるので、なるべく安く自動更新する為にはS3+CloudFrontのような構成で自動検証をクリアするのが良いかもしれませんね。

CloudFrontでACMを利用する場合はus-east-1で証明書を作成する必要がありました。そのため、東京リージョンで取得したACMの証明書は上記の構成で自動検証をクリアすることができませんでした。
us-east-1以外のリージョンで自動更新を通過させるためには、現状はELBに証明書をセットするしか手がなさそうです。