20200925のAWSに関する記事は18件です。

Java で Amazon Product Advertising API 5.0 (PA-API v5) をコールする

概要

  • Java で Amazon Product Advertising API 5.0 (PA-API v5) をコールする
  • AWS が提供している公式の SDK を使わず API を直接コールする
  • AWS 署名バージョン 4 (AWS Signature Version 4) の処理を Java 標準ライブラリのみで実装する
  • 今回の実行環境: macOS Catalina + Java 15 (AdoptOpenJDK 15) + Jackson Databind 2.11.1 + Gradle 6.6.1

サンプルコード

ファイル一覧

├── build.gradle
└── src
    └── main
        └── java
            ├── AwsSignature4.java
            ├── JsonUtil.java
            ├── MyApp.java
            └── PaApi5Wrapper.java

build.gradle

Gradle の設定ファイル。
JSON 操作ライブラリは Java の標準ライブラリにないため Jackson を使用する。

plugins {
  id 'java'
  id 'application'
}

repositories {
  mavenCentral()
}

dependencies {
  // Jackson を使う
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
}

application {
  mainClassName = 'MyApp'
}

sourceCompatibility = JavaVersion.VERSION_15

src/main/java/MyApp.java

import java.util.HashMap;
import java.util.Map;

/**
 * PA-API v5 をコールするサンプルクラス。
 */
public class MyApp {

  public static void main(String[] args) throws Exception {
    // PA-API v5 をコールする
    searchItems();
    getItems();
  }

  private static final String ACCESS_KEY = "<YOUR-ACCESS-KEY-HERE>"; // 取得したアクセスキー
  private static final String SECRET_KEY = "<YOUR-SECRET-KEY-HERE>"; // 取得したシークレットキー
  private static final String TRACKING_ID = "<YOUR-PARTNER-TAG-HERE>"; // トラッキングID (例: XXXXX-22)

  // キーワードから商品を検索
  public static void searchItems() throws Exception {

    String keywords = "シェイクスピア";

    // リクエスト情報
    Map<String, Object> req = new HashMap<>() {
      {
        put("ItemCount", 3); // 検索結果の数
        put("PartnerTag", TRACKING_ID); // ストアID or トラッキングID
        put("PartnerType", "Associates"); // パートナータイプ
        put("Keywords", keywords); // 検索キーワード
        put("SearchIndex", "All"); // 検索カテゴリー (All, AmazonVideo, Books, Hobbies, Music などを指定可能)
        put("Resources", new String[]{ // レスポンスに含む値のタイプ
          "ItemInfo.Title",
          "ItemInfo.ByLineInfo",
          "ItemInfo.ProductInfo",
          "ItemInfo.ProductInfo",
          "Images.Primary.Large",
          "Images.Primary.Medium",
          "Images.Primary.Small"
        });
      }
    };

    // リクエスト情報を JSON 文字列にする
    String reqJson = new JsonUtil().objectToJson(req);
    System.out.println("===== キーワードから商品を検索: リクエスト =====");
    System.out.println(reqJson);

    // PA-API v5 をコールして結果を JSON 文字列で受け取る
    PaApi5Wrapper api = new PaApi5Wrapper(ACCESS_KEY, SECRET_KEY);
    String resJson = api.searchItems(reqJson);
    System.out.println("===== キーワードから商品を検索: レスポンス =====");
    System.out.println(new JsonUtil().prettyPrint(resJson));
  }

  // ASIN から商品情報を取得
  public static void getItems() throws Exception {

    String[] asinList = new String[]{"4391641585", "B010EB1HR4", "B0125SPF90", "B07V52KSGT"};

    // リクエスト情報
    Map<String, Object> req = new HashMap<>() {
      {
        put("PartnerTag", TRACKING_ID); // ストアID or トラッキングID
        put("PartnerType", "Associates"); // パートナータイプ
        put("ItemIds", asinList); // ASINのリスト
        put("Resources", new String[]{ // レスポンスに含む値のタイプ
          "ItemInfo.Title",
          "ItemInfo.ByLineInfo"
        });
      }
    };

    // リクエスト情報を JSON 文字列にする
    String reqJson = new JsonUtil().objectToJson(req);
    System.out.println("===== ASIN から商品情報を取得: リクエスト =====");
    System.out.println(reqJson);

    // PA-API v5 をコールして結果を JSON 文字列で受け取る
    PaApi5Wrapper api = new PaApi5Wrapper(ACCESS_KEY, SECRET_KEY);
    String resJson = api.getItems(reqJson);
    System.out.println("===== ASIN から商品情報を取得: レスポンス =====");
    System.out.println(new JsonUtil().prettyPrint(resJson));
  }
}

src/main/java/PaApi5Wrapper.java

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Map;

/**
 * PA-API v5 ラッパークラス。
 */
public class PaApi5Wrapper {

  private static final String HOST = "webservices.amazon.co.jp"; // Amazon.co.jp の Web API ホスト
  private static final String REGION = "us-west-2"; // Amazon.co.jp では us-west-2 を指定

  private final String accessKey;
  private final String secretKey;

  /**
   * コンストラクタ。
   * @param accessKey アクセスキー
   * @param secretKey シークレットキー
   */
  public PaApi5Wrapper(String accessKey, String secretKey) {
    this.accessKey = accessKey;
    this.secretKey = secretKey;
  }

  /**
   * キーワードから商品を検索する。
   * @param reqJson リクエスト情報 JSON
   * @return レスポンス情報 JSON
   * @throws InterruptedException
   * @throws IOException
   * @throws NoSuchAlgorithmException
   * @throws InvalidKeyException
   * @throws URISyntaxException
   */
  public String searchItems(String reqJson) throws InterruptedException, IOException, NoSuchAlgorithmException, InvalidKeyException, URISyntaxException {
    String path = "/paapi5/searchitems";
    String target = "com.amazon.paapi5.v1.ProductAdvertisingAPIv1.SearchItems";
    return callApi(reqJson, path, target);
  }

  /**
   * ASIN から商品情報を取得する。
   * @param reqJson リクエスト情報 JSON
   * @return レスポンス情報 JSON
   * @throws InterruptedException
   * @throws IOException
   * @throws NoSuchAlgorithmException
   * @throws InvalidKeyException
   * @throws URISyntaxException
   */
  public String getItems(String reqJson) throws InterruptedException, IOException, NoSuchAlgorithmException, InvalidKeyException, URISyntaxException {
    String path = "/paapi5/getitems";
    String target = "com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems";
    return callApi(reqJson, path, target);
  }

  /**
   * PA-API v5 をコールする。
   * @param reqJson リクエスト情報 JSON
   * @param path API エントリポイントのパス
   * @param target リクエストの送信先サービスおよびデータのオペレーション
   * @return レスポンス情報 JSON
   * @throws URISyntaxException
   * @throws InvalidKeyException
   * @throws NoSuchAlgorithmException
   * @throws IOException
   * @throws InterruptedException
   */
  public String callApi(String reqJson, String path, String target) throws URISyntaxException, InvalidKeyException, NoSuchAlgorithmException, IOException, InterruptedException {

    // Java 11 から正式導入された HTTP Client API を使う

    // HTTP リクエスト情報を構築
    HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
      .uri(new URI("https://" + HOST + path)) // API コール用の URL
      .POST(HttpRequest.BodyPublishers.ofString(reqJson)) // API コールのパラメータをセット
      .timeout(Duration.ofSeconds(10));

    // 署名情報を付加したヘッダ情報を取得
    AwsSignature4 awsv4Auth = new AwsSignature4(accessKey, secretKey, path, REGION, HOST, target);
    Map<String, String> signedHeaders = awsv4Auth.getHeaders(reqJson);

    // リクエスト情報にヘッダをセット
    signedHeaders.remove("host"); // Host ヘッダは付加しない (jdk.httpclient.allowRestrictedHeaders)
    for (Map.Entry<String, String> entrySet : signedHeaders.entrySet()) {
      reqBuilder.header(entrySet.getKey(), entrySet.getValue());
    }

    // HTTP リクエスト情報を生成
    HttpRequest req = reqBuilder.build();

    // API をコールして結果を取得
    HttpClient client = HttpClient.newBuilder()
      .version(HttpClient.Version.HTTP_1_1)
      .connectTimeout(Duration.ofSeconds(10))
      .build();
    HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());

    // ステータスコードで成功・失敗を判断
    if (res.statusCode() == 200) {
      return res.body();
    } else {
      throw new RuntimeException(res.body());
    }
  }
}

src/main/java/AwsSignature4.java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.TreeMap;

/**
 * AWS 署名バージョン 4 (AWS Signature Version 4)
 */
public class AwsSignature4 {

  private static final String SERVICE = "ProductAdvertisingAPI"; // PA-API
  private static final String HMAC_ALGORITHM = "AWS4-HMAC-SHA256";
  private static final String AWS_4_REQUEST = "aws4_request";

  private final String awsAccessKey;
  private final String awsSecretKey;
  private final String path;
  private final String region;
  private final String host;
  private final String target;

  /**
   * コンストラクタ。
   * @param awsAccessKey アクセスキー
   * @param awsSecretKey シークレットキー
   * @param path API エントリポイントのパス
   * @param region リージョン
   * @param host API エントリポイントのパス
   * @param target リクエストの送信先サービスおよびデータのオペレーション
   */
  public AwsSignature4(
    String awsAccessKey, String awsSecretKey,
    String path, String region,
    String host, String target) {
    this.awsAccessKey = awsAccessKey;
    this.awsSecretKey = awsSecretKey;
    this.path = path;
    this.region = region;
    this.host = host;
    this.target = target;
  }

  /**
   * 認証用のヘッダ情報を返す。
   * @param payload リクエスト情報 JSON
   * @return 認証用のヘッダ情報
   * @throws NoSuchAlgorithmException
   * @throws InvalidKeyException
   */
  public Map<String, String> getHeaders(String payload) throws NoSuchAlgorithmException, InvalidKeyException {
    // ベースになるヘッダ
    TreeMap<String, String> headers = new TreeMap<>();
    headers.put("host", host);
    headers.put("content-type", "application/json; charset=utf-8");
    headers.put("content-encoding", "amz-1.0");
    headers.put("x-amz-target", target);
    // 署名を作成するときに使用されるタイムスタンプ
    final Date date = new Date();
    headers.put("x-amz-date", getXAmzDateString(date));
    // 署名付きヘッダー (signed headers)
    String signedHeaders = createSignedHeaders(headers);
    // 正規リクエスト (canonical request)
    String canonicalRequest = createCanonicalRequest(path, headers, signedHeaders, payload);
    // 署名文字列 (string to sign)
    String stringToSign = createStringToSign(date, region, canonicalRequest);
    // 署名 (signature)
    String signature = calculateSignature(awsSecretKey, date, region, stringToSign);
    // Authorization ヘッダー値
    String authorization = buildAuthorizationString(awsAccessKey, region, signature, signedHeaders, date);
    headers.put("Authorization", authorization);
    return headers;
  }

  // 署名付きヘッダー (signed headers)
  private static String createSignedHeaders(TreeMap<String, String> headers) {
    StringBuilder signedHeaderBuilder = new StringBuilder();
    for (String key : headers.keySet()) {
      signedHeaderBuilder.append(key).append(";");
    }
    return signedHeaderBuilder.substring(0, signedHeaderBuilder.length() - 1);
  }

  // 正規リクエスト (canonical request)
  private static String createCanonicalRequest(String path, TreeMap<String, String> headers, String signedHeaders, String payload) throws NoSuchAlgorithmException {
    StringBuilder canonicalRequest = new StringBuilder();
    canonicalRequest.append("POST").append("\n");
    canonicalRequest.append(path).append("\n").append("\n");
    for (String key : headers.keySet()) {
      canonicalRequest.append(key).append(":").append(headers.get(key)).append("\n");
    }
    canonicalRequest.append("\n");
    canonicalRequest.append(signedHeaders).append("\n");
    canonicalRequest.append(sha256(payload));
    return canonicalRequest.toString();
  }

  // 署名文字列 (string to sign)
  private static String createStringToSign(Date current, String region, String canonicalRequest) throws NoSuchAlgorithmException {
    return HMAC_ALGORITHM + "\n"
      + getXAmzDateString(current) + "\n"
      + getYMDString(current) + "/" + region + "/" + SERVICE + "/" + AWS_4_REQUEST + "\n"
      + sha256(canonicalRequest);
  }

  // 署名 (signature)
  private static String calculateSignature(String awsSecretKey, Date current, String region, String stringToSign) throws InvalidKeyException, NoSuchAlgorithmException {
    final String currentDate = getYMDString(current);
    byte[] signatureKey = getSigningKey(awsSecretKey, currentDate, region);
    byte[] signature = hmacSha256(signatureKey, stringToSign);
    return bytesToHex(signature);
  }

  // 署名キー (signing key)
  private static byte[] getSigningKey(String key, String date, String region) throws InvalidKeyException, NoSuchAlgorithmException {
    // 各ハッシュ関数の結果が次のハッシュ関数の入力になる
    byte[] kSecret = ("AWS4" + key).getBytes(StandardCharsets.UTF_8);
    byte[] kDate = hmacSha256(kSecret, date);
    byte[] kRegion = hmacSha256(kDate, region);
    byte[] kService = hmacSha256(kRegion, SERVICE);
    byte[] kSigning = hmacSha256(kService, AWS_4_REQUEST);
    return kSigning;
  }

  // Authorization ヘッダー値
  private static String buildAuthorizationString(String awsAccessKey, String region, String signature, String signedHeaders, Date current) {
    return HMAC_ALGORITHM + " "
      + "Credential=" + awsAccessKey + "/" + getYMDString(current) + "/" + region + "/" + SERVICE + "/" + AWS_4_REQUEST + ","
      + "SignedHeaders=" + signedHeaders + ","
      + "Signature=" + signature;
  }

  // ハッシュ関数 SHA-256
  private static String sha256(String data) throws NoSuchAlgorithmException {
    MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
    messageDigest.update(data.getBytes(StandardCharsets.UTF_8));
    byte[] digest = messageDigest.digest();
    return String.format("%064x", new java.math.BigInteger(1, digest));
  }

  // HMAC-SHA256 関数
  private static byte[] hmacSha256(byte[] key, String data) throws NoSuchAlgorithmException, InvalidKeyException {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(key, "HmacSHA256"));
    return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
  }

  // バイナリ値を 16 進数表現に変換
  private static String bytesToHex(byte[] data) {
    final char[] hexCode = "0123456789ABCDEF".toCharArray();
    StringBuilder r = new StringBuilder(data.length * 2);
    for (byte b : data) {
      r.append(hexCode[(b >> 4) & 0xF]);
      r.append(hexCode[(b & 0xF)]);
    }
    return r.toString().toLowerCase();
  }

  // x-amz-date ヘッダ用日時文字列 (UTC で YYYYMMDD'T'HHMMSS'Z' の ISO 8601 形式)
  private static String getXAmzDateString(Date date) {
    DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"); // ISO 8601
    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
    return dateFormat.format(date);
  }

  // 日付文字列 yyyyMMdd 形式
  private static String getYMDString(Date date) {
    DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
    return dateFormat.format(date);
  }
}

src/main/java/JsonUtil.java

// 外部ライブラリの Jackson を使う
import com.fasterxml.jackson.core.json.JsonWriteFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;

/**
 * JSON 操作クラス。
 */
public class JsonUtil {

  /**
   * オブジェクトから JSON 文字列を生成する。
   * @param obj オブジェクト
   * @return JSON 文字列
   * @throws IOException
   */
  public String objectToJson(Map<String, Object> obj) throws IOException {
    StringWriter out = new StringWriter();
    ObjectMapper mapper = new ObjectMapper();
    // ASCII 文字以外は Unicode escape する
    mapper.configure(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature(), true);
    mapper.writerWithDefaultPrettyPrinter().writeValue(out, obj);
    return out.toString();
  }

  /**
   * JSON を読みやすい形にする。
   * @param json JSON 文字列
   * @return 読みやすい形になった JSON 文字列
   * @throws IOException
   */
  public String prettyPrint(String json) throws IOException {
    StringWriter out = new StringWriter();
    ObjectMapper mapper = new ObjectMapper();
    mapper.writerWithDefaultPrettyPrinter().writeValue(out, mapper.readValue(json, Map.class));
    return out.toString();
  }
}

サンプルコードの実行結果

実行環境: macOS Catalina + Java 15 (AdoptOpenJDK 15) + Gradle 6.6.1

$ gradle run
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :run
===== キーワードから商品を検索: リクエスト =====
{
  "PartnerType" : "Associates",
  "PartnerTag" : "XXXXX-22",
  "Keywords" : "\u30B7\u30A7\u30A4\u30AF\u30B9\u30D4\u30A2",
  "SearchIndex" : "All",
  "ItemCount" : 3,
  "Resources" : [ "ItemInfo.Title", "ItemInfo.ByLineInfo", "ItemInfo.ProductInfo", "ItemInfo.ProductInfo", "Images.Primary.Large", "Images.Primary.Medium", "Images.Primary.Small" ]
}
===== キーワードから商品を検索: レスポンス =====
{
  "SearchResult" : {
    "Items" : [ {
      "ASIN" : "B06ZZH149Y",
      "DetailPageURL" : "https://www.amazon.co.jp/dp/B06ZZH149Y?tag=XXXXX-22&linkCode=osi&th=1&psc=1",
      "Images" : {
        "Primary" : {
          "Large" : {
            "Height" : 500,
            "URL" : "https://m.media-amazon.com/images/I/41Ms+C0NwNL.jpg",
            "Width" : 311
          },
          "Medium" : {
            "Height" : 160,
            "URL" : "https://m.media-amazon.com/images/I/41Ms+C0NwNL._SL160_.jpg",
            "Width" : 100
          },
          "Small" : {
            "Height" : 75,
            "URL" : "https://m.media-amazon.com/images/I/41Ms+C0NwNL._SL75_.jpg",
            "Width" : 47
          }
        }
      },
      "ItemInfo" : {
        "ByLineInfo" : {
          "Contributors" : [ {
            "Locale" : "ja_JP",
            "Name" : "河合祥一郎",
            "Role" : "著",
            "RoleType" : "author"
          } ],
          "Manufacturer" : {
            "DisplayValue" : "祥伝社",
            "Label" : "Manufacturer",
            "Locale" : "ja_JP"
          }
        },
        "ProductInfo" : {
          "IsAdultProduct" : {
            "DisplayValue" : false,
            "Label" : "IsAdultProduct",
            "Locale" : "en_US"
          },
          "ReleaseDate" : {
            "DisplayValue" : "2017-04-21T00:00:00.000Z",
            "Label" : "ReleaseDate",
            "Locale" : "en_US"
          }
        },
        "Title" : {
          "DisplayValue" : "あらすじで読むシェイクスピア全作品 (祥伝社新書)",
          "Label" : "Title",
          "Locale" : "ja_JP"
        }
      }
    }, {
      "ASIN" : "B015BY1Q6Q",
      "DetailPageURL" : "https://www.amazon.co.jp/dp/B015BY1Q6Q?tag=XXXXX-22&linkCode=osi&th=1&psc=1",
      "Images" : {
        "Primary" : {
          "Large" : {
            "Height" : 500,
            "URL" : "https://m.media-amazon.com/images/I/516XD+o35gL.jpg",
            "Width" : 375
          },
          "Medium" : {
            "Height" : 160,
            "URL" : "https://m.media-amazon.com/images/I/516XD+o35gL._SL160_.jpg",
            "Width" : 120
          },
          "Small" : {
            "Height" : 75,
            "URL" : "https://m.media-amazon.com/images/I/516XD+o35gL._SL75_.jpg",
            "Width" : 56
          }
        }
      },
      "ItemInfo" : {
        "ByLineInfo" : {
          "Contributors" : [ {
            "Locale" : "ja_JP",
            "Name" : "メル・ギブソン",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "グレン・クローズ",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "アラン・ベイツ",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ポール・スコフィールド",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "フランコ・ゼフィレッリ",
            "Role" : "監督",
            "RoleType" : "director"
          }, {
            "Locale" : "ja_JP",
            "Name" : "クリストファー・デヴォア",
            "Role" : "Writer",
            "RoleType" : "writer"
          } ]
        },
        "ProductInfo" : {
          "IsAdultProduct" : {
            "DisplayValue" : false,
            "Label" : "IsAdultProduct",
            "Locale" : "en_US"
          },
          "ReleaseDate" : {
            "DisplayValue" : "2015-09-16T00:00:00.000Z",
            "Label" : "ReleaseDate",
            "Locale" : "en_US"
          }
        },
        "Title" : {
          "DisplayValue" : "ハムレット(字幕版)",
          "Label" : "Title",
          "Locale" : "ja_JP"
        }
      }
    }, {
      "ASIN" : "B07WPXRT5W",
      "DetailPageURL" : "https://www.amazon.co.jp/dp/B07WPXRT5W?tag=XXXXX-22&linkCode=osi&th=1&psc=1",
      "Images" : {
        "Primary" : {
          "Large" : {
            "Height" : 375,
            "URL" : "https://m.media-amazon.com/images/I/51CnJBKwu5L.jpg",
            "Width" : 500
          },
          "Medium" : {
            "Height" : 120,
            "URL" : "https://m.media-amazon.com/images/I/51CnJBKwu5L._SL160_.jpg",
            "Width" : 160
          },
          "Small" : {
            "Height" : 56,
            "URL" : "https://m.media-amazon.com/images/I/51CnJBKwu5L._SL75_.jpg",
            "Width" : 75
          }
        }
      },
      "ItemInfo" : {
        "ByLineInfo" : {
          "Contributors" : [ {
            "Locale" : "ja_JP",
            "Name" : "マーク・ベントン",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ジョー・ジョイナー",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "アンバー・アガ",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "リチャード・サイニー",
            "Role" : "監督",
            "RoleType" : "director"
          }, {
            "Locale" : "ja_JP",
            "Name" : "イアン・バーバー",
            "Role" : "監督",
            "RoleType" : "director"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ウィル・トロッター",
            "Role" : "プロデュース",
            "RoleType" : "producer"
          } ]
        },
        "ProductInfo" : {
          "IsAdultProduct" : {
            "DisplayValue" : false,
            "Label" : "IsAdultProduct",
            "Locale" : "en_US"
          }
        },
        "Title" : {
          "DisplayValue" : "第1話",
          "Label" : "Title",
          "Locale" : "ja_JP"
        }
      }
    } ],
    "SearchURL" : "https://www.amazon.co.jp/s?k=%E3%82%B7%E3%82%A7%E3%82%A4%E3%82%AF%E3%82%B9%E3%83%94%E3%82%A2&rh=p_n_availability%3A-1&tag=XXXXX-22&linkCode=osi",
    "TotalResultCount" : 146
  }
}
===== ASIN から商品情報を取得: リクエスト =====
{
  "PartnerType" : "Associates",
  "PartnerTag" : "XXXXX-22",
  "Resources" : [ "ItemInfo.Title", "ItemInfo.ByLineInfo" ],
  "ItemIds" : [ "4391641585", "B010EB1HR4", "B0125SPF90", "B07V52KSGT" ]
}
===== ASIN から商品情報を取得: レスポンス =====
{
  "ItemsResult" : {
    "Items" : [ {
      "ASIN" : "4391641585",
      "DetailPageURL" : "https://www.amazon.co.jp/dp/4391641585?tag=XXXXX-22&linkCode=ogi&th=1&psc=1",
      "ItemInfo" : {
        "ByLineInfo" : {
          "Contributors" : [ {
            "Locale" : "ja_JP",
            "Name" : "サンエックス",
            "Role" : "監修",
            "RoleType" : "consultant_editor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "主婦と生活社",
            "Role" : "編集",
            "RoleType" : "editor"
          } ],
          "Manufacturer" : {
            "DisplayValue" : "主婦と生活社",
            "Label" : "Manufacturer",
            "Locale" : "ja_JP"
          }
        },
        "Title" : {
          "DisplayValue" : "すみっコぐらし検定公式ガイドブック すみっコぐらし大図鑑 (生活シリーズ)",
          "Label" : "Title",
          "Locale" : "ja_JP"
        }
      }
    }, {
      "ASIN" : "B010EB1HR4",
      "DetailPageURL" : "https://www.amazon.co.jp/dp/B010EB1HR4?tag=XXXXX-22&linkCode=ogi&th=1&psc=1",
      "ItemInfo" : {
        "ByLineInfo" : {
          "Brand" : {
            "DisplayValue" : "Hostess Entertainmen",
            "Label" : "Brand",
            "Locale" : "ja_JP"
          },
          "Contributors" : [ {
            "Locale" : "ja_JP",
            "Name" : "ハロウィン",
            "Role" : "アーティスト",
            "RoleType" : "artist"
          } ],
          "Manufacturer" : {
            "DisplayValue" : "ホステス",
            "Label" : "Manufacturer",
            "Locale" : "ja_JP"
          }
        },
        "Title" : {
          "DisplayValue" : "守護神伝 第二章 <エクスパンデッド・エディション>(リマスター)",
          "Label" : "Title",
          "Locale" : "ja_JP"
        }
      }
    }, {
      "ASIN" : "B0125SPF90",
      "DetailPageURL" : "https://www.amazon.co.jp/dp/B0125SPF90?tag=XXXXX-22&linkCode=ogi&th=1&psc=1",
      "ItemInfo" : {
        "ByLineInfo" : {
          "Brand" : {
            "DisplayValue" : "ルービーズジャパン(RUBIE'S JAPAN)",
            "Label" : "Brand",
            "Locale" : "ja_JP"
          },
          "Manufacturer" : {
            "DisplayValue" : "ルービーズジャパン(RUBIE'S JAPAN)",
            "Label" : "Manufacturer",
            "Locale" : "ja_JP"
          }
        },
        "Title" : {
          "DisplayValue" : "ハロウィン ロッキング パンプキン ホームデコレーション用小物 H 130cm",
          "Label" : "Title",
          "Locale" : "ja_JP"
        }
      }
    }, {
      "ASIN" : "B07V52KSGT",
      "DetailPageURL" : "https://www.amazon.co.jp/dp/B07V52KSGT?tag=XXXXX-22&linkCode=ogi&th=1&psc=1",
      "ItemInfo" : {
        "ByLineInfo" : {
          "Contributors" : [ {
            "Locale" : "ja_JP",
            "Name" : "リアン・リース",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ジェイミー・リー・カーティス",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ウィル・パットン",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ジュディ・グリア",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ヴァージニア・ガードナー",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ジェファーソン・ホール",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "アンディ・マティチャック",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ニック・キャッスル",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ジェームス・ジュード・コートニー",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ハルク・ビルギナー",
            "Role" : "出演",
            "RoleType" : "actor"
          }, {
            "Locale" : "ja_JP",
            "Name" : "デヴィッド・ゴードン・グリーン",
            "Role" : "監督",
            "RoleType" : "director"
          }, {
            "Locale" : "ja_JP",
            "Name" : "デヴィッド・ゴードン・グリーン",
            "Role" : "Writer",
            "RoleType" : "writer"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ダニー・マクブライド",
            "Role" : "Writer",
            "RoleType" : "writer"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ジェフ・フラッドリー",
            "Role" : "Writer",
            "RoleType" : "writer"
          }, {
            "Locale" : "ja_JP",
            "Name" : "マレク・アカッド",
            "Role" : "プロデュース",
            "RoleType" : "producer"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ジェイソン・ブラム",
            "Role" : "プロデュース",
            "RoleType" : "producer"
          }, {
            "Locale" : "ja_JP",
            "Name" : "ビル・ブロック",
            "Role" : "プロデュース",
            "RoleType" : "producer"
          } ]
        },
        "Title" : {
          "DisplayValue" : "ハロウィン (字幕版)",
          "Label" : "Title",
          "Locale" : "ja_JP"
        }
      }
    } ]
  }
}

BUILD SUCCESSFUL in 13s
2 actionable tasks: 2 executed

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS IAMでユーザーを作成しよう

AWSのアカウントを作ろうでアカウント作成するとルートアカウントだけ作成されます。ここでは、作成したAWSアカウントのIAMでユーザーを作成してみましょう。

IAMユーザーをなぜ作成するのか

別な記事にも書きましたが、ルートアカウントは、Administrator権限を有する非常に強力なアカウントであるため、開発や運用時には制限したIAMユーザーアカウントで操作することが推奨されます。
誤操作することを防ぐ目的があり、また、操作する人やプログラムへユーザーやロールを割り当てることで誰が操作をおこなったのか?という証跡を残す目的もあります。
(と書いたけど、少し雑な言い方かもしれない。あとで書き直す)

この記事で行うこと

AWSのIAMユーザーを追加する。

事前に決めておくこと

AWSでは、ユーザーへ様々な権限を付与することができます。そのため、ユーザーへ付与する権限を事前に決めておく必要があります。

権限付与でオススメする方法

IAMでグループを作成し、作成したグループに必要な権限を付与しておきます。
本記事の手順で権限を付与する部分がありますが、その時にグループを指定する方法をおすすめします。

IAMユーザーを追加する手順

  1. AWSコンソールへログイン
  2. IAMへアクセスし、ユーザー追加をする
  3. ユーザーのIDや詳細情報を登録する
  4. ユーザーへ権限を設定する

AWSコンソールへログイン

ルートアカウントへログインし、AWSコンソールを表示します

スクリーンショット 2020-09-25 14.01.55.png

IAMへアクセスし、ユーザー追加をする

スクリーンショット 2020-09-25 14.02.18.png

スクリーンショット 2020-09-25 14.02.30.png

ユーザーのIDや詳細情報を登録する

まずはユーザ名を入力します。これは、ログインする際に利用するユーザーIDになります。そして、アクセスの種類を選択します。

・プログラムによるアクセス:
 コマンドラインやAPIなどプログラムからアクセスを許す場合にはチェックを付けましょう。
 アクセスキーとシークレットアクセスキーが払い出されます。
・AWSマネジメントコンソール:
 ルートアカウントと同じく、AWSコンソールを使用する場合にはチェックを付けましょう。
 AWSコンソールへアクセスするためのパスワードが払い出されます。

2つのチェックボックスは、ユーザーを作成した後でも有効化することが可能です。

スクリーンショット 2020-09-25 14.02.53.png

スクリーンショット 2020-09-25 14.03.26.png

ユーザーへ権限を設定する

作成するユーザーへ権限を設定してください。
グループを割り当てるか、それとも既存のポリシーを直接選択することも可能です。

スクリーンショット 2020-09-25 14.04.29.png

タグを追加する

AWSでは作成したリソースへタグをつけて管理することができます。

スクリーンショット 2020-09-25 14.04.46.png

作成されるユーザーの確認

スクリーンショット 2020-09-25 14.05.01.png

作成されたユーザーへの認証情報

ユーザー作成が成功した場合、ユーザーへアクセスするための認証情報が表示されます。
パスワードやシークレットアクセスキーは、本画面でしか表示されないため、コピペなどで保持するようにしてください。

スクリーンショット 2020-09-25 14.05.51.png

補足

認証情報を紛失してしまってもルートアカウントにてIAMユーザーの認証情報を再発行することができます。

一覧で作成されたIAMユーザーを確認

IAMユーザーの一覧で、作成されたユーザーを確認できます。
また、この画面ではアクセスキーやパスワードがいつ設定されたのかを確認することができ、それらの認証情報がいつ使われたのかという最後のアクティビティも確認することが可能です。

スクリーンショット 2020-09-25 14.09.21.png

AWS公式ドキュメント

AWS アカウントでの IAM ユーザーの作成

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonで天気予報botもどきを作ってみた。

Pythonで天気予報botもどきを作ってみた。

タイトル通りPythonで天気予報botもどきを作ってみました(botではないです)。
天気予報を確認することすらめんどくさがってしまう性格で「LINEで送れたらなー」と思っていた所、既に先人の方達がやっていたので、知恵を借りながら(ほぼパクリながら)作ってみました。

やったこと

・スクレイピングでYahooの天気情報を取得
・スクレイピングで取得した情報をLINE Notifyで表示

準備

必要なライブラリのインストール

$pip install beautifulsoup4
$pip install requests

トークンの取得

LINENotifyでトークンを発行しておきます。

コード

import urllib.request
import requests
from bs4 import BeautifulSoup

line_notify_token = 'xxxxxxxxxxxxxxxxxxxxx'#発行したトークンを使います。
line_notify_api = 'https://notify-api.line.me/api/notify'

rssurl = "https://rss-weather.yahoo.co.jp/rss/days/3410.xml"#このコードでは仙台の天気情報を取得します。

URL = "https://weather.yahoo.co.jp/weather/jp/8/3410/8201.html"

tenki = []
detail = []


def Parser(rssurl):
   with urllib.request.urlopen(rssurl) as res:
      xml = res.read()
      soup = BeautifulSoup(xml, "html.parser")
      for item in soup.find_all("item"):
         title = item.find("title").string
         description = item.find("description").string
         if title.find("[ PR ]") == -1:
            tenki.append(title)
            detail.append(description)

def Otenki():
    Parser(rssurl)
    for i in range(0,2):
        message = tenki[i]
        payload = {'message': "\n" + message}
        headers = {'Authorization': 'Bearer ' + line_notify_token}
        line_notify = requests.post(line_notify_api, data=payload, headers=headers)

Otenki()

実行結果

IMG_8243.jpg
しっかり送られてきて嬉しい気持ちになりました。

感想

本当はAWS,Herokuを使って自動化までやりたかったですが、知識が何も無い状態で飛び込んで高額請求がきたら対処出来ないので、今回はここまでにしておきました笑。
scheduleライブラリをインストールしてみても良いのかも知れません。
自分で動かしてみると分からないなりにも色々出来て楽しかったです。

またスキルを身に付けていきながらこの記事も更新していきたいです。

参考記事

【Yahoo!天気リプレース版】LINE Notify + Pythonで天気情報を取得する方法
Pythonで天気予報をLINE通知する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Pythonを使って天気予報botもどきを作ってみた。

Pythonを使って天気予報botもどきを作ってみた。

タイトル通りPythonを使ってLineで天気予報のお知らせがくるbotもどきを作ってみました。
天気予報を見ることすら面倒なめんどくさがり屋なので、「何とか出来ないかなー」と考えていたら先人の方達が似たようなものを作っていたので99%パクリで作ってみました。

行ったこと

・Yahooの天気予報で天気に関する情報をスクレイピングで取得する
・スクレイピングで取得した情報をLine Notifyで表示する

準備

必要なライブラリのインストール

$pip install beautifulsoup4
$pip install requests

LineNotifyのトークン発行

LineNotifyでトークンを発行して以下のコードで貼り付けてください。

コード

import urllib.request
import requests
from bs4 import BeautifulSoup

line_notify_token = 'xxxxxxxxxxxxxxxxxxx'#自分で発行したトークンを貼り付けてください.
line_notify_api = 'https://notify-api.line.me/api/notify'

rssurl = "https://rss-weather.yahoo.co.jp/rss/days/3410.xml"#デフォルトでは仙台市の天気情報になっています.
URL = "https://weather.yahoo.co.jp/weather/jp/8/3410/8201.html"

tenki = []
detail = []


def Parser(rssurl):
   with urllib.request.urlopen(rssurl) as res:
      xml = res.read()
      soup = BeautifulSoup(xml, "html.parser")
      for item in soup.find_all("item"):
         title = item.find("title").string
         description = item.find("description").string
         if title.find("[ PR ]") == -1:
            tenki.append(title)
            detail.append(description)

def Otenki():
    Parser(rssurl)
    for i in range(0,2):
        message = tenki[i]
        payload = {'message': "\n" + message}
        headers = {'Authorization': 'Bearer ' + line_notify_token}
        line_notify = requests.post(line_notify_api, data=payload, headers=headers)

Otenki()

実行結果

ちゃんと送られてきて嬉しい気持ちになりました。
IMG_8243.jpg

感想

先人の方達のおかげでほとんど詰まることもなく作成することが出来ました。
本当はAWSやHerokuを使って自動化までしたかったですが、全く知識がない状態で飛び込んで高額請求とか来たら対処出来そうもないので、とりあえずここまでにしておきました。
scheduleライブラリをインストールしてやってみても面白いかも知れません。
初めてアプリもどきを作ってみたのですが、出来は良くないし、完全に自己満足なのですが、Lineに通知が来た時、結構嬉しかったです。
スキルが付いてきたらまた記事も改良していこうと思います。

参考記事

【Yahoo!天気リプレース版】LINE Notify + Pythonで天気情報を取得する方法
Pythonで天気予報をLINE通知する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Athena】既存データをPartition分割する

今使っているデータをPartition分割したい

Athenaで利用しているデータが拡大していき、費用削減・速度向上を狙ってPartitionでデータを分割したくなることがあると思います。
初期からデータを分けていた場合は問題がないのですが、何も考えていなかった場合、1ファイルまたは1ディレクトリ(S3にディレクトリはありませんが便宜上ディレクトリと表現します)に全てのデータが入っていることがあります。
この際、CTAS(CREATE TABLE AS SELECT)クエリで分割、というかデータを再構築します。

分割例

少し長いですが以下のようなデータを用意します。

id name team_id year
1 佐藤 2 2019
2 高橋 2 2019
3 近藤 3 2019
4 田中 1 2019
5 山田 2 2019
6 野村 4 2019
7 河合 2 2019
8 五十嵐 1 2019
9 川端 4 2019
10 比屋根 3 2019
11 上田 2 2020
12 村上 4 2020
13 5 2020
14 歳内 3 2020
15 西浦 2 2020
16 西田 1 2020
17 中村 4 2020
18 畠山 5 2020
19 真中 3 2020
20 高津 2 2020

このデータを元にAthenaテーブルを作成します。

create.sql
CREATE EXTERNAL TABLE IF NOT EXISTS db_name.partition_test (
  `id` int,
  `name` string,
  `team_id` int,
  `year` int 
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'
WITH SERDEPROPERTIES (
  'serialization.format' = ',',
  'field.delim' = ','
) LOCATION 's3://example_bucket/partition_test/original/'
TBLPROPERTIES ('has_encrypted_data'='false');

これはCSVファイルをただ読み取るだけのcreate文です。
現在はs3://example_bucket/partition_test/original/ディレクトリに全テータを入れています。

このファイルを分割するCTAS文が

ctas.sql
CREATE TABLE db_name.partitioned_test 
WITH (
     format = 'PARQUET',  
     external_location = 's3://example_bucket/partition_test/partitioned/', 
     partitioned_by = ARRAY['team_id', 'year']
) 
AS SELECT id, name, team_id, year FROM partition_test;

これを実行することでlsコマンドでディレクトリ構造を見てみると…

$ aws s3 ls s3://example_bucket/partition_test/partitioned/
                           PRE team_id=1/
                           PRE team_id=2/
                           PRE team_id=3/
                           PRE team_id=4/
                           PRE team_id=5/
$ aws s3 ls s3://example_bucket/partition_test/partitioned/team_id=1/
                           PRE year=2019/
                           PRE year=2020/

このようにteam_idyearでデータが分割されています。これでwhere句でteam_idyearを絞るとアクセスするファイルを減らせるため速度・費用の節約を図ることができます。

しかし、今回のようにデータ量が少ない場合は、where句で条件を指定しない場合逆にS3へのリクエスト量が増えるため費用は上がってしまいます。
Athenaはどのような読み取り量も最低でも10MBとなる点、S3の費用も考えてデータを構成する必要があります。
ファイルサイズの最適化でも書きましたが、1ファイルサイズは128MB程度になるのが一番オーバーヘッドが少なくなります。もちろんこのような調整は簡単でないことも多いのですが、気に留めておく必要あります。

エラー

HIVE_COLUMN_ORDER_MISMATCH: Partition keys must be the last columns in the table and in the same order as the table properties: [team_id, year]. You may need to manually clean the data at location

CTAS文を実行した際にこのようなエラーが出る場合があります。これは読んでそのままなのですが、パーティションキーに指定したものはselect文の末尾に順番通り並べる必要があります。
なので今回の例でいうと

SELECT id, name, year, team_id FROM partition_test;

だと上記エラーが出ます。エラー文読めば問題なく対応できるのですが、直感的にこのようなエラーが出ることは予想できないので注意が必要です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSのEC2でrootになれなくなった場合の対処方法

ubuntuユーザーのsudoers設定を変更してしまい、スーパーユーザーになれなくなった場合の対処法

  1. AWSマネジメントコンソールにログインする
  2. EC2を開く
  3. インスタンスを選択する
  4. インスタンスを停止する(停止時に削除されないように注意)
  5. 「アクションメニュー」の「インスタンスの設定」→「ユーザーデータの表示/変更」を選ぶ
  6. ユーザーデータに下記を記入

cloud-config

bootcmd:
- echo "ubuntu ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers
7. 「保存」を押した後、インスタンスを起動する
8. ubuntuユーザーで、パスワードなしで「sudo su -」が出来るか確認
9. 出来ることが確認出来たら、インスタンスを停止し、ユーザーデータに記述したものを削除
10. インスタンスを起動

以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS QuickSightに関して

AWS QuickSightとは?

AWS QuickSightとはサーバーレスでフルマネージドなBIサービスになります。S3にあるデータはもちろんのことRDS,Athena,Redshift、オンプレミスにあるJDBC接続(JDBC:javaで書かれたプログラムとデータベースの仲立ちをするもの)可能なデータベースのデータなどと連携して可視化を行うことが出来る。SPICEというインメモリ型の高速データベースが内蔵されており、そこにデータを取り込んでおくとデータソースに負荷をかけることなく高速なクエリが可能になる。

QuickSightを実際に使用してみた。

まずはCSVファイルをアップロードします。新しい分析というところをクリックし「新しいデータセット」→「ファイルのアップロード」を選択し分析したいデータをアップロードします。
2020-09-25 (1).png

そうすると下図のような画面になるので問題がなければ「保存して視覚化」を押します。ここではカラム名を変更したり計算フィールドを使用した変更を行ったりすることが出来ます。
2020-09-25 (2).png

次に可視化を行っていきたいと思います。画面の左側にあるフィールドリストには読み込んだデータのカラムがあります。それを使用して可視化を行っていきます。デフォルトで「AutoGraph」となっているため、カラムを選択するだけで自動的にビジュアルタイプを選択してくれます。左上にある「追加(+)」のところで「ビジュアルを追加」というところを選択すると複数のビジュアルを選択することが可能です。
2020-09-25 (3).png

先ほどは「AutoGraph」による自動的な可視化でしたが、自分自身でビジュアルのタイプを選んで可視化したい場合はどのように行うとよいでしょうか?それはビジュアルタイプを自分で選択すれば可能になります。左下にビジュアルタイプがあるので可視化したいタイプを選択します(今回はピボットテーブル)。その次に使用したいフィールドを選択しフィールドウェルの「行」「列」「値」にそれぞれドラッグします。このように簡単にピボットテーブルを作成することが出来ます。
2020-09-25 (4).png

続いてフィルターを使用してみたいと思います。フィルターを使用する事で表示する範囲を絞れたり、特定のものを抽出したりすることが出来ます。フィルターを使用するには左側にあるフィルターを選択します。今回はオーダー日にフィルターをかけたいのでオーダー日を選択し開始日と終了日の設定を行います。そうすると指定した範囲のみの表示を行うことが出来ます。またパラメータを使用した分析を行いたい場合は「パラメータを使用」にチェックすることで使用できます。パラメータの作成は左側にあるパラメータというところでパラメータを作成することによって使用する事が出来ます。
2020-09-25 (6).png
売上への寄与度の分析を行います。売上への寄与度を分析する際には、棒グラフをクリックし、「売上への寄与度を分析」を選択します。そうすることによって左側に分析結果が表示されます。結果を見てみると大阪府が売り上げの増加に一番貢献したということが分かります。
2020-09-25 (7).png

続いて機械学習による予測を行っていきたいと思います。今回は売り上げの予測をしていきます。売り上げの予測も難しいわけではなくクリックだけで実装することが出来ます。「ビジュアルタイプ」に折れ線グラフを選択し可視化しました。その後「…」を押し「予測の追加」を押します。そうするとオレンジ色の予測結果が可視化されます。見方としては、濃いオレンジの実践を中心に薄いオレンジの幅で売上が推移すると予測しています。
2020-09-25 (8).png

このように可視化をするのであればQuickSightは有用であると思います。しかしながら従量課金制であるため、料金には注意が必要になってくると思います。

<参考にさせていただいたサイト>
JDBCとは
AWSではじめるデータレイク: クラウドによる統合型データリポジトリ構築入門

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EBSのファーストタッチペナルティをくらったときに反省したこと

小ネタ。

久々にファーストタッチペナルティをくらって反省したので、
ポストモーテム的なものをまとめます。
出せる部分だけ。

発生事象

概要

ファーストタッチペナルティによるレスポンスの遅延及びエラーの発生

image.png

ELBの平均レスポンスタイムとパーセンタイルのグラフ。
紫の線がレスポンスタイム。
ほとんどのリクエストのレスポンスタイムが悪化している。

詳細

  1. あるwebサーバのネットワークの帯域が不足し始めたので、スケールアウトすることで対処しようとした。
  2. ディスクの高IOが必要で、オートスケールは設定していないサーバだった。
  3. EC2で作成されており、定期的に取得しているAMIから復元しようとしていた。
  4. EBSに800GB程度のデータを保持していた。
  5. AMIからインスタンス構築し、追加した台数分ロードバランサの配下に登録した。
  6. 登録したインスタンスにバランシングされだしたタイミングからレスポンスタイム及びエラー率が上昇した。
  7. 登録したインスタンスのログからエラー等は未検出だった。
  8. 調査したところディスクのIOPSが既存環境の1/7程度の性能となっていた。
  9. ロードバランサから切り離し回復した。

原因

直接原因

  • ファーストタッチペナルティにより、ファイル読み込みに遅延、結果レスポンスが遅延した

正常時のCPUの状態
※緑色の部分がiowait、それ以外はsystemやuser、stealなど。
image.png

問題発生当時のCPUの状態
※system,userなども本来含まれているが、iowaitが占めていて他の値が見えないほどになっている
image.png

対策が必要そうな項目

  • ファーストタッチペナルティが起こることを認識できていなかった
  • 事前の動作確認でIOの性能の確認は含まれていなかった
  • 復旧手順を参考に復旧するも、実際に復旧させたときに発覚するたぐいの問題だった

対応

  • grep hoge -r /hoge で、既存ファイル読み込むことでブロックにアクセスした。※公式はddやfioをお勧めしてる
  • IOPSが正常時動作と同等程度出ることを確認した。
  • 再度ロードバランサに接続し問題ないことを確認した。

再発防止策

  • AMIからの復旧が必要な場合
  • 高IOが必要なディスクに対して
    • スナップショットから復元するような運用をやめる
    • EBSを作り直しデータを他からダウンロードするなどの仕組みにする
    • 作成時にディスクを確保(新規作成)するようにする

見送った再発防止策

  • 復旧訓練
    • 一回同じことをやったことがあり、その際は運よく問題にならなかった
    • 定期的な運用見直しで、定期的にファーストタッチペナルティのことを思い出すよう訓練を行うことは、人員と工数とコストの兼ね合いから難しい
  • 高IOの復元だけ必ずFSRを利用するよう自動化する
    • こういう自動化やアラートがないとFSRを使うというノウハウが忘れ去られ再発するリスクは免れない
    • 同じような条件をどう判定するか、判定にかかる部分の自動化が困難と思われる

感想

再発防止策と書きましたが、防止策というより、再発したときに何をやるのかという内容で、反省としてはあまり良くないかもしれません。
何か良さそうな防止策があれば教えていただきたいところです。

そもそもファーストタッチペナルティがどういう時に起きるか知ってても、実際の事象と結び付けられないのが良くないです。
こういうことが起きると気をつけなきゃってなるけど、しばらくしたら忘れると思うのであまり意味がないです。
起きないために気づく方法は現状テストしかないけど、テストの観点にも無ければテストもできない。
テストの観点に含めるためには経験を積まないといけないしでデッドロック。
どうしたらいいんだろ・・・?一律リリース時にディスクパフォーマンスを計測する?
みんなしてるのかなあ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSルートアカウントへMFAを設定する

AWSのアカウントを作ろうで作成したルートアカウントへMFAを設定します。

なぜMFAをおこなうのか

ルートアカウントは、Administrator権限を有する非常に強力なアカウントなので、セキュリティを向上させるためにMFAを設定することが推奨されています。

ここで行うこと

AWSのルートアカウントの多要素認証へ仮想MFAデバイスを設定する。

MFAを設定する手順

  1. AWSコンソールへログイン
  2. マイセキュリティ資格情報へアクセス
  3. セキュリティ認証情報で多要素認証(MFA)を有効化する
  4. 設定するMFAデバイスを選択する
  5. 仮想MFAデバイスの設定をおこなう
  6. セキュリティ認証情報で多要素認証(MFA)が有効化されたことを確認する

AWSコンソールへログイン

ルートアカウントへログインし、AWSコンソールを表示します

スクリーンショット 2020-09-25 11.51.45.png

マイセキュリティ資格情報へアクセス

右上のメニューバーの中にあるマイセキュリティ資格情報からセキュリティ認証情報へアクセスします

スクリーンショット 2020-09-25 11.52.07.png

セキュリティ認証情報で多要素認証(MFA)を有効化する

多要素認証(MFA)のセクションを開き、MFAの有効化をクリックします

スクリーンショット 2020-09-25 11.52.22.png

設定するMFAデバイスを選択する

MFAデバイスをここでは仮想MFAデバイスを選択します。

スクリーンショット 2020-09-25 11.52.57.png

補足

ハードウェアMFAデバイスを指定することも可能ですが、AWSがサポートされているMFAデバイスが必要になりますので、購入する際にはご注意ください。

仮想MFAデバイスの設定をおこなう

この画面へ表示されたQRコードもしくはシークレッキーを仮想MFAデバイスとなるアプリケーションへ入力してMFAコードを入手しましょう。

MFAコードが2つあり、ここには時間差で2つのコードを入力する必要があります。
まずは、コード1へ仮想MFAデバイスアプリへ表示されいてるコードを入力して、そして、30秒ほど待ち、新しく表示されたコードをコード2へ入力して、MFAの割り当てをクリックします。

スクリーンショット 2020-09-25 11.53.22.png

セキュリティ認証情報で多要素認証(MFA)が有効化されたことを確認する

多要素認証(MFA)セクションへデバイスタイプ仮想のMFAが登録されていることを確認しましょう

スクリーンショット 2020-09-25 11.55.02.png

AWS公式ドキュメント

AWS アカウントの root ユーザーの仮想 MFA デバイスを有効にする (コンソール)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSって何?

生徒 ジョン:relaxed:
講師 先輩:frowning2:
  

ここで登場するのは、

プログラミング歴なしのジョン(主人公)。

AWS講師の先輩。

ジョンが一人前のAWSエンジニアになる物語です。

インフラって何??それ美味しいの???

って方に向けて、極力わかりやすくAWSを紹介していきます。
____________________________________________

ジョン
「インフラって言葉聞いたことあるんだけど、ネットにおけるインフラってなんっすか?」

先輩
「そんなの自分で調べろ。ググったら出てくる」

ジョン
「え?わからないんですか?www」

先輩
怒。。
「生活や仕事などを営む上で不可欠なものあるだろ(公共の福祉のため整備・提供される施設)。水道とか道路とか」
「ネットでもこの土台部分が整備されてないとコンテンツすら見れないって訳だ」
「だからネットにおけるインフラとは、主にサーバーやストレージやネットワークのことを指すんだ」

ジョン
「おお。わかりやすい。」
「じゃあ。クラウドってなんですか?」

先輩
「雲だよ!!!!!!!」

ジョン
「馬鹿にしないでくださいw」

先輩
「馬鹿にしてるようだけど、間違ってもいないんだな」

ジョン
・・・・・?????

先輩
「そもそもクラウドって定義が曖昧なんだ」
「インフラにおけるクラウドとは。。。。。」
「簡単に説明すると、ジョンがサーバーを持っていなかったとしても、ネット上でサーバーを持つことができるんだ」
「サーバーに限らず、ストレージっていうデータの保管場所やネットワークも構築できる」
「ようは、秋葉や日本橋に行ってサーバーを購入してーセットアップしてーなんてことしたくねーだろ?」

ジョン
「そんなことしてる時間があれば、BBQでもしたいっすねw」

先輩
「遊ぶことしか頭にねーのか。w」
「時間だけでなく、サーバーを借りることで費用も抑えられる。」
「1台サーバーを用意して何万って払うのと、借りて1ヶ月課金の何百円ならどっち選ぶ?」

ジョン
「後者ですねーー」

先輩
「もちろんここだけ伝えれば、クラウドのメリットばかりに感じるよな」
「でも自前でインフラを準備する。これをオンプレ(オンプレミスの略)っていうんだが、オンプレにした方が有利な場合もある。」
「ケース×ケースだが、例えば自社で独自のセキュリティーを高めたいとかの場面に有効なんだ」
「しかし、そうなったらセットアップやサーバーを構築・維持する技術者も必要だよな」

ジョン
「確かに。」

先輩
「固定費を支払っても十分メリットがある場合はオンプレにする場合もあるんだ」
「でも時代はクラウド化してるらしいけどな」

ジョン
「どこでそのインフラを借りることができるのですか?」

先輩
「いろいろあるけど、今後インフラに興味があるならAWSがいいんじゃなかな?」

ジョン
「【AWS】って????」

先輩
「みなさんお馴染みのAmazon.com社が提供するクラウドコンピューティングサービスのことだ」
「Amazon Web Serviceの頭文字をとったものだな」
「Amazonって、膨大な情報量(商品や顧客などなど)を処理しなければならないだろ?その技術を一般の人にも提供してる訳」

ジョン
「AWSがなぜおすすめかというと」

・低価格、初期費用なし、従量課金制
 →使用した分しか費用が発生しないし
・継続的な値下げ
 →過去10年で70回の値下げ
・サイジングからの解放
 →サーバーの台数を瞬時に増減できる。
・ビジネス機会を逃さない
 →オンプレだとサーバーを運用するまでに、購入、セットアップなどで半年や数年かかることもあるが、AWSなら瞬時にサーバー構築ができる。
・最先端の技術をいつでも使用可能
 →AWS では 165 を超えるサービスを提供(投稿日現在)
・いつでも即時にグローバル展開
 →AWSでは世界中にデータセンター(リージョン)があり、アベイラビリティゾーン(複数のデータセンター郡)が存在しているため。グローバルに展開可能。
・開発速度の向上と属人性の排除
・マネージドサービスによる、運用負荷の軽減
・高いセキュリティーを確保
・24時間365日日本語によるサポート

詳しくは
https://aws.amazon.com/jp/aws-ten-reasons/

ジョン
「訳わからないですけど、とりあえずメリットは沢山あるってことですね」
「ちょっとAWSに興味を持ってきました。今から勉強します!!!」

先輩
「質問も従量課金だからな」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSを使用する利点

生徒 ジョン:relaxed:
講師 先輩:frowning2:
  

ここで登場するのは、

プログラミング歴なしのジョン(主人公)。

AWS講師の三郎。

ジョンが一人前のAWSエンジニアになるための物語です。

インフラって何??それ美味しいの???

って方に向けてAWSを使用するメリットを紹介します。
____________________________________________

ジョン
「インフラって言葉聞いたことあるんだけど、ネットにおけるインフラってなんっすか?」

先輩
「そんなの自分で調べろ。ググったら出てくる」

ジョン
「え?わからないんですか?www」

先輩
怒。。
「生活や仕事などを営む上で不可欠なものあるだろ(公共の福祉のため整備・提供される施設)。水道とか道路とか」
「ネットでもこの土台部分が整備されてないとコンテンツすら見れないって訳だ」
「ってことはサーバーやストレージやネットワークのことをインフラって呼ぶ訳」

ジョン
「おお。わかりやすい。」
「じゃあ。クラウドってなんですか?」

先輩
「雲だよ!!!!!!!」

ジョン
「馬鹿にしないでくださいw」

先輩
「馬鹿にしてるようだけど、間違ってもいないんだな」

ジョン
・・・・・?????

先輩
「そもそもクラウドって定義が曖昧なんだ」
「インフラにおけるクラウドとは。。。。。」
「簡単に説明すると、ジョンがサーバーを持っていなかったとしても、ネット上でサーバーを持つことができるんだ」
「サーバーに限らず、ストレージっていうデータの保管場所やネットワークも構築できる」
「ようは、秋葉や日本橋に行ってサーバーを購入してーセットアップしてーなんてことしたくねーだろ?」

ジョン
「そんなことしてる時間があれば、BBQでもしたいっすねw」

先輩
「遊ぶことしか頭にねーのか。w」
「時間だけでなく、サーバーを借りることで費用も抑えられる。」
「1台サーバーを用意して何万って払うのと、借りて1ヶ月課金の何百円ならどっち選ぶ?」

ジョン
「後者ですねーー」

先輩
「もちろんここだけ伝えれば、クラウドのメリットばかりに感じるよな」
「でも自前でインフラを準備する。これをオンプレ(オンプレミスの略)っていうんだが、オンプレにした方が有利な場合もある。」
「ケース×ケースだが、例えば自社で独自のセキュリティーを高めたいとかの場面に有効なんだ」
「しかし、そうなったらセットアップやサーバーを構築・維持する技術者も必要だよな」

ジョン
「確かに。」

先輩
「固定費を支払っても十分メリットがある場合はオンプレにする場合もあるんだ」
「でも時代はクラウド化してるらしいけどな」

ジョン
「どこでそのインフラを借りることができるのですか?」

先輩
「いろいろあるけど、今後インフラに興味があるならAWSがいいんじゃなかな?」

ジョン
「【AWS】って????」

先輩
「みなさんお馴染みのAmazon.com社が提供するクラウドコンピューティングサービスのことだ」
「Amazon Web Serviceの頭文字をとったものだな」
「Amazonって、膨大な情報量(商品や顧客などなど)を処理しなければならないだろ?その技術を一般の人にも提供してる訳」

ジョン
「AWSがなぜおすすめかというと」

・低価格、初期費用なし、従量課金制
 →使用した分しか費用が発生しないし
・継続的な値下げ
 →過去10年で70回の値下げ
・サイジングからの解放
 →サーバーの台数を瞬時に増減できる。
・ビジネス機会を逃さない
 →オンプレだとサーバーを運用するまでに、購入、セットアップなどで半年や数年かかることもあるが、AWSなら瞬時にサーバー構築ができる。
・最先端の技術をいつでも使用可能
 →AWS では 165 を超えるサービスを提供(投稿日現在)
・いつでも即時にグローバル展開
 →AWSでは世界中にデータセンター(リージョン)があり、アベイラビリティゾーン(複数のデータセンター郡)が存在しているため。グローバルに展開可能。
・開発速度の向上と属人性の排除
・マネージドサービスによる、運用負荷の軽減
・高いセキュリティーを確保
・24時間365日日本語によるサポート

詳しくは
https://aws.amazon.com/jp/aws-ten-reasons/

ジョン
「訳わからないですけど、とりあえずメリットは沢山あるってことですね」
「ちょっとAWSに興味を持ってきました。今から勉強します!!!」

先輩
「質問も従量課金だからな」

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

S3バケットをOrganizations内全アカウントにアクセス許可

S3バケットをAWSアカウントを超えて共有したい、Organizatins内の全AWSアカウントに共有できるとなおよいとおもったので、方法を確認してみました。

S3バケットのクロスアカウントアクセス

まず別のAWSアカウントの特定のIAMユーザーにアクセスを許すところから。

ここでは簡単な「別の AWS アカウントのユーザーに、オブジェクトを S3 バケットにアップロードするアクセス権を与える」を試します。このドキュメントは下図のように、S3アクセスを実現するためにIAMポリシーとバケットポリシーを設定する、という内容になっています。

image.png

アカウントAでは、バケット名 bucketname への書込み操作をIAMユーザーに許可するために、IAMポリシーを作成してアタッチします。IAMポリシーの内容は以下のようになります。実施時は bucketname は実際のバケット名に合わせて置き換えます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::bucketname",
                "arn:aws:s3:::bucketname/*"
            ]
        }
    ]
}

これでIAMユーザー UploadDatabucketname に書込みに行けるようになりました。でも実際に書込みに行ってみると書き込む権限がないと断られる状況です。

そこで次に、アカウントBでバケット名 bucketname への書込みをIAMユーザー UploadData に許すために、バケットポリシーを作成してアタッチします。バケットポリシーの内容は以下のようになります。実施時は bucketname は実際のバケット名、999999999999 は実際のAWSアカウントAのアカウントID、 UploadData は実際のIAMユーザー名で置き換えます。ドキュメント通りの手順でやるなら arn:aws:iam::999999999999:user/UploadData がまるごとIAMユーザーのARNで置き換えられます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DelegateS3Access",
            "Effect": "Allow",
            "Principal": {"AWS": "arn:aws:iam::999999999999:user/UploadData"},
            "Action": ["s3:PutObject", "s3:ListBucket"],
            "Resource": [
                "arn:aws:s3:::bucketname",
                "arn:aws:s3:::bucketname/*"
            ]
        }
    ]
}

これで UploadData さんは無事にS3バケット bucketname にオブジェクトを保存(put)できるようになったはず、ということでこのドキュメントは終了。あとは、他アカウントのS3バケットへのアップロードはWebのAWSマネジメントコンソールからは試せないので、AWS CLIで確認します。

aws s3 cp ローカルファイル s3://bucketname/ 

本気で考えるなら「S3 バケット内のオブジェクトへのクロスアカウントアクセスを許可する」で示されている以下の3つのアプローチから、どれがいいか選ぶところから検討することになるのでしょう。上の内容は、このうち最初のものに相当するはず。

  • リソースベースのポリシーと IAM ポリシー
  • リソースベースのアクセスコントロールリスト (ACL) と IAM ポリシー
  • クロスアカウント IAM ロール

他AWSアカウントの全IAMユーザーにアクセス許可

次に別のAWSアカウントのすべてのIAMユーザーにアクセスを許したいと思います。バケットポリシーで読み書きを許可するユーザー名にワイルドカード * を指定したいところだけど、これはできません。

Principal エレメント内でユーザーを指定する際に、"すべてのユーザー" の意味でワイルドカード (*) を使用することはできません。プリンシパルには、常に特定のユーザー(または複数の特定ユーザー)を指定する必要があります。
(AWS JSON ポリシーの要素: Principal - AWS Identity and Access Management)

そこで、AWSアカウントそのものを読み書きの許可対象に指定します。アカウントIDを指定する場合は、以下のどちらの書き方をしてもいいことになっています。

"Principal": { "AWS": "arn:aws:iam::123456789012:root" }
"Principal": { "AWS": "123456789012" }

バケットポリシーを以下のように書き換えます。ここでは上の1行目の書き方をしましたが、より簡素な2行目の書き方でもOKです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DelegateS3Access",
            "Effect": "Allow",
            "Principal": {"AWS": "arn:aws:iam::999999999999:root"},
            "Action": ["s3:PutObject", "s3:ListBucket"],
            "Resource": [
                "arn:aws:s3:::bucketname",
                "arn:aws:s3:::bucketname/*"
            ]
        }
    ]
}

少し待ってからローカルファイルのアップロードを試します。

aws s3 cp ローカルファイル s3://bucketname/ 

Organizations内の全AWSアカウントにアクセス許可

いよいよOrganizations内の全AWSアカウントへのアクセス許可を考えます。Organizations内のAWSアカウントには arn:${Partition}:organizations::${MasterAccountId}:account/o-${OrganizationId}/${AccountId} のようなOrganizations IDを含んだARNが付与されるので、これとワイルドカードを組合わせればできそうに思えますが、実際にはこのARNはPrincipalで利用できないようです。

以下のリソースタイプは、このサービスによって定義され、IAM アクセス許可ポリシーステートメントの Resource 要素で使用できます。
AWS Organizations のアクション、リソース、および条件キー - AWS Identity and Access Management

ここはPrincipalは完全にワイルドカード * にしてしまいます。その代わり、Organizations外からのアクセスができないように、Conditions(条件)を設定します。aws:PrincipalOrgIDが利用できます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DelegateS3Access",
            "Effect": "Allow",
            "Principal": "*",
            "Action": ["s3:PutObject", "s3:ListBucket"],
            "Resource": [
                "arn:aws:s3:::bucketname",
                "arn:aws:s3:::bucketname/*"
            ]
            "Condition": {"StringEquals":
                {"aws:PrincipalOrgID":["o-xxxxxxxxxxx"]}
            }
        }
    ]
}

少し待ってからローカルファイルのアップロードを試してみます。

aws s3 cp ローカルファイル s3://bucketname/ 

これでOrganizations内のどのAWSアカウントのIAMユーザーでも、このバケットへの読み書きが可能になったはずです。できればOrganizations外のAWSアカウントのIAMユーザーでは、バケットへの読み書きができないことも確認しておいた方がよいでしょう。

参考

本稿は以下のドキュメントを参考にまとめました。

触れていませんが、IAMユーザーとS3バケットにそれぞれポリシーを設定するというのは「アイデンティティベースのポリシーおよびリソースベースのポリシー」に該当します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VPC エンドポイントへのアクセス許可は SourceIp ではなく VpcSourceIp を利用する

概要

VPC エンドポイントを設定した AWS サービスへの IP アドレスによるアクセス許可は SourceIp ではなく、VpcSourceIp を利用する必要があります。

詳細

前提条件

S3 の特定のバケットに対して VPC エンドポイントを利用していると仮定します。

その S3 に対しては次のようにパブリックなアクセス許可を与えています。

{
    "Version": "2012-10-17",
    "Id": "Policy156583539788",
    "Statement": [
        {
            "Sid": "Stmt156583536527",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::bucket/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "0.0.0.0/0"
                }
            }
        }
    ]
}

この設定ではすべての IP アドレスからの GetObject を許可しています。

VpcSourceIp の指定

この状態で VPC 内のインスタンスなどからこのバケットにアクセスしようとしても Access Denied になります。

VPC エンドポイントへのアクセス許可(およびアクセス制限)に SourceIp は使えないからです。

VPC エンドポイントを介した Amazon S3 へのリクエストに、IAM ポリシーの aws:SourceIp 条件を使用することはできません。これはユーザーとロールの IAM ポリシー、およびバケットポリシーに適用されます。ステートメントに aws:SourceIp 条件が含まれる場合、値は指定した IP アドレスまたは IP アドレス範囲に一致しません。

ref: https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-endpoints-s3.html 

その代わり、VpcSourceIp を利用します。

リクエスト実行元が Amazon VPC エンドポイントを使用するホストである場合、aws:SourceIp キーは使用できません。代わりに、aws:VpcSourceIpなどの VPC 固有のキーを使用する必要があります。

ref: https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_condition-keys.html

{
    "Version": "2012-10-17",
    "Id": "Policy1565835397817",
    "Statement": [
        {
            "Sid": "Stmt1565835396529",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::bucket/*",
            "Condition": {
                "IpAddress": {
                    "aws:VpcSourceIp": "0.0.0.0/0"
                }
            }
        }
    ]
}

これで VPN エンドポイントを経由した VPC 内からのアクセスを許可することができます。

SourceIp と VpcSourceIp の併用

VPC 内に加えて、外部からもアクセスを許可したい場合は次のように SourceIp と VpcSourceIp のアクセス許可を二つ列挙します。

{
    "Version": "2012-10-17",
    "Id": "Policy1565835397817",
    "Statement": [
       {
            "Sid": "Stmt156583539651",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::bucket/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "0.0.0.0/0"
                }
            }
        },
        {
            "Sid": "Stmt1565835396529",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::bucket/*",
            "Condition": {
                "IpAddress": {
                    "aws:VpcSourceIp": "0.0.0.0/0"
                }
            }
        }
    ]
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CloudFrontとLambdaでBasic認証設定してみた

こんにちは
「あつまれどうぶつの森」のためにswitchの販売抽選に応募するが当たらず「あつもり」ブームも過ぎていたことに最近気付いたstreampackのrisakoです:girl_tone1::koala:
そして9月も後半!だんだん涼しくなってきて過ごしやすくなってきましたね!

今回のテーマは「Basic認証」です:dizzy:
これまでなかなか触れることがなかったので、初めて設定してみた記念にQiitaに残そうと思います。

Basic認証とは?

Web上で利用できる認証システムのことです。
Basic認証が設定されているページにアクセスすると、ブラウザ上にユーザー名とパスワードを入力するポップアップが表示されます。
私はBasic認証を意識する前に数回web上でみたことがあるので、多くの人が見たことがあるのではないでしょうか?

Safariだとこのようなポップアップが出てきます!
スクリーンショット 2020-09-16 13.17.43.png

事前に用意するもの

  • S3 bucketに保存したhtmlなど

Basic認証で保護したいwebページなどをS3bucketに保存しておきます。

まず初めにCloudFront設定

デフォルトの設定から変更が必要な箇所のみ記載します。

Origin Settings

  • Origin Domain Name:webページで保存したS3bucketを指定します

Default Cache Behavior Settings

  • Viewer Protocol Policy :Redirect HTTP to HTTPS
  • Cache and origin request settings:Use legacy cache settings
  • Cache Based on Selected Request Headers:Whitelist
  • Whitelist Headers:Access-Control-Request-Headers・Access-Control-Request-Method・Origin
  • Object Caching:全てのTTLを0にする

Distribution Settings

  • Default Root Object:S3に保存しているwebページをしていると、CloudFrontのURLから直で行けるので便利です。例えば、https://CF-URL/test.html と指定しなくてもhttps://CF-URL だけで、test.htmlが表示されます。

次にLambda設定

:star: Lambda@edgeを使用できるリージョンは米国東部(バージニア北部)のみなのでリージョンを移動します。

  • 任意の関数名を入力します。
  • ランタイム:Node.js 12.x
  • 実行ロール:Basic認証用に作成したものとわかるように命名します。仮に(test-basic-role)とします。
  • ポリシーテンプレート:「基本的なLambda@Edgeのアクセス権限(CloudFrontトリガーの場合」を選択 スクリーンショット 2020-09-24 15.35.59.png

Lambda関数が作成できたら、コードを書いていきます :pen_ballpoint:
参考記事のコードを使わせてもらっています。
下記の箇所を自分の設定したいユーザー名とパスワードに変更してください。
ここで設定したユーザー名とパスワードがbasic認証で使用するものになります。

// Configure authentication
const authUser = 'user';
const authPass = 'pass';

'use strict';
exports.handler = (event, context, callback) => {

    // Get request and request headers
    const request = event.Records[0].cf.request;
    const headers = request.headers;

    // Configure authentication
    const authUser = 'user';
    const authPass = 'pass';

    // Construct the Basic Auth string
    const authString = 'Basic ' + new Buffer(authUser + ':' + authPass).toString('base64');

    // Require Basic authentication
    if (typeof headers.authorization == 'undefined' || headers.authorization[0].value != authString) {
        const body = 'Unauthorized';
        const response = {
            status: '401',
            statusDescription: 'Unauthorized',
            body: body,
            headers: {
                'www-authenticate': [{key: 'WWW-Authenticate', value:'Basic'}]
            },
        };
        callback(null, response);
    }

    // Continue request processing if authentication passed
    callback(null, request);
};

コードを保存したら、右上に表示される「アクション」から「Lambda@Edgeへのデプロイ」に進みます。
スクリーンショット 2020-09-24 15.49.12.png

CloudFront トリガーの設定

  • 先ほど作成したCloudFrontを指定します。
  • CloudFrontイベント:ビューアーリクエスト
  • Lambda@Edge へのデプロイを確認::heavy_check_mark:を入れます
  • 「デプロイ」を押下

スクリーンショット 2020-09-24 15.54.09.png

Cloudfrontとlambdaが連携できているか確認する

作成したCloudFrontのBehaviorから確認します。

Lambda Function Associations

  • CloudFront Event:Viewer Request
  • Lambda Function ARN:Lambda@EdgeのARN

スクリーンショット 2020-09-24 16.04.39.png

動作確認

CloudFrontのドメインでアクセスしてみましょう。

https://xxxxxxx.cloudfront.net
スクリーンショット 2020-09-24 16.15.36.png

Basic認証がちゃんと設定されているようです。
設定したユーザー名とパスワードでログインできればOKです!:ok_hand:

参考

簡単だった!CloudFront + S3 に BASIC認証を入れる方法
できた!S3 オリジンへの直接アクセス制限と、インデックスドキュメント機能を共存させる方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2上のwebサーバをSSL化対応

今までの準備

今まではSSL化対応していなかった?

以下の画像を見ていただければわかると思いますが、
鍵のマークに赤い斜線がひかれています。
これは安全ではない通信、いわゆるSSL化対応が出来ていないということになります。

無題 - ペイント 2020-09-17 21.30.00.png

SSL化対応しているwebページだと以下のように
鍵のマークが表示され、安全な通信だということが記載されます。
今回は以下のようにSSL化対応していきます。

無題 - ペイント 2020-09-17 21.31.54.png

やること

  • ACMでSSL証明書を作成
  • ターゲットグループの作成
  • ターゲットグループにターゲットを紐付ける
  • ロードバランサーの作成
  • Route53で設定を変更する
  • セキュリティグループの変更

ACMでSSL証明書を作成

  • ACMを探す

ACMとはSSL/TML証明書のプロビジョニング、管理及びデプロイ と書いてあるように、SSL証明書を発行してくれるサービスです。

AWS マネジメントコンソール - Google Chrome 2020-09-17 21.55.1.png

  • 証明書のプロビジョニング

今回は、パブリックな証明書のプロビジョニングを行います。

AWS Certificate Manager - Google Chrome 2020-09-17.png

  • 証明書のリクエスト

プライベートではなく、パブリック証明書のリクエストになります。
チェックを入れて証明書のリクエストを行います。

1AWS Certificate Manager - Google Chrome 2020-09-17.png

  • 検証方法の選択

ざっくりいうと
証明書を発行する際に今申請している人(組織)と発行元のドメインを管理している人(組織)が同一か、というのを見ています。
そのための方法として、以下2つの方法があります。
1. DNS認証
2. Eメール認証

今回DNS認証にした理由とは、
Eメール認証だと証明書リクエストを行うドメインへメールが送られます。
メール見るだけやろ?って思う方いるかと思いますが、
今このドメイン(domainname.com)にはメールを受け取る設定もサーバも立てていません。
また、受け取るユーザも決まっていてadmin,postmasterなど決まったユーザしか受け取ることが出来ないのです。
メールサーバ立てるのも少し面倒なので、一旦DNS認証にしました。

DNS認証にすると、この値(CNAMEレコード)をDNSに書き込んでねというのが指定されるのでそれをDNSに書き込みます(今回はRoute53)
またRoute53で管理していると、ワンクリックで自動的にレコード入れてくれるのでとても楽です。

ACMのSSL証明書は1年毎の更新ですが、
DNS認証は自動更新になります(例外あり!)
ただし、これから追加するDNSのレコードを消してしまうと更新できないので注意。
メール認証は、毎年メールが送られてきてURLをクリックすると更新されます。
メールなので作業漏れ結構有りなので注意

2AWS Certificate Manager - Google Chrome 2020-09-17.png

  • タグは今回設定しません

3AWS Certificate Manager - Google Chrome 2020-09-17.png

  • ドメイン名の追加

ここでは、どんなドメインのSSL証明書を発行するか をドメイン名の箇所に記載します。
www.domainname.comをここでは入れます。

無題 - ペイント 2020-09-17 21.58.33.png

  • 最終確認

このドメインでこの認証方法でリクエストするよ、と確認されます。
間違っていたら戻って再度修正してください

無題 - ペイント 2020-09-17 22.01.49.png

  • 検証

証明書のリクエストが完了し、検証に入りました。
DNS認証にしたので、ドメインの箇所にDNSに入れるべきレコードが記載されています。
Route53でのレコード作成というボタンがありますが、これが↑で言ってたワンクリックでRoute53にレコードを書き込んでくれるボタンです。

無題 - ペイント 2020-09-17 22.03.52.png

  • Route53でのレコードの作成

このレコードをホストゾーンdomainname.comに入れますか?と記載があるので、作成を行います。

無題 - ペイント 2020-09-17 22.05.09.png

  • 検証が成功に変わる

レコードがRoute53に追加されたら一覧に戻ります。
少し待っていると検証状態が検証保留中から成功に変わります。

無題 - ペイント 2020-09-17 22.08.44.png

  • 証明書一覧に戻る

証明書一覧に戻ると、作成した証明書が追加されていました。
状況が発行済みになっています。
ここでは証明書の期限など詳細を知ることが出来ます

ちなみに↑でも書きましたが、
DNS認証だと1年後に自動更新がされます。(※DNSのレコードを消しちゃダメだよ!)
メール認証だと毎年メールが来て、作業が漏れることがあるのでDNS認証のほうが楽なのでは?と思います。

無題 - ペイント 2020-09-17 22.11.35.png

ターゲットグループの作成

ターゲットグループとは、後で作るロードバランサーに関係があります。
ロードバランサーとは、ロードバランサーに来たアクセスを紐付いてるサーバに割り振る役割を行います。
その、紐付いてるサーバのグループをターゲットグループと呼びます。
ロードバランサー1つに紐づくターゲットグループは1つになります。

  • EC2に進む

ターゲットグループはEC2で作成行います。

インスタンス _ EC2 Management Console - Google Chrome 201.png

  • ターゲットグループの作成

EC2から左の下の方にロードバランシングというのがあります。
その中にターゲットグループがあり、クリックすると一覧が表示されます(今はなにもないので見えないですが...)
ターゲットグループの作成を行います。

ターゲットグループ _ EC2 Management Console - Google Chrome.png

  • ターゲットグループの詳細設定

ここではターゲットグループの設定を行います。

ターゲットグループ名 : targetgrouptest
ターゲットの種類     : インスタンス
プロトコル           : HTTP
ポート               : 80
VPC                  : EC2インスタンスが存在するVPC
ヘルスチェックのプロトコル : HTTP
ヘルスチェックのパス       : /

無題 - ペイント 2020-09-17 22.45.22.png

  • 一覧に戻る

一覧に戻るとターゲットグループが表示されます。

1ターゲットグループ _ EC2 Management Console - Google Chrom.png

ターゲットグループにターゲットを紐付ける

先程作成したターゲットグループにwebサーバのEC2インスタンスを紐付けます

  • 編集

ターゲットグループにチェックを入れ、編集に進みます

ターゲットグループ _ EC2 Management Console - Google Chrome1.png

  • webサーバのインスタンスを追加
  1. [インスタンス]の項目で追加するwebサーバにチェックを入れ、
  2. 「登録済みに追加」をすると
  3. 「登録済みターゲット」にwebサーバのインスタンスが追加されました。

保存を行います。

無題 - ペイント 2020-09-17 23.06.23.png

  • 一覧に戻る

保存すると、webサーバが登録済みターゲットに追加されています。
ステータスがunusedなのは、LBにターゲットグループを登録していないのでこの表示になっています。

無題 - ペイント 2020-09-17 23.18.57.png

ロードバランサーの作成

いよいよロードバランサー(LB)の追加です。

  • ロードバランサーの作成

EC2の左下にロードバランシングがあり、その中にロードバランサーがある
一覧の箇所にロードバランサーの作成があるので、作成を行う

ロードバランサー _ EC2 Management Console - Google Chrome .png

  • ロードバランサーの種類の選択

実はロードバランサーには種類があって以下があります。

・ ALB : アプリケーションレイヤー
・ NLB : ネットワークレイヤー
・ CLB : アプリケーションもネットワークもOK。だが古い(クラシック)なので上記2つを推奨

今回はHTTPSのアクセスなので、ALBにします。

ロードバランサーの作成 _ EC2 Management Console - Google Chro.png

  • ロードバランサーの設定
名前 : loadbalancertest
スキーム : インターネット向け

▼リスナー
HTTP, HTTPS 2つ追加します

▼アベイラビリティーゾーン
VPC : webサーバのあるVPCを選択
アベイラビリティーゾーン : 1つはwebサーバのあるアベイラビリティーゾーン
                        もう1つはどこでもいい(ここの項目は2つ選択しないといけない為。)

無題 - ペイント 2020-09-17 23.33.13.png

  • セキュリティ設定の構成
証明書タイプ : ACMから証明書を選択する
証明書の名前 : 先程作成した証明書(www.domainname.com)を選択する

無題 - ペイント 2020-09-17 23.38.14.png

  • セキュリティの設定

新しいセキュリティグループ作成
セキュリティグループ名はなんでもよい(今回は「loadbarancertestsecuritygroup」)
許可するポートは、80と443でどこからでもアクセス出来るように、0.0.0.0/0, ::/0 を許可する

100ロードバランサーの作成 _ EC2 Management Console - Google Chro.png

  • ルーティングの設定

↑で作ったターゲットグループを設定
既存のターゲットグループ と ターゲットグループ名さえ入れれば自動的に入る

1ロードバランサーの作成 _ EC2 Management Console - Google Chro.png

  • ターゲットの登録

ターゲットグループで指定したEC2インスタンスがここに表示される

無題 - ペイント 2020-09-17 23.43.58.png

  • 確認

今まで設定したものが表示される。
意図していないものが表示された場合には、戻って修正を行う。

無題 - ペイント 2020-09-17 23.45.17.png

  • ロードバランサーが作成される

2ロードバランサーの作成 _ EC2 Management Console - Google Chro.png

  • ターゲットの確認

ターゲットグループの一覧からターゲットを確認すると、
LB作成前はunusedになっていたが、
LB作成後はhealthyになり、アクセス出来る状態になった。

無題 - ペイント 2020-09-18 00.14.31.png

Route53で設定を変更する

  • Route53のdomainname.comのホストゾーンに移動

無題 - ペイント 2020-09-19 15.13.18.png

  • レコードの編集

www.domainname.comのAレコードにチェックを入れ、編集を行う

無題 - ペイント 2020-09-19 15.15.30.png

  • 値の変更

ALBとCLBへのエイリアス にし、その下の段はLBがあるリージョンを指定。
一番下はLBを指定します。

無題 - ペイント 2020-09-19 15.19.35.png

  • 保存された状態

保存されると、www.domainname.com のAレコードはLBに変更されました。

無題 - ペイント 2020-09-19 15.22.21.png

セキュリティグループの変更

↑で、実質www.domainname.comにアクセスを行うと、
LBを通り、EC2インスタンスにアクセスが来るように経路を変更することが出来ました。
しかし、ここで問題となっているのが、
EC2インスタンスのセキュリティグループです。
なぜかというと、EC2インスタンスのセキュリティグループは以下のように
自分のPCからしかアクセスできないようになっています。
要するに、ALBからのアクセスも遮断してしまいます。

無題 - ペイント 2020-09-19 15.35.56.png

ですので、ALBからのアクセスのみ許可する変更をします。

  • EC2のセキュリティグループを変更

EC2 → セキュリティグループ へ進み、
EC2のセキュリティグループにチェックを入れ、インバウンドを編集します。

このときに、LBのセキュリティグループのグループIDをメモしておいてください
グループIDはsg-xxxxxxと書かれている、↓で言ったら青の枠の箇所になります。

無題 - ペイント 2020-09-19 15.41.00.png

  • 編集

HTTPとHTTPSに書かれていた、自分のIPアドレスを
↑でメモしたグループID(sg-xxxxxx)に書き換えて保存します。

無題 - ペイント 2020-09-19 15.44.20.png

  • 一覧に戻る

一覧に戻るとHTTPとHTTPSのソースがセキュリティグループ(sg-xxxxxx)に変更されていました。

無題 - ペイント 2020-09-19 15.46.07.png

webで確認

  • webで確認

webの検索でhttps://domainname.com/index.htmlと検索
すると、以下のように鍵がかかった状態でhello worldが表示されましいた。

無題 - ペイント 2020-09-19 15.53.31.png

  • 証明書情報を見る

証明書情報を見ると、Amazonが認証局になり有効期限が来年になっていることが確認できました。

無題 - ペイント 2020-09-19 15.56.08.png

これで、EC2上のwebサーバをSSL化することが出来ました。
LBを利用しているので、アクセス数によって費用がかかります。
費用によっては、きちんと管理いただければと思います。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CodeDeployのIn-Place Deploymentでハマったときの対応

はじめに

AWSを使っているとCodeDeployを利用してデプロイすることがあると思います。
CodeDeployのIn-Place Deploymentでデプロイしたときにハマった点がありましたので、ハマった内容と対処方法を紹介します。

ハマった内容

In-Place Deploymentは、稼働中サーバーに対して直接新しいアプリケーションをデプロイする方法です。
デプロイ先のサーバーのストレージ容量によりますが、何度かデプロイを行っているとストレージ容量を使い切ってしまい、デプロイを行っても失敗を繰り返しました。

原因

CodeDeployエージェントは、/opt/codedeploy-agent/deployment-root/配下にリビジョン、デプロイメント履歴、デプロイメントスクリプトを保存していました。そのため、デプロイを何度も行うと過去のデプロイメント履歴が残りストレージ容量を使い切っていました。
/opt/codedeploy-agent/deployment-root/配下のディレクトリ構成については、Working with the CodeDeploy agent
のFiles installed by the CodeDeploy agentを参考にしてみてください。

対処方法

ストレージ容量を使い切ったときに手動で過去のデプロイリソースを削除するでも良いですが、ストレージ容量の確保を自動化しました。
DeploymentGroupID_last_successfull_installファイルに、最後(最新)に正常にデプロイされたアーカイブディレクトリのパスが記述されているので、以下のようにスクリプトを作成し、appspec.ymlでスクリプトを実行し、最後(最新)に正常にデプロイされた場所のアーカイブディレクトリ以外のディレクトリを削除することで、ストレージ容量を確保することに成功しました!

#!/bin/bash

###デプロイグループIDを取得
DEPLOYMENT_GROUP_ID=`cat /opt/codedeploy-agent/deployment-root/deployment-instructions/*_last_successful_install | cut -d"/" -f 5`

###デプロイIDを取得
LAST_SUCCESSFUL_DEPLOY_ID=`cat /opt/codedeploy-agent/deployment-root/deployment-instructions/*_last_successful_install | cut -d"/" -f 6`

DIR_PATH="/opt/codedeploy-agent/deployment-root/$DEPLOYMENT_GROUP_ID/"

###最後(最新)に正常にデプロイされた場所のアーカイブディレクトリ以外のディレクトリを削除する
cd $DIR_PATH && ls | grep -v $LAST_SUCCESSFUL_DEPLOY_ID | xargs rm -rf

注意点

次のデプロイを行うときに、最後(最新)に正常にデプロイされたアーカイブディレクトリが必要になるので、アーカイブディレクトリを削除してしまうとデプロイを失敗してしまうので、お気を付け下さい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ACMのPrivate CAを利用した場合のAmazon API GatewayのmTLS構成を試してみた。

はじめに

先週、こちらのふたつの記事でAmazon API GatewayのmTLS認証について検証・検討しました。

前回、記事が長くなってに分割しましたが、その中でも確認したいことが書けなかったので、その後続の記事として ACM(AWS Certificate Manager)のPrivate CA( Certificate Authority:認証局) を利用したAmazon API GatewayのmTLS構成について書きたいと思います。なお、興味本位でAWS ACM Private CAを今回は利用していますが、opensslコマンドを利用して以下を実施いただいても同じ内容かと思います。

  • 仮想のルートCAの秘密鍵、自己証明書(ルートCA証明書)
  • 仮想の中間CAの秘密鍵、中間CA証明書(ルートCAによる署名)
  • クライアントの秘密鍵、クライアント証明書(中間CAによる署名)

サマリ

  • 中間認証局がクライアント証明書発行している場合は、ルート認証局までのチェーンをトラストストアに登録する必要がある(各CAの公開鍵証明書)
  • 複数の認証局をサポートしたい場合はその認証局とそのチェーン(ルート認証局までに登場する中間認証局)をトラストストアに登録すればよい
  • トラストストア内は重複した認証局の証明書の登録はできないので、既に登録されている認証局の証明書は登録時にエラーとなる。

AWS Certificate ManagerのPrivate CAとは

ACM Private CA は、オンプレミスCA の運用にかかる投資コストや保守コストなしに、ルート CA や下位 CA を含むプライベート認証機関 (CA) 階層を作成できます。リリース当初はルートCAのサポートがありませんでしたが、可能になっています。Private CAを利用するとX.509 証明書を発行できます。
X.509 証明書を発行およびデプロイする AWS サービスは 二つあります。

-ACM Private CA
-AWS Certificate Manager

どちらを利用するかは、利用ニーズによります。

ACM Private CA

組織内でのプライベートな使用を目的として証明書を利用する場合にACM Private CA を使用すると、独自の認証局、および、その 階層を作成できます。独自の認証局をマネージドで構築することで、運用の負荷を軽減でき、かつ、社内・組織内で利用可能な独自の証明書の発行が可能とンります。この証明を、Public、つまり、インターネット等で利用してもルート認証局が公開され、かつ、認められた認証局ではないため信頼はされない証明書となります。

  • 任意のサブジェクト名で証明書を作成可能
  • 任意の有効期限で証明書を作成可能
  • サポートされている任意のプライベートキーアルゴリズムとキー長を使用可能
  • サポートされている任意の署名アルゴリズムを使用可能

AWS Certificate Manager

ACMで発行する証明書は、AWS Elastic Load Balancing、Amazon CloudFront、Amazon API Gateway、およびその他の 統合サービスにデプロイできます。ACM が提供するパブリック証明書 または ACM にインポートされた証明書を上記の統合サービスに適用でき、証明書に指定されたドメイン名でTLS通信を可能にします。

当記事で利用する証明書は3種類

当記事では以下の3州類の証明書を作成・利用します。

  • (Private)ACM Private CAで発行するルート認証局ならびに中間認証局の証明書。クライアント証明書の署名検証用
  • (Private)ACM Private CAでで発行した中間認証局の証明書で証明されたクライアント証明書。mTLS用
  • (Public)ACMで発行されたサーバ証明書。TSL通信用

A.一つ目の中間認証局の環境構築

環境構築は以下の順序で実施します。

  1. ルート認証局の作成(ACMPrivate CA)
  2. 中間認証局の作成(ACM Private CA)
  3. APIの作成
  4. カスタムドメインの有効化(ACM)
  5. mTLSの有効化
  6. クライアント証明書の作成(ACM Private CA)
  7. 接続

1. ACMのPrivate CAを利用したルート認証局の構築

ルート認証局の作成

ACMを利用することで僅か数分でルート認証局(中間認証局も)が構築可能です。今回は以下の設定でAWS マネジメントコンソールから構築しました。
※ルート認証局を当文書内ではルートCA/中間認証局を中間CAと表記することもあります。

image.png

ルート認証局に証明書のインストール

前回の記事ではopensslコマンドで

  • 秘密キーの作成
  • (CSR作成は省略して証明書作成時に入力)
  • 自己署名証明書の作成

を実施しましたが今回はAWS マネジメントコンソールから証明書の生成& ルート認証局へのインポートを実施しました。(前回の記事では、自己署名の証明書をつくっただけで、認証局を運用したわけではありません)
個別に秘密鍵の生成、CSRの作成は必要なく、CSRに入力する情報を画面から入力する形でした。有効期限は1か月にしておきました。

image.png

以上で、Private CAのルート認証局が立ち上がりました。Private CAで構築したルート認証局とAPI Gatewayの組み合わせで設定を確認するだけであれば、ここまでで十分です。ただ、今回は中間認証局(証明書のチェーン)も合わせて試したかったので先ほど構築したルート認証局をルートとした中間認証局も構築します

2. ACMのPrivate CAを利用した中間認証局の構築

中間認証局の作成

こちらは、ルート認証局を構築した手順とほぼ同様の手順ですが、CA TypeがSubordinate(下位)となっている点が違いです。添付の画像を確認してみてください。
image.png

続いて、中間認証局の証明書の作成とインポートです。こちらの証明書は先ほどのルート認証局用の証明書とは異なります。どこが異なるかというと、自己署名、つまり自分自身の秘密鍵を使って署名をするのではなく、ルート認証局の秘密鍵を使って証明書に署名する部分がルート認証局の署名時と異なる部分です。つまり、ルート認証局の証明書・秘密鍵が必要になるため、ルートを指定します。以下の画像でParent private CAでルート認証局のIDを選択していることを確認してください。

image.png

image.png

image.png

今までの手順で作成した認証局です。
image.png

なお、上位の認証局(今回の場合、ルート認証局)のIDが分からない場合は、以下の画像にある通り、対象の認証局をACMの一覧から表示してStatusをご確認ください。ARN(Amazon Resource Names)の中にIDがあります。

中間認証局の作成2

さて、本来であれば、ここまででクライアント証明書の作成に進むわけですが、ここで気になったのが、中間認証局を複数立ち上げた場合、つまり、クライアント証明書を複数の業者に依頼して発行した場合等はどういう構成になるのか?ということで、別のCAを構築することにしました。こちらのCAの階層としてはという階層構成で、先ほど作った認証局とは、ルート認証局のみが共通しているものとなります

ルート認証局->中間認証局->中間認証局
image.png

3.APIの作成 & 4. カスタムドメインの有効化(ACM)

APIの作成および、カスタムドメインの有効化設定は先日ご紹介したAmazon API GatewayでmTLSを試してみた。(1/2)の記事の通りです。今回は省略します。前回、無意識のうちにRoute 53へのレコード登録をしていたようですが、今回確認した結果、やはり、カスタムドメインの有効化(ACMでのPublicな証明書の作成&API Gatewayのカスタムドメイン登録)に加えRoute53のレコード登録が必要でした。

5.mTLSの有効化

さて、カスタムドメインの登録まで実施し、https://カスタムドメイン名/ で接続できたことを確認した後で、mTLSの有効化設定をAPI Gatewayのカスタムドメインの設定の中で実施します。

CA証明書の入手

 mTLSの登録に必要なものは、認証局の公開鍵証明書です。今回は、ACM Private CAで認証局の証明書を作成したため、ACM Private CAから証明書をダウンロードし登録します。

CA証明書はAWSマネジメントコンソールまたはCLIで抽出可能です。今回はコンソールからリンクをクリックしてbodyおよびchainの両方を取得しました。
image.png

CA証明書の登録

入手した中間認証局のCA証明書をS3バケットに格納し、API Gatewayのカスタムドメイン設定を更新しました。すると以下のエラーメッセージが設定後、数分で表示されました。
image.png

詳細なエラーは次の通りです。

image.png

原因は、中間認証局の証明書のみを登録したからです。

では、中間認証局のCA証明書と、そのチェーンを連結した証明書を登録したいと思います。参考になる方法としてはこちらの手順9をご確認下さい。
ルート認証局までのチェーンを登録することで、以下の画像の通り、正常にMutualTLSが有効化されました。
image.png

6. クライアント証明書の作成(ACM Private CA)

サーバサイドの準備が整ったところで、クライアント証明書と秘密鍵を入手したいと思います。
今回は、中間認証局をACM Private CAで構築したので、その中間認証局に証明書の作成を依頼し、そこから、秘密鍵を取り出したいと思います。

ACMによるPrivte Certificateの作成

Private Certificateなので、インターネットで正規の証明書として利用することはできないということを改めて述べつつ、解説します。

まずは、ACMの画面から証明書発行のリクエストを行います。そこでは、Private CAを構築していると、以下の画面にあるようにRequest a private certificateを選択することができます。Private certificateを選択します。

image.png

その後、認証局を選択します。今回は中間認証局を利用してクライアント証明書を作るので、以下の画像にある通り、Typeはsubordinateとなっておりルート認証局を選択していないことが分かります。
image.png

そして、適当にドメイン名を設定することで証明書の作成要求ができます。ACMで証明書発行する場合、ここはドメイン形式で入れる必要があります。任意のフォーマットは使えません。

image.png

そして、作成完了。

ACMからクライアント証明書と秘密鍵をExport

ACM上で証明書を作成したら、AWS CLIまたは、AWSマネジメントコンソールを利用してクライアント証明書、その秘密鍵、チェーンをExportすることが可能です。Exportする際には秘密鍵を暗号化するためのパスフレーズの入力が求められます。パスフレーズを入力しExportを実行すると以下のような形で証明書、チェーン、秘密鍵(暗号化されたもの)が表示されますので、保存してください。

image.png

7. 接続

接続には前回と同様にcurlコマンドを利用します。以下のような形で --passを指定してあげることで、暗号化された秘密鍵も利用可能です。パスワードは証明書Export時にしていしたものを指定してください。

curl -v --key  client1.key --cert client1.pem --pass password  https://api.xxx.xxx/hoge/auth

B.二つ目の中間認証局の環境構築

ここまでは、単体の中間認証局に対する検証でした。
ここからは複数の認証局をサポートするか試したいと思います。ケースとしては、クライアント証明書の発行を複数の認証局が担当するケースを想像してみました。

今回の認証局の構成イメージ

image.png

先ほどまでは、この図で言うところのintermediateCA 0 が発行したクライアント証明書を利用して接続しました。その際には、intermediateCA 0 とrootCAの証明書をtruststore(S3のオブジェクト)に格納しました。API GatewayのトラストストアはAmazon S3の1つのオブジェクトを指定(オプションでVersionも指定可能)する形です。今回はintermeidiateCA 0をすでに登録してあるので、IntermediateCA 2 が発行したクライアント証明書利用できるように構成したいと思います。

ちなみにintermediateCA 2 (3階層目)は上図にあるとおりrootCA->intermediateCA 1 ->IintermediateCA 2というチェーンになっています。

Private CAの構築は先ほど構築した手順の繰り返しとなりますので省略します。ここでは、API Gatewayへの登録方法についてご説明します。

二つ目の中間認証局によるクライアント証明書の発行と接続

この場合に必要な手順は次の通りです。

1. 未登録のCA証明書を入手しAPI Gatewayのトラストストアに登録

トラストストアに未登録のintermediateCA1とintermediateCA2の証明書を入手します。今回は、中間認証局intermediateCA2がクライアント証明書を発行するため、intermediateCA2の証明書に加えて、その上位の認証局であるintermediateCA1の証明書が必要となります。さらに上位のrootCAの証明書も本来は必要ですが既にトラストストア内に登録済みのため今回は不要です。入手方法は先ほど「mTLSの有効化手順で記載したCA証明書の入手」をご確認ください。

この時、私は最初、intermediateCA2のチェーン(ルート認証局までのすべてのチェーン)を登録したのですが、エラーメッセージが登録時に表示 され登録できませんでした。重複したCA認証局の証明書は登録できないようです。したがって、rootCAの証明書は今回はすでに追加されているので追加しませんでした。
image.png

2. API Gatewayのトラストストア設定を更新

S3のVersioning機能を有効化している場合に上記1で上書きした場合は、そのバージョンIDをAPI Gatewayのカスタムドメイン内のmTLSの設定で指定して更新します。また、別名のオブジェクトをS3バケットに格納した場合はパスを変更して登録してください。

上記を実施することでintermediateCA 2 が発行したクライアント証明書による接続が可能となります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GCPとAWS どっちがいい

重要なこと

僕は、インフラメインの人間ではないです。
ポエムだと思ってください

主観の感想

インフラ専門の方はAWSを使われるイメージが強いです
今までの知見とかが溜まっているとかがあるんでしょうか
自分の周りは会社ではAWSを使っているとよく聞く気がします
でも最近GCP勢が増えてきてる気もします
kubernetesとか機械学習が影響しているんでしょうか

個人的にはドキュメントと操作性でGCPのが使いやすく感じました
あとCIとの連携もGCPのがやりやすかったです
慣れの問題だと思いますが、、

特に機械学習をやるときにSageServerとTensorflow周りが使いにくかった印象です
料金面の優越は正直分かりませんが、ある程度のアクセス数が無い限りはどっちでもいいのではと思える額でした。
個人開発で使う分ではGCPの永久無料枠と最初の3万円無料は大きかったです

GCPのai-platformはかなり安いので、colabとか使ってる人は試しに使ってみてもいいかもしれません

僕は最初AWSだったんですけど、機械学習があってGCPに乗り換えました

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む